@telepath-computer/vault-server 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,1287 @@
1
+ /** Parses, serializes, lists, and resolves references for vault notes. */
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join, posix, relative } from "node:path";
5
+ import matter from "gray-matter";
6
+ import { CORE_SCHEMA, load } from "js-yaml";
7
+ import { isMap, parseDocument, stringify as stringifyYaml } from "yaml";
8
+ import { hasDotSegment, toVaultPath } from "./paths.js";
9
+ const invalidFrontmatterError = { code: "invalid_frontmatter", message: "Frontmatter could not be parsed" };
10
+ /** Canonical visible paths and cached record data maintained by watcher events. */
11
+ export class VaultIndex {
12
+ linkFormat;
13
+ vaultRoot;
14
+ fileSystem;
15
+ paths = new Map();
16
+ backlinksByTarget = new Map();
17
+ outboundBySource = new Map();
18
+ suffixRoot = { children: new Map(), paths: new Set() };
19
+ rawBodyFolders;
20
+ pathGeneration = 0;
21
+ linksPathGeneration = 0;
22
+ constructor(vaultRoot, fileSystem = { readFile, stat }, linkFormat = "wikilink", rawBodies = []) {
23
+ this.vaultRoot = vaultRoot;
24
+ this.fileSystem = fileSystem;
25
+ this.linkFormat = linkFormat;
26
+ this.rawBodyFolders = rawBodies.map((path) => toVaultPath(path).replace(/^\/+|\/+$/g, ""));
27
+ }
28
+ get files() { return new Set(this.paths.keys()); }
29
+ get markdownNoteCount() {
30
+ let count = 0;
31
+ for (const entry of this.paths.values())
32
+ if (entry.kind === "record")
33
+ count += 1;
34
+ return count;
35
+ }
36
+ pathFor(absolutePath) { return toVaultPath(relative(this.vaultRoot, absolutePath)); }
37
+ isRawBody(linkingNotePath) {
38
+ const recordPath = noteApiPath(toVaultPath(linkingNotePath).replace(/^\/+/, ""));
39
+ return this.rawBodyFolders.some((folder) => folder === "" || recordPath === folder || recordPath.startsWith(`${folder}/`));
40
+ }
41
+ add(absolutePath) {
42
+ const path = this.pathFor(absolutePath);
43
+ if (!hasDotSegment(path))
44
+ this.addPath(path);
45
+ }
46
+ delete(absolutePath) {
47
+ const path = this.pathFor(absolutePath);
48
+ this.deletePath(path);
49
+ }
50
+ /** Read and cache a record after an add or change event. */
51
+ async refresh(absolutePath) {
52
+ const diskPath = this.pathFor(absolutePath);
53
+ if (hasDotSegment(diskPath))
54
+ return "superseded";
55
+ this.addPath(diskPath);
56
+ if (!diskPath.endsWith(".md")) {
57
+ this.syncLinks(diskPath);
58
+ return "committed";
59
+ }
60
+ const current = this.paths.get(diskPath);
61
+ const entry = current?.kind === "record" ? current : { kind: "record", generation: 0 };
62
+ if (entry !== current)
63
+ this.paths.set(diskPath, entry);
64
+ const generation = entry.generation + 1;
65
+ entry.generation = generation;
66
+ try {
67
+ const bytes = await this.fileSystem.readFile(absolutePath);
68
+ if (!this.isCurrent(diskPath, entry, generation))
69
+ return "superseded";
70
+ const fileStat = await this.fileSystem.stat(absolutePath);
71
+ if (!this.isCurrent(diskPath, entry, generation))
72
+ return "superseded";
73
+ const updated = fileStat.mtime.toISOString();
74
+ const contentHash = hash(bytes);
75
+ if (entry.suppressMatchingWatcherEvent && entry.updated === updated && entry.contentHash === contentHash) {
76
+ delete entry.suppressMatchingWatcherEvent;
77
+ return "unchanged";
78
+ }
79
+ delete entry.suppressMatchingWatcherEvent;
80
+ this.cacheRecord(entry, bytes, updated, contentHash);
81
+ this.syncLinks(diskPath);
82
+ return "committed";
83
+ }
84
+ catch (error) {
85
+ if (!this.isCurrent(diskPath, entry, generation))
86
+ return "superseded";
87
+ if (isMissing(error)) {
88
+ this.deletePath(diskPath);
89
+ this.rebuildLinks();
90
+ return "removed";
91
+ }
92
+ throw error;
93
+ }
94
+ }
95
+ /** Return a structured record entirely from cached data. */
96
+ read(absolutePath) {
97
+ const diskPath = this.pathFor(absolutePath);
98
+ const entry = this.paths.get(diskPath);
99
+ if (entry?.kind === "record" && entry.fields !== undefined && entry.updated !== undefined) {
100
+ return {
101
+ path: noteApiPath(diskPath),
102
+ fields: resolveReferences(entry.fields, this, diskPath),
103
+ ...(entry.body === undefined ? {} : { body: this.isRawBody(diskPath) ? entry.body : serveMarkdownBody(entry.body, this, diskPath) }),
104
+ links: this.linksFor(diskPath),
105
+ updated: entry.updated,
106
+ ...(entry.error ? { error: entry.error } : {}),
107
+ };
108
+ }
109
+ if (entry?.kind === "record" && entry.unreadable)
110
+ throw new InvalidUtf8Error("markdown file is not valid UTF-8");
111
+ throw Object.assign(new Error("record is not indexed"), { code: "ENOENT" });
112
+ }
113
+ /** Update cached record data directly after an atomic API write. */
114
+ updateFromNote(note, absolutePath, rawRecord, source) {
115
+ const diskPath = this.pathFor(absolutePath);
116
+ this.addPath(diskPath);
117
+ const current = this.paths.get(diskPath);
118
+ this.paths.set(diskPath, {
119
+ kind: "record",
120
+ generation: current?.kind === "record" ? current.generation + 1 : 1,
121
+ fields: rawRecord.fields,
122
+ ...(rawRecord.body === undefined ? {} : { body: rawRecord.body }),
123
+ updated: note.updated,
124
+ contentHash: hash(Buffer.from(source)),
125
+ suppressMatchingWatcherEvent: true,
126
+ ...(note.error ? { error: note.error } : {}),
127
+ });
128
+ this.syncLinks(diskPath);
129
+ }
130
+ /** Return this record's outbound links followed by links made to it. */
131
+ linksFor(linkingNotePath) {
132
+ this.ensureLinks();
133
+ const recordPath = noteApiPath(linkingNotePath);
134
+ const outbound = (this.outboundBySource.get(recordPath) ?? []).map((link) => ({ ...link }));
135
+ const backlinks = (this.backlinksByTarget.get(recordPath) ?? [])
136
+ .map((link) => ({ ...link, backlink: true }))
137
+ .sort(compareLinks);
138
+ return [...outbound, ...backlinks];
139
+ }
140
+ /** Return cached note metadata beneath a vault-relative directory. */
141
+ list(directory) {
142
+ const prefix = directory === "" ? "" : `${directory.replace(/\/$/, "")}/`;
143
+ return [...this.paths.entries()]
144
+ .filter(([diskPath]) => noteApiPath(diskPath).startsWith(prefix))
145
+ .flatMap(([diskPath, entry]) => {
146
+ if (entry.kind !== "record" || entry.fields === undefined || entry.updated === undefined)
147
+ return [];
148
+ const fields = resolveReferences(entry.fields, this, diskPath);
149
+ return [{
150
+ path: noteApiPath(diskPath),
151
+ fields,
152
+ ...(typeof entry.body === "string" ? { body: this.isRawBody(diskPath) ? entry.body : serveMarkdownBody(entry.body, this, diskPath) } : {}),
153
+ updated: entry.updated,
154
+ ...(entry.error ? { error: entry.error } : {}),
155
+ }];
156
+ })
157
+ .sort((left, right) => left.path.localeCompare(right.path));
158
+ }
159
+ /** Resolve a wikilink target against the indexed visible files. */
160
+ resolveReference(linkingNotePath, target) {
161
+ const alternatives = [target, `${target}.md`];
162
+ const linkingDirectory = dirname(linkingNotePath);
163
+ for (const desired of alternatives) {
164
+ let node = this.suffixRoot;
165
+ for (const segment of desired.split("/").reverse()) {
166
+ node = node.children.get(segment);
167
+ if (!node)
168
+ break;
169
+ }
170
+ if (!node)
171
+ continue;
172
+ let nearest;
173
+ let nearestDistance = Number.POSITIVE_INFINITY;
174
+ for (const candidate of node.paths) {
175
+ const distance = relative(linkingDirectory, candidate).split(/[\\/]/).length;
176
+ if (distance < nearestDistance || (distance === nearestDistance && (nearest === undefined || candidate.localeCompare(nearest) < 0))) {
177
+ nearest = candidate;
178
+ nearestDistance = distance;
179
+ }
180
+ }
181
+ if (nearest)
182
+ return nearest;
183
+ }
184
+ return null;
185
+ }
186
+ /** Resolve an already vault-relative markdown target without suffix matching. */
187
+ resolveMarkdownReference(target) {
188
+ return [target, `${target}.md`].find((path) => this.paths.has(path)) ?? null;
189
+ }
190
+ addPath(path) {
191
+ if (this.paths.has(path))
192
+ return;
193
+ this.paths.set(path, path.endsWith(".md") ? { kind: "record", generation: 0 } : { kind: "file" });
194
+ this.pathGeneration += 1;
195
+ let node = this.suffixRoot;
196
+ for (const segment of path.split("/").reverse()) {
197
+ let child = node.children.get(segment);
198
+ if (!child) {
199
+ child = { children: new Map(), paths: new Set() };
200
+ node.children.set(segment, child);
201
+ }
202
+ child.paths.add(path);
203
+ node = child;
204
+ }
205
+ }
206
+ deletePath(path) {
207
+ if (!this.paths.delete(path))
208
+ return;
209
+ this.pathGeneration += 1;
210
+ const parents = [];
211
+ let node = this.suffixRoot;
212
+ for (const segment of path.split("/").reverse()) {
213
+ const child = node.children.get(segment);
214
+ if (!child)
215
+ return;
216
+ parents.push({ node, segment, child });
217
+ node = child;
218
+ }
219
+ for (const { child } of parents)
220
+ child.paths.delete(path);
221
+ for (const { node: parent, segment, child } of parents.reverse()) {
222
+ if (child.paths.size === 0)
223
+ parent.children.delete(segment);
224
+ }
225
+ }
226
+ rebuildLinks() {
227
+ this.outboundBySource.clear();
228
+ this.backlinksByTarget.clear();
229
+ for (const [diskPath, entry] of this.paths) {
230
+ if (entry.kind === "record" && entry.fields !== undefined) {
231
+ this.addOutboundLinks(diskPath, { fields: entry.fields, ...(entry.body === undefined ? {} : { body: entry.body }) });
232
+ }
233
+ }
234
+ this.linksPathGeneration = this.pathGeneration;
235
+ }
236
+ syncLinks(diskPath) {
237
+ if (this.linksPathGeneration !== this.pathGeneration)
238
+ return;
239
+ if (!diskPath.endsWith(".md"))
240
+ return;
241
+ const source = noteApiPath(diskPath);
242
+ this.clearOutboundLinks(source);
243
+ const entry = this.paths.get(diskPath);
244
+ if (entry?.kind === "record" && entry.fields !== undefined) {
245
+ this.addOutboundLinks(diskPath, { fields: entry.fields, ...(entry.body === undefined ? {} : { body: entry.body }) });
246
+ }
247
+ }
248
+ ensureLinks() {
249
+ if (this.linksPathGeneration !== this.pathGeneration)
250
+ this.rebuildLinks();
251
+ }
252
+ addOutboundLinks(diskPath, record) {
253
+ const source = noteApiPath(diskPath);
254
+ const links = extractOutboundLinks(record, this, diskPath);
255
+ if (links.length > 0)
256
+ this.outboundBySource.set(source, links);
257
+ for (const link of links) {
258
+ if (link.path === source)
259
+ continue;
260
+ const backlinks = this.backlinksByTarget.get(link.path) ?? [];
261
+ backlinks.push({ path: source, ...(link.field === undefined ? {} : { field: link.field }) });
262
+ this.backlinksByTarget.set(link.path, backlinks);
263
+ }
264
+ }
265
+ clearOutboundLinks(source) {
266
+ const previous = this.outboundBySource.get(source);
267
+ if (!previous)
268
+ return;
269
+ this.outboundBySource.delete(source);
270
+ for (const target of new Set(previous.map(({ path }) => path))) {
271
+ const backlinks = (this.backlinksByTarget.get(target) ?? []).filter(({ path }) => path !== source);
272
+ if (backlinks.length === 0)
273
+ this.backlinksByTarget.delete(target);
274
+ else
275
+ this.backlinksByTarget.set(target, backlinks);
276
+ }
277
+ }
278
+ isCurrent(diskPath, entry, generation) {
279
+ return this.paths.get(diskPath) === entry && entry.generation === generation;
280
+ }
281
+ cacheRecord(entry, bytes, updated, contentHash) {
282
+ entry.updated = updated;
283
+ entry.contentHash = contentHash;
284
+ try {
285
+ const parsed = parseMatter(decodeUtf8(bytes));
286
+ entry.fields = parsed.header;
287
+ if (parsed.body === undefined)
288
+ delete entry.body;
289
+ else
290
+ entry.body = parsed.body;
291
+ if (parsed.invalidFrontmatter)
292
+ entry.error = invalidFrontmatterError;
293
+ else
294
+ delete entry.error;
295
+ delete entry.unreadable;
296
+ }
297
+ catch (error) {
298
+ if (!(error instanceof InvalidUtf8Error))
299
+ throw error;
300
+ delete entry.fields;
301
+ delete entry.body;
302
+ delete entry.error;
303
+ entry.unreadable = true;
304
+ }
305
+ }
306
+ }
307
+ /** Error raised when a markdown file is not valid UTF-8. */
308
+ export class InvalidUtf8Error extends Error {
309
+ }
310
+ /** Error raised when a write uses the reserved $type key incorrectly. */
311
+ export class InvalidReferenceError extends Error {
312
+ }
313
+ /** Return a record from the index's cached parsed data. */
314
+ export async function readNote(_vaultRoot, absolutePath, index) {
315
+ return index.read(absolutePath);
316
+ }
317
+ /** Serialize and atomically replace a note, returning its resulting JSON view. */
318
+ export async function writeNote(vaultRoot, absolutePath, index, record, fieldPatch, bodyTouched = false, preservedBody) {
319
+ await mkdir(dirname(absolutePath), { recursive: true });
320
+ const body = fieldPatch !== undefined && !bodyTouched && preservedBody !== undefined
321
+ ? preservedBody
322
+ : record.body ?? "";
323
+ let source;
324
+ if (fieldPatch === undefined)
325
+ source = serializeNote(record.fields, body, "");
326
+ else {
327
+ const currentSource = await readUtf8(absolutePath);
328
+ const parsed = parseMatter(currentSource);
329
+ source = serializePatchedNote(parsed, record.fields, body, fieldPatch, bodyTouched);
330
+ }
331
+ const temporaryPath = join(dirname(absolutePath), `.${basename(absolutePath)}.${randomUUID()}.tmp`);
332
+ const parsed = parseMatter(source);
333
+ let renamed = false;
334
+ let updated;
335
+ try {
336
+ await writeFile(temporaryPath, source, "utf8");
337
+ updated = (await stat(temporaryPath)).mtime.toISOString();
338
+ await rename(temporaryPath, absolutePath);
339
+ renamed = true;
340
+ }
341
+ finally {
342
+ if (!renamed)
343
+ await unlink(temporaryPath).catch(() => undefined);
344
+ }
345
+ const note = { updated, ...(parsed.invalidFrontmatter ? { error: invalidFrontmatterError } : {}) };
346
+ index.updateFromNote(note, absolutePath, record, source);
347
+ return index.read(absolutePath);
348
+ }
349
+ /** Delete a note from disk. */
350
+ export async function deleteNote(absolutePath, index) {
351
+ await unlink(absolutePath);
352
+ index.delete(absolutePath);
353
+ }
354
+ /** List cached markdown note metadata beneath a vault-relative directory. */
355
+ export function listNotes(index, directory) { return index.list(directory); }
356
+ /** Enforce the API-wide reservation of objects carrying $type. */
357
+ export function validateReferenceObjects(fields) {
358
+ if (Object.hasOwn(fields, "$type")) {
359
+ if (fields.$type === "ref" && typeof fields.path !== "string") {
360
+ throw new InvalidReferenceError("reference path must be a string");
361
+ }
362
+ throw new InvalidReferenceError("invalid $type object");
363
+ }
364
+ for (const value of Object.values(fields))
365
+ validateReferenceValue(value);
366
+ }
367
+ /**
368
+ * Put fields into the form they will be stored in — references written back as
369
+ * the links a file carries — so a caller inspecting a record about to be
370
+ * written judges exactly what lands on disk.
371
+ */
372
+ export function storedFields(fields, previous, index, linkingNotePath, patch) {
373
+ validateReferenceObjects(patch ?? fields);
374
+ return patch === undefined
375
+ ? serializeFieldMap(fields, previous, index, linkingNotePath)
376
+ : serializePatchedFieldMap(fields, previous, patch, index, linkingNotePath);
377
+ }
378
+ /** Canonicalize a submitted body, retaining exact stored bytes on a served-form echo. */
379
+ export function storedBody(body, previous, index, linkingNotePath) {
380
+ if (body.trim().length === 0)
381
+ return undefined;
382
+ if (index.isRawBody(linkingNotePath))
383
+ return body;
384
+ return previous !== undefined && body === serveMarkdownBody(previous, index, linkingNotePath)
385
+ ? previous
386
+ : canonicalizeMarkdownBody(body, linkingNotePath, index);
387
+ }
388
+ /** Apply RFC 7386 object merge semantics. */
389
+ export function mergeFields(target, patch) {
390
+ const result = { ...target };
391
+ for (const [key, value] of Object.entries(patch)) {
392
+ if (value === null)
393
+ delete result[key];
394
+ else if (isObject(value))
395
+ result[key] = mergeFields(isObject(result[key]) ? result[key] : {}, value);
396
+ else
397
+ result[key] = value;
398
+ }
399
+ return result;
400
+ }
401
+ /** Read the raw parsed record used for echo and merge-patch. */
402
+ export async function readRawRecord(absolutePath) {
403
+ const parsed = parseMatter(await readUtf8(absolutePath));
404
+ if (parsed.invalidFrontmatter)
405
+ return { fields: {}, ...(parsed.body === undefined ? {} : { body: parsed.body }), rawBody: parsed.rawBody };
406
+ return { fields: parsed.header, ...(parsed.body === undefined ? {} : { body: parsed.body }), rawBody: parsed.rawBody };
407
+ }
408
+ async function readUtf8(absolutePath, read = readFile) {
409
+ return decodeUtf8(await read(absolutePath));
410
+ }
411
+ function decodeUtf8(bytes) {
412
+ try {
413
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
414
+ }
415
+ catch {
416
+ throw new InvalidUtf8Error("markdown file is not valid UTF-8");
417
+ }
418
+ }
419
+ function parseMatter(source) {
420
+ try {
421
+ // gray-matter caches failures by source and returns a different result on
422
+ // the next parse. Always bypass its global cache.
423
+ const parsed = matter(source, { engines: { yaml: (header) => load(header, { schema: CORE_SCHEMA }) } });
424
+ if (!isObject(parsed.data))
425
+ throw new Error("header is not a map");
426
+ return {
427
+ header: parsed.data,
428
+ ...(parsed.content.trim().length === 0 ? {} : { body: parsed.content }),
429
+ invalidFrontmatter: false,
430
+ rawHeader: parsed.matter,
431
+ rawBody: parsed.content,
432
+ };
433
+ }
434
+ catch {
435
+ return { header: {}, ...(source.length === 0 ? {} : { body: source }), invalidFrontmatter: true, rawHeader: "", rawBody: source };
436
+ }
437
+ }
438
+ function serializeNote(header, body, originalBody) {
439
+ const stringifyMatter = matter.stringify;
440
+ let source = Object.keys(header).length === 0 ? body : stringifyMatter(body, header, { noCompatMode: true });
441
+ if (!body.endsWith("\n") && source.endsWith("\n"))
442
+ source = source.slice(0, -1);
443
+ if (body === "" && originalBody === "" && source.endsWith("\n"))
444
+ source = source.slice(0, -1);
445
+ return source;
446
+ }
447
+ function serializePatchedNote(parsed, header, body, patch, bodyTouched) {
448
+ const changedKeys = new Set(Object.keys(patch));
449
+ if (changedKeys.size === 0) {
450
+ if (bodyTouched)
451
+ return serializeHeaderAndBody(parsed.rawHeader, header, body);
452
+ throw new Error("empty patch must not be written");
453
+ }
454
+ const document = parseDocument(parsed.rawHeader, { keepSourceTokens: true });
455
+ if (!isMap(document.contents))
456
+ return serializeNote(header, body, parsed.body ?? "");
457
+ const replacements = [];
458
+ collectMapReplacements(parsed.rawHeader, document.contents, header, Object.fromEntries([...changedKeys].map((key) => [key, patch[key]])), replacements);
459
+ let rawHeader = parsed.rawHeader;
460
+ for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
461
+ rawHeader = rawHeader.slice(0, replacement.start) + replacement.text + rawHeader.slice(replacement.end);
462
+ }
463
+ // Splicing text into a header is only worth doing while it keeps the header
464
+ // readable. If an edit ever produces something that no longer parses, write
465
+ // the record whole rather than leaving an unreadable file on disk.
466
+ if (!parsesAsMap(rawHeader))
467
+ return serializeNote(header, body, parsed.body ?? "");
468
+ return serializeHeaderAndBody(rawHeader, header, body);
469
+ }
470
+ function collectMapReplacements(source, map, target, patch, replacements) {
471
+ const pairs = map.items;
472
+ const pairByKey = new Map();
473
+ pairs.forEach((pair, index) => pairByKey.set(String(pair.key && "value" in pair.key ? pair.key.value : pair.key), { pair, index }));
474
+ const mapRange = map.range;
475
+ if (source[mapRange[0]] === "{" && pairs.length > 0 && [...pairByKey.keys()].every((key) => !(key in target))) {
476
+ const contents = Object.entries(target).map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)}`).join(", ");
477
+ replacements.push({ start: mapRange[0] + 1, end: mapRange[1] - 1, text: contents });
478
+ return;
479
+ }
480
+ for (const [key, patchValue] of Object.entries(patch)) {
481
+ const found = pairByKey.get(key);
482
+ if (!found)
483
+ continue;
484
+ if (!(key in target)) {
485
+ replacements.push({ ...pairRange(source, map, pairs, found.index), text: "" });
486
+ continue;
487
+ }
488
+ if (isObject(patchValue) && isMap(found.pair.value) && isObject(target[key])) {
489
+ collectMapReplacements(source, found.pair.value, target[key], patchValue, replacements);
490
+ continue;
491
+ }
492
+ const range = found.pair.value?.range;
493
+ if (range) {
494
+ // An empty value's range starts right after the colon (`draft:`), so
495
+ // splicing without a space would produce `draft:true` — a plain scalar
496
+ // that breaks the whole header.
497
+ const prefix = source[range[0] - 1] === ":" ? " " : "";
498
+ const suffix = source[range[1]] === "#" ? " " : "";
499
+ // A block collection's range runs to the end of its last line, so the
500
+ // replaced span carries the newline separating it from the next key.
501
+ // Dropping it would run the new value into that key.
502
+ const trailing = source.slice(range[0], range[1]).endsWith("\n") ? "\n" : "";
503
+ const value = renderPatchedValue(source, range[0], target[key]);
504
+ replacements.push({ start: range[0], end: range[1], text: `${prefix}${value}${suffix}${trailing}` });
505
+ }
506
+ else {
507
+ const end = found.pair.key?.range?.[1];
508
+ if (end !== undefined)
509
+ replacements.push({ start: end, end, text: `: ${JSON.stringify(target[key])}` });
510
+ }
511
+ }
512
+ const additions = Object.keys(patch).filter((key) => !pairByKey.has(key) && key in target);
513
+ if (additions.length === 0)
514
+ return;
515
+ const range = mapRange;
516
+ const flow = source[range[0]] === "{";
517
+ const text = additions.map((key) => `${JSON.stringify(key)}: ${JSON.stringify(target[key])}`).join(flow ? ", " : `\n${mapIndent(source, pairs)}`);
518
+ if (flow) {
519
+ const close = source.lastIndexOf("}", range[1]);
520
+ replacements.push({ start: close, end: close, text: `${pairs.length === 0 ? "" : ", "}${text}` });
521
+ }
522
+ else {
523
+ let insertion = range[1];
524
+ while (insertion > range[0] && source[insertion - 1] === "\n")
525
+ insertion -= 1;
526
+ const indent = mapIndent(source, pairs);
527
+ replacements.push({ start: insertion, end: insertion, text: `${insertion === range[0] ? "" : "\n"}${indent}${text}` });
528
+ }
529
+ }
530
+ function mapIndent(source, pairs) {
531
+ const start = pairs[0]?.key?.range?.[0] ?? 0;
532
+ const lineStart = source.lastIndexOf("\n", start - 1) + 1;
533
+ return source.slice(lineStart, start).match(/^\s*/)?.[0] ?? "";
534
+ }
535
+ /**
536
+ * Render a patched value where its old text sat. A value on its own line is a
537
+ * block, and stays one — inline JSON there would flatten a nested structure
538
+ * into a single unreadable line in a file people edit by hand. A value sharing
539
+ * its key's line stays inline.
540
+ */
541
+ function parsesAsMap(rawHeader) {
542
+ try {
543
+ return isObject(load(rawHeader, { schema: CORE_SCHEMA }));
544
+ }
545
+ catch {
546
+ return false;
547
+ }
548
+ }
549
+ function renderPatchedValue(source, start, value) {
550
+ if (value === null || typeof value !== "object")
551
+ return JSON.stringify(value);
552
+ const indent = source.slice(source.lastIndexOf("\n", start - 1) + 1, start);
553
+ if (indent.trim() !== "")
554
+ return JSON.stringify(value);
555
+ return stringifyYaml(value, { indent: 2 }).trimEnd().split("\n").join(`\n${indent}`);
556
+ }
557
+ function serializeHeaderAndBody(rawHeader, header, body) {
558
+ if (Object.keys(header).length === 0)
559
+ return body;
560
+ const normalized = rawHeader.replace(/^\n/, "").replace(/\n?$/, "\n");
561
+ return `---\n${normalized}---\n${body}`;
562
+ }
563
+ function pairRange(source, map, pairs, index) {
564
+ const pair = pairs[index];
565
+ const flow = source[map.range?.[0] ?? -1] === "{";
566
+ let start = pair.key?.range?.[0] ?? 0;
567
+ let end = pair.value?.range?.[2] ?? pair.value?.range?.[1] ?? pair.key?.range?.[1] ?? start;
568
+ if (flow) {
569
+ const commaBefore = source.lastIndexOf(",", start);
570
+ const openBefore = source.lastIndexOf("{", start);
571
+ if (commaBefore > openBefore)
572
+ start = commaBefore;
573
+ else {
574
+ const commaAfter = source.indexOf(",", end);
575
+ if (commaAfter >= 0 && commaAfter < (map.range?.[1] ?? end))
576
+ end = commaAfter + 1;
577
+ }
578
+ }
579
+ return { start, end };
580
+ }
581
+ function resolveReferences(value, index, linkingNotePath) {
582
+ if (typeof value === "string") {
583
+ // A link is a reference because of how it is written, not because of what
584
+ // happens to exist, and only the vault's configured syntax is recognized.
585
+ const link = readFieldReference(value, index, linkingNotePath);
586
+ if (!link)
587
+ return value;
588
+ return { $type: "ref", path: link.path, ...(link.rendered === link.path ? {} : { label: link.rendered }) };
589
+ }
590
+ if (Array.isArray(value))
591
+ return value.map((item) => resolveReferences(item, index, linkingNotePath));
592
+ if (isObject(value))
593
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveReferences(item, index, linkingNotePath)]));
594
+ return value;
595
+ }
596
+ function serializeReferences(value, previous, vaultIndex, linkingNotePath) {
597
+ if (Array.isArray(value))
598
+ return value.map((item, index) => serializeReferences(item, Array.isArray(previous) ? previous[index] : undefined, vaultIndex, linkingNotePath));
599
+ if (isObject(value)) {
600
+ if (Object.hasOwn(value, "$type")) {
601
+ assertReferenceObject(value);
602
+ const rendered = value.label ?? value.path;
603
+ const oldLink = readFieldReference(previous, vaultIndex, linkingNotePath);
604
+ if (oldLink
605
+ && resolvedSubmittedReferencePath(vaultIndex, linkingNotePath, value.path) === oldLink.path
606
+ && rendered === oldLink.rendered)
607
+ return previous;
608
+ return vaultIndex.linkFormat === "wikilink"
609
+ ? `[[${value.path}${value.label === undefined ? "" : `|${value.label}`}]]`
610
+ : serializeMarkdownReference(value.path, value.label, linkingNotePath);
611
+ }
612
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
613
+ key,
614
+ serializeReferences(item, isObject(previous) ? previous[key] : undefined, vaultIndex, linkingNotePath),
615
+ ]));
616
+ }
617
+ return value;
618
+ }
619
+ function serializeFieldMap(fields, previous, vaultIndex, linkingNotePath) {
620
+ return Object.fromEntries(Object.entries(fields).map(([key, value]) => [
621
+ key,
622
+ serializeReferences(value, previous?.[key], vaultIndex, linkingNotePath),
623
+ ]));
624
+ }
625
+ /**
626
+ * Convert only values supplied by a merge-patch. Values absent from the patch
627
+ * retain their raw stored form, including mappings that use the reserved
628
+ * `$type` key as ordinary pre-existing data.
629
+ */
630
+ function serializePatchedFieldMap(fields, previous, patch, vaultIndex, linkingNotePath) {
631
+ return Object.fromEntries(Object.entries(fields).map(([key, value]) => {
632
+ if (!Object.hasOwn(patch, key)) {
633
+ return [key, previous && Object.hasOwn(previous, key) ? previous[key] : serializeReferences(value, undefined, vaultIndex, linkingNotePath)];
634
+ }
635
+ return [key, serializePatchedValue(value, previous?.[key], patch[key], vaultIndex, linkingNotePath)];
636
+ }));
637
+ }
638
+ function serializePatchedValue(value, previous, submitted, vaultIndex, linkingNotePath) {
639
+ if (!isObject(submitted) || Array.isArray(submitted))
640
+ return serializeReferences(value, previous, vaultIndex, linkingNotePath);
641
+ if (Object.hasOwn(submitted, "$type"))
642
+ return serializeReferences(value, previous, vaultIndex, linkingNotePath);
643
+ // A partial PATCH of a served reference merges into its object view, but the
644
+ // corresponding stored value is a scalar link and must be serialized as one
645
+ // complete reference at this position.
646
+ if (isObject(value) && Object.hasOwn(value, "$type") && readFieldReference(previous, vaultIndex, linkingNotePath)) {
647
+ return serializeReferences(value, previous, vaultIndex, linkingNotePath);
648
+ }
649
+ if (!isObject(value))
650
+ return serializeReferences(value, previous, vaultIndex, linkingNotePath);
651
+ const previousMap = isObject(previous) ? previous : undefined;
652
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
653
+ if (!Object.hasOwn(submitted, key)) {
654
+ return [key, previousMap && Object.hasOwn(previousMap, key) ? previousMap[key] : serializeReferences(item, undefined, vaultIndex, linkingNotePath)];
655
+ }
656
+ return [key, serializePatchedValue(item, previousMap?.[key], submitted[key], vaultIndex, linkingNotePath)];
657
+ }));
658
+ }
659
+ function validateReferenceValue(value) {
660
+ if (Array.isArray(value)) {
661
+ for (const item of value)
662
+ validateReferenceValue(item);
663
+ return;
664
+ }
665
+ if (!isObject(value))
666
+ return;
667
+ if (Object.hasOwn(value, "$type")) {
668
+ assertReferenceObject(value);
669
+ return;
670
+ }
671
+ for (const item of Object.values(value))
672
+ validateReferenceValue(item);
673
+ }
674
+ function isReferenceObject(value) {
675
+ const keys = Object.keys(value);
676
+ return value.$type === "ref"
677
+ && typeof value.path === "string"
678
+ && (keys.length === 2 || (keys.length === 3 && typeof value.label === "string"))
679
+ && keys.every((key) => key === "$type" || key === "path" || key === "label");
680
+ }
681
+ function assertReferenceObject(value) {
682
+ if (value.$type === "ref" && typeof value.path !== "string") {
683
+ throw new InvalidReferenceError("reference path must be a string");
684
+ }
685
+ if (!isReferenceObject(value))
686
+ throw new InvalidReferenceError("invalid $type object");
687
+ }
688
+ function parseWikilink(value) {
689
+ if (typeof value !== "string")
690
+ return null;
691
+ if (!value.startsWith("[[") || !value.endsWith("]]"))
692
+ return null;
693
+ const contents = value.slice(2, -2);
694
+ if (!isWikilinkContents(contents))
695
+ return null;
696
+ const alias = contents.indexOf("|");
697
+ return alias < 0
698
+ ? { target: contents, rendered: contents }
699
+ : { target: contents.slice(0, alias), rendered: contents.slice(alias + 1) };
700
+ }
701
+ function readFieldReference(value, index, linkingNotePath) {
702
+ if (index.linkFormat === "wikilink") {
703
+ const link = parseWikilink(value);
704
+ if (!link)
705
+ return null;
706
+ return { path: resolvedWikilinkPath(index, linkingNotePath, link.target), rendered: link.rendered };
707
+ }
708
+ if (typeof value !== "string")
709
+ return null;
710
+ const link = inlineMarkdownLinkAt(value, 0);
711
+ if (!link || link.end !== value.length)
712
+ return null;
713
+ const destination = markdownDestinationValue(link.destination);
714
+ const decoded = destination === null ? null : decodeMarkdownTarget(destination);
715
+ if (destination === null || decoded === null || !isInternalMarkdownDestination(destination))
716
+ return null;
717
+ const target = positionalWikilinkTarget(linkingNotePath, decoded);
718
+ return {
719
+ path: resolvedMarkdownPath(index, linkingNotePath, target),
720
+ rendered: markdownLabelValue(value.slice(link.labelStart, link.labelEnd)),
721
+ };
722
+ }
723
+ function extractOutboundLinks(record, index, linkingNotePath) {
724
+ const links = [];
725
+ for (const [field, value] of Object.entries(record.fields)) {
726
+ collectFieldLinks(value, field, index, linkingNotePath, links);
727
+ }
728
+ if (record.body !== undefined && !index.isRawBody(linkingNotePath)) {
729
+ links.push(...extractBodyLinks(record.body, index, linkingNotePath));
730
+ }
731
+ return links;
732
+ }
733
+ function collectFieldLinks(value, field, index, linkingNotePath, links) {
734
+ if (typeof value === "string") {
735
+ const path = resolvedFieldRecordPath(value, index, linkingNotePath);
736
+ if (path !== null)
737
+ links.push({ path, field });
738
+ return;
739
+ }
740
+ if (Array.isArray(value)) {
741
+ for (const item of value)
742
+ collectFieldLinks(item, field, index, linkingNotePath, links);
743
+ return;
744
+ }
745
+ if (isObject(value)) {
746
+ for (const item of Object.values(value))
747
+ collectFieldLinks(item, field, index, linkingNotePath, links);
748
+ }
749
+ }
750
+ function resolvedFieldRecordPath(value, index, linkingNotePath) {
751
+ if (index.linkFormat === "wikilink") {
752
+ const link = parseWikilink(value);
753
+ return link === null ? null : resolvedWikilinkRecordPath(index, linkingNotePath, link.target);
754
+ }
755
+ const link = inlineMarkdownLinkAt(value, 0);
756
+ if (!link || link.end !== value.length)
757
+ return null;
758
+ return resolvedMarkdownRecordPath(index, linkingNotePath, link.destination);
759
+ }
760
+ function resolvedWikilinkRecordPath(index, linkingNotePath, target) {
761
+ const { wanted } = splitReferenceTarget(target);
762
+ if (wanted === "")
763
+ return noteApiPath(linkingNotePath);
764
+ const resolved = index.resolveReference(linkingNotePath, wanted);
765
+ return resolved?.endsWith(".md") ? noteApiPath(resolved) : null;
766
+ }
767
+ function resolvedMarkdownRecordPath(index, linkingNotePath, rawDestination) {
768
+ const destination = markdownDestinationValue(rawDestination);
769
+ const decoded = destination === null ? null : decodeMarkdownTarget(destination);
770
+ if (destination === null || decoded === null || !isInternalMarkdownDestination(destination))
771
+ return null;
772
+ const { wanted } = positionalWikilinkTarget(linkingNotePath, decoded);
773
+ if (wanted === "")
774
+ return noteApiPath(linkingNotePath);
775
+ const resolved = index.resolveMarkdownReference(wanted);
776
+ return resolved?.endsWith(".md") ? noteApiPath(resolved) : null;
777
+ }
778
+ function serializeMarkdownReference(path, label, linkingNotePath) {
779
+ const { wanted, suffix } = splitReferenceTarget(path);
780
+ const destination = { wanted: relativeReferenceDestination(linkingNotePath, wanted), suffix };
781
+ return `[${escapeMarkdownLabel(label ?? path)}](${formatMarkdownTarget(destination)})`;
782
+ }
783
+ function resolvedSubmittedReferencePath(index, linkingNotePath, target) {
784
+ return index.linkFormat === "wikilink"
785
+ ? resolvedWikilinkPath(index, linkingNotePath, target)
786
+ : resolvedMarkdownPath(index, linkingNotePath, splitReferenceTarget(target));
787
+ }
788
+ function resolvedWikilinkPath(index, linkingNotePath, target) {
789
+ const { wanted, suffix } = splitReferenceTarget(target);
790
+ if (wanted === "")
791
+ return `${noteApiPath(linkingNotePath)}${suffix}`;
792
+ const resolved = index.resolveReference(linkingNotePath, wanted);
793
+ return `${resolved === null ? wanted : noteApiPath(resolved)}${suffix}`;
794
+ }
795
+ function resolvedMarkdownPath(index, linkingNotePath, { wanted, suffix }) {
796
+ if (wanted === "")
797
+ return `${noteApiPath(linkingNotePath)}${suffix}`;
798
+ const resolved = index.resolveMarkdownReference(wanted);
799
+ return `${resolved === null ? wanted : noteApiPath(resolved)}${suffix}`;
800
+ }
801
+ function canonicalizeMarkdownBody(body, linkingNotePath, index) {
802
+ return transformMarkdownBody(body, linkingNotePath, (source, path) => canonicalizeInlineLinks(source, path, index));
803
+ }
804
+ function serveMarkdownBody(body, index, linkingNotePath) {
805
+ return transformMarkdownBody(body, linkingNotePath, (source, path) => serveInlineWikilinks(source, index, path));
806
+ }
807
+ function extractBodyLinks(body, index, linkingNotePath) {
808
+ const links = [];
809
+ transformMarkdownBody(body, linkingNotePath, (source, path) => transformInlineProse(source, path, "inspect", index, (link) => {
810
+ const target = link.syntax === "wikilink"
811
+ ? resolvedWikilinkRecordPath(index, path, link.target)
812
+ : resolvedMarkdownRecordPath(index, path, link.destination);
813
+ if (target !== null)
814
+ links.push({ path: target });
815
+ return undefined;
816
+ }));
817
+ return links;
818
+ }
819
+ function transformMarkdownBody(body, linkingNotePath, transformInline) {
820
+ let output = "";
821
+ let plain = "";
822
+ let fence;
823
+ let htmlBlock;
824
+ for (let start = 0; start < body.length;) {
825
+ const end = markdownLineEnd(body, start);
826
+ const fullLine = body.slice(start, end);
827
+ const line = fullLine.replace(/(?:\r\n|\r|\n)$/, "");
828
+ if (fence) {
829
+ output += fullLine;
830
+ if (closesFence(line, fence.marker, fence.length))
831
+ fence = undefined;
832
+ }
833
+ else if (htmlBlock) {
834
+ output += fullLine;
835
+ if (endsHtmlBlock(line, htmlBlock.end))
836
+ htmlBlock = undefined;
837
+ }
838
+ else {
839
+ const opening = markdownContainerContent(line).match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
840
+ if (opening?.[1] && (opening[1][0] !== "`" || !opening[2]?.includes("`"))) {
841
+ output += transformInline(plain, linkingNotePath) + fullLine;
842
+ plain = "";
843
+ fence = { marker: opening[1][0], length: opening[1].length };
844
+ }
845
+ else {
846
+ const html = htmlBlockStart(line, plain.length === 0 || endsWithBlankLine(plain));
847
+ if (html) {
848
+ output += transformInline(plain, linkingNotePath) + fullLine;
849
+ plain = "";
850
+ if (!endsHtmlBlock(line, html.end))
851
+ htmlBlock = html;
852
+ }
853
+ else
854
+ plain += fullLine;
855
+ }
856
+ }
857
+ start = end;
858
+ }
859
+ return output + transformInline(plain, linkingNotePath);
860
+ }
861
+ function markdownLineEnd(source, start) {
862
+ let end = start;
863
+ while (end < source.length && source[end] !== "\n" && source[end] !== "\r")
864
+ end += 1;
865
+ if (source[end] === "\r" && source[end + 1] === "\n")
866
+ return end + 2;
867
+ return end < source.length ? end + 1 : end;
868
+ }
869
+ function endsWithBlankLine(source) {
870
+ const lines = source.split(/\r\n|\r|\n/);
871
+ return lines.length > 1 && lines[lines.length - 2]?.trim() === "";
872
+ }
873
+ function htmlBlockStart(line, allowTypeSeven) {
874
+ const special = line.match(/^ {0,3}<(script|pre|style|textarea)(?:[ \t>]|$)/i)?.[1];
875
+ if (special)
876
+ return { end: new RegExp(`</${special}[ \\t]*>`, "i") };
877
+ if (/^ {0,3}<!--/.test(line))
878
+ return { end: /-->/ };
879
+ if (/^ {0,3}<\?/.test(line))
880
+ return { end: /\?>/ };
881
+ if (/^ {0,3}<!\[CDATA\[/.test(line))
882
+ return { end: /\]\]>/ };
883
+ if (/^ {0,3}<![A-Z]/.test(line))
884
+ return { end: />/ };
885
+ const blockTag = /^(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)$/i;
886
+ const tag = line.match(/^ {0,3}<\/?([^ \t/>]+)/)?.[1];
887
+ if (tag && blockTag.test(tag))
888
+ return { end: "blank" };
889
+ const completeTag = /^ {0,3}(?:<[A-Za-z][A-Za-z0-9-]*(?:[ \t]+[A-Za-z_:][A-Za-z0-9_.:-]*(?:[ \t]*=[ \t]*(?:[^ "'=<>`]+|'[^']*'|"[^"]*"))?)*[ \t]*\/?>|<\/[A-Za-z][A-Za-z0-9-]*[ \t]*>)[ \t]*$/;
890
+ return allowTypeSeven && completeTag.test(line) ? { end: "blank" } : null;
891
+ }
892
+ function endsHtmlBlock(line, end) {
893
+ return end === "blank" ? line.trim() === "" : end.test(line);
894
+ }
895
+ function closesFence(line, marker, minimumLength) {
896
+ const candidate = markdownContainerContent(line).match(/^[ \t]*(\S+)[ \t]*$/)?.[1];
897
+ return candidate !== undefined && candidate.length >= minimumLength && [...candidate].every((character) => character === marker);
898
+ }
899
+ function markdownContainerContent(line) {
900
+ let content = line;
901
+ while (true) {
902
+ const quote = content.match(/^ {0,3}>[ \t]?/);
903
+ if (quote) {
904
+ content = content.slice(quote[0].length);
905
+ continue;
906
+ }
907
+ const list = content.match(/^ {0,3}(?:[*+-]|\d{1,9}[.)])(?:[ \t]+|$)/);
908
+ if (list) {
909
+ content = content.slice(list[0].length);
910
+ continue;
911
+ }
912
+ return content;
913
+ }
914
+ }
915
+ function canonicalizeInlineLinks(source, linkingNotePath, index) {
916
+ return transformInlineProse(source, linkingNotePath, index.linkFormat, index);
917
+ }
918
+ function serveInlineWikilinks(source, index, linkingNotePath) {
919
+ return transformInlineProse(source, linkingNotePath, "serve", index);
920
+ }
921
+ function transformInlineProse(source, linkingNotePath, mode, index, onLink) {
922
+ let result = "";
923
+ let codeTicks;
924
+ for (let cursor = 0; cursor < source.length;) {
925
+ if (source[cursor] === "`") {
926
+ let end = cursor + 1;
927
+ while (source[end] === "`")
928
+ end += 1;
929
+ const run = end - cursor;
930
+ if (codeTicks === undefined && hasClosingBackticks(source, end, run))
931
+ codeTicks = run;
932
+ else if (codeTicks === run)
933
+ codeTicks = undefined;
934
+ result += source.slice(cursor, end);
935
+ cursor = end;
936
+ continue;
937
+ }
938
+ if (codeTicks === undefined && source[cursor] === "\\" && /[!-/:-@[-`{-~]/.test(source[cursor + 1] ?? "")) {
939
+ result += source.slice(cursor, cursor + 2);
940
+ cursor += 2;
941
+ continue;
942
+ }
943
+ if (codeTicks === undefined) {
944
+ const definitionEnd = referenceDefinitionEnd(source, cursor);
945
+ if (definitionEnd !== null) {
946
+ result += source.slice(cursor, definitionEnd);
947
+ cursor = definitionEnd;
948
+ continue;
949
+ }
950
+ }
951
+ if (codeTicks === undefined && source[cursor] === "<") {
952
+ const end = inlineHtmlEnd(source, cursor);
953
+ if (end !== null) {
954
+ result += source.slice(cursor, end);
955
+ cursor = end;
956
+ continue;
957
+ }
958
+ }
959
+ if (codeTicks === undefined) {
960
+ const wikilink = bodyWikilinkAt(source, cursor);
961
+ if (wikilink) {
962
+ onLink?.({ syntax: "wikilink", target: wikilink.target });
963
+ result += mode === "inspect"
964
+ ? source.slice(cursor, wikilink.end)
965
+ : mode === "serve"
966
+ ? renderBodyReference(wikilink, index, linkingNotePath)
967
+ : mode === "markdown"
968
+ ? renderBodyReference(wikilink, index, linkingNotePath)
969
+ : source.slice(cursor, wikilink.end);
970
+ cursor = wikilink.end;
971
+ continue;
972
+ }
973
+ const image = source[cursor] === "!" && source[cursor + 1] === "[";
974
+ const markdown = inlineMarkdownLinkAt(source, image ? cursor + 1 : cursor);
975
+ if (markdown) {
976
+ onLink?.({ syntax: "markdown", destination: markdown.destination });
977
+ if (mode === "serve" || mode === "inspect")
978
+ result += source.slice(cursor, markdown.end);
979
+ else if (mode === "wikilink") {
980
+ const destination = markdownDestinationValue(markdown.destination);
981
+ const decoded = destination === null ? null : decodeMarkdownTarget(destination);
982
+ if (destination === null || decoded === null || !isInternalMarkdownDestination(destination))
983
+ result += source.slice(cursor, markdown.end);
984
+ else {
985
+ const target = positionalWikilinkTarget(linkingNotePath, decoded);
986
+ const label = markdownLabelValue(source.slice(markdown.labelStart, markdown.labelEnd));
987
+ const targetText = `${target.wanted}${target.suffix}`;
988
+ result += isWikilinkComponent(targetText) && isWikilinkComponent(label)
989
+ ? `${image ? "!" : ""}[[${targetText}${label === targetText ? "" : `|${label}`}]]`
990
+ : source.slice(cursor, markdown.end);
991
+ }
992
+ }
993
+ else
994
+ result += rewriteRootedMarkdownDestination(source, cursor, markdown, linkingNotePath);
995
+ cursor = markdown.end;
996
+ continue;
997
+ }
998
+ }
999
+ result += source[cursor];
1000
+ cursor += 1;
1001
+ }
1002
+ return result;
1003
+ }
1004
+ function bodyWikilinkAt(source, start) {
1005
+ const image = source[start] === "!";
1006
+ const opening = image ? start + 1 : start;
1007
+ if (!source.startsWith("[[", opening))
1008
+ return null;
1009
+ const close = source.indexOf("]]", opening + 2);
1010
+ if (close < 0)
1011
+ return null;
1012
+ const contents = source.slice(opening + 2, close);
1013
+ if (!isWikilinkContents(contents))
1014
+ return null;
1015
+ const alias = contents.indexOf("|");
1016
+ return {
1017
+ target: alias < 0 ? contents : contents.slice(0, alias),
1018
+ rendered: alias < 0 ? contents : contents.slice(alias + 1),
1019
+ image,
1020
+ end: close + 2,
1021
+ };
1022
+ }
1023
+ function renderBodyReference(link, index, linkingNotePath) {
1024
+ const { wanted, suffix } = splitReferenceTarget(link.target);
1025
+ const resolved = index.resolveReference(linkingNotePath, wanted);
1026
+ const destination = {
1027
+ wanted: resolved === null ? wanted : relativeReferenceDestination(linkingNotePath, noteApiPath(resolved)),
1028
+ suffix,
1029
+ };
1030
+ return `${link.image ? "!" : ""}[${escapeMarkdownLabel(link.rendered)}](${formatMarkdownTarget(destination)})`;
1031
+ }
1032
+ function escapeMarkdownLabel(label) { return label.replace(/[\\`*_\[\]<>&]/g, "\\$&"); }
1033
+ function formatMarkdownDestination(destination) {
1034
+ return encodeURI(destination).replace(/[()]/g, "\\$&");
1035
+ }
1036
+ function formatMarkdownTarget({ wanted, suffix }) {
1037
+ const encodedSuffix = formatMarkdownDestination(suffix).replace(/%25(?=[0-9A-Fa-f]{2})/g, "%");
1038
+ return `${formatMarkdownDestination(wanted)}${encodedSuffix}`;
1039
+ }
1040
+ function referenceDefinitionEnd(source, start) {
1041
+ if (start > 0 && source[start - 1] !== "\n" && source[start - 1] !== "\r")
1042
+ return null;
1043
+ let end = markdownLineEnd(source, start);
1044
+ const line = source.slice(start, end).replace(/(?:\r\n|\r|\n)$/, "");
1045
+ if (!/^ {0,3}\[(?:\\.|[^\]\r\n])+\]:/.test(line))
1046
+ return null;
1047
+ while (end < source.length) {
1048
+ const next = markdownLineEnd(source, end);
1049
+ const continuation = source.slice(end, next).replace(/(?:\r\n|\r|\n)$/, "");
1050
+ if (!/^[ \t]+\S/.test(continuation))
1051
+ break;
1052
+ end = next;
1053
+ }
1054
+ return end;
1055
+ }
1056
+ function inlineMarkdownLinkAt(source, openBracket) {
1057
+ if (source[openBracket] !== "[")
1058
+ return null;
1059
+ let depth = 0;
1060
+ for (let cursor = openBracket; cursor < source.length; cursor += 1) {
1061
+ if (source[cursor] === "\\") {
1062
+ cursor += 1;
1063
+ continue;
1064
+ }
1065
+ if (source[cursor] === "[")
1066
+ depth += 1;
1067
+ else if (source[cursor] === "]") {
1068
+ depth -= 1;
1069
+ if (depth !== 0)
1070
+ continue;
1071
+ if (source[cursor + 1] !== "(")
1072
+ return null;
1073
+ const destination = inlineDestination(source, cursor);
1074
+ if (!destination)
1075
+ return null;
1076
+ return {
1077
+ labelStart: openBracket + 1,
1078
+ labelEnd: cursor,
1079
+ destinationStart: destination.start,
1080
+ destinationEnd: destination.end,
1081
+ destination: source.slice(destination.start, destination.end),
1082
+ end: destination.close + 1,
1083
+ };
1084
+ }
1085
+ }
1086
+ return null;
1087
+ }
1088
+ function rewriteRootedMarkdownDestination(source, start, link, linkingNotePath) {
1089
+ const destination = markdownDestinationValue(link.destination);
1090
+ const target = destination === null ? null : decodeMarkdownTarget(destination);
1091
+ if (destination === null || target === null || !target.wanted.startsWith("/") || destination.startsWith("//")) {
1092
+ return source.slice(start, link.end);
1093
+ }
1094
+ const relativeTarget = rootedMarkdownTarget(linkingNotePath, target);
1095
+ return source.slice(start, link.destinationStart)
1096
+ + formatMarkdownTarget(relativeTarget)
1097
+ + source.slice(link.destinationEnd, link.end);
1098
+ }
1099
+ function markdownDestinationValue(destination) {
1100
+ return destination.replace(/\\([!-/:-@[-`{-~])/g, "$1");
1101
+ }
1102
+ function decodeMarkdownTarget(destination) {
1103
+ const { wanted, suffix } = splitReferenceTarget(destination);
1104
+ try {
1105
+ return { wanted: decodeURIComponent(wanted), suffix };
1106
+ }
1107
+ catch {
1108
+ return null;
1109
+ }
1110
+ }
1111
+ function markdownLabelValue(label) {
1112
+ return label.replace(/\\([!-/:-@[-`{-~])/g, "$1");
1113
+ }
1114
+ function isInternalMarkdownDestination(destination) {
1115
+ return !destination.startsWith("//") && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(destination);
1116
+ }
1117
+ function positionalWikilinkTarget(linkingNotePath, { wanted: pathname, suffix }) {
1118
+ const rooted = pathname.startsWith("/");
1119
+ const relativePath = rooted ? pathname.slice(1) : pathname;
1120
+ if (rooted && relativePath === "")
1121
+ return rootedMarkdownTarget(linkingNotePath, { wanted: "/", suffix });
1122
+ if (!rooted && relativePath === "")
1123
+ return { wanted: "", suffix };
1124
+ const normalized = rooted ? posix.normalize(relativePath) : posix.normalize(posix.join(posix.dirname(linkingNotePath), relativePath));
1125
+ const target = clampVaultPath(normalized);
1126
+ return { wanted: target, suffix };
1127
+ }
1128
+ function clampVaultPath(path) {
1129
+ let clamped = path;
1130
+ while (clamped === ".." || clamped.startsWith("../"))
1131
+ clamped = clamped === ".." ? "" : clamped.slice(3);
1132
+ return clamped;
1133
+ }
1134
+ function hasClosingBackticks(source, start, length) {
1135
+ for (let index = source.indexOf("`", start); index >= 0; index = source.indexOf("`", index)) {
1136
+ let end = index + 1;
1137
+ while (source[end] === "`")
1138
+ end += 1;
1139
+ if (end - index === length)
1140
+ return true;
1141
+ index = end;
1142
+ }
1143
+ return false;
1144
+ }
1145
+ function inlineHtmlEnd(source, start) {
1146
+ if (source.startsWith("<!--", start)) {
1147
+ const end = source.indexOf("-->", start + 4);
1148
+ return end < 0 ? source.length : end + 3;
1149
+ }
1150
+ for (const [opening, closing] of [["<?", "?>"], ["<![CDATA[", "]]>"]]) {
1151
+ if (!source.startsWith(opening, start))
1152
+ continue;
1153
+ const end = source.indexOf(closing, start + opening.length);
1154
+ return end < 0 ? source.length : end + closing.length;
1155
+ }
1156
+ if (/^<![A-Z]/.test(source.slice(start))) {
1157
+ const end = source.indexOf(">", start + 2);
1158
+ return end < 0 ? source.length : end + 1;
1159
+ }
1160
+ const autolink = source.slice(start).match(/^<(?:[A-Za-z][A-Za-z0-9+.-]{1,31}:[^<>\x00-\x20]*|[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*)>/)?.[0];
1161
+ if (autolink)
1162
+ return start + autolink.length;
1163
+ if (!/^<\/?[A-Za-z][A-Za-z0-9-]*(?=[\s/>])/.test(source.slice(start)))
1164
+ return null;
1165
+ let quote;
1166
+ for (let index = start + 1; index < source.length; index += 1) {
1167
+ const character = source[index];
1168
+ if (quote) {
1169
+ if (character === quote)
1170
+ quote = undefined;
1171
+ }
1172
+ else if (character === "\"" || character === "'")
1173
+ quote = character;
1174
+ else if (character === ">")
1175
+ return index + 1;
1176
+ }
1177
+ return null;
1178
+ }
1179
+ function inlineDestination(source, closeBracket) {
1180
+ let cursor = closeBracket + 2;
1181
+ while (/[ \t\r\n]/.test(source[cursor] ?? ""))
1182
+ cursor += 1;
1183
+ if (source[cursor] === "<") {
1184
+ const start = cursor + 1;
1185
+ cursor = start;
1186
+ while (cursor < source.length && source[cursor] !== ">") {
1187
+ if (source[cursor] === "\n" || source[cursor] === "\r" || source[cursor] === "<")
1188
+ return null;
1189
+ if (source[cursor] === "\\")
1190
+ cursor += 1;
1191
+ cursor += 1;
1192
+ }
1193
+ if (source[cursor] !== ">")
1194
+ return null;
1195
+ const close = inlineLinkClose(source, cursor + 1);
1196
+ return close === null ? null : { start, end: cursor, close };
1197
+ }
1198
+ const start = cursor;
1199
+ let parentheses = 0;
1200
+ while (cursor < source.length) {
1201
+ const character = source[cursor];
1202
+ if (character === "\\") {
1203
+ cursor += 2;
1204
+ continue;
1205
+ }
1206
+ if (character === "(") {
1207
+ parentheses += 1;
1208
+ cursor += 1;
1209
+ continue;
1210
+ }
1211
+ if (character === ")") {
1212
+ if (parentheses === 0)
1213
+ return { start, end: cursor, close: cursor };
1214
+ parentheses -= 1;
1215
+ cursor += 1;
1216
+ continue;
1217
+ }
1218
+ if (/[ \t\r\n]/.test(character ?? "") && parentheses === 0) {
1219
+ const close = inlineLinkClose(source, cursor);
1220
+ return close === null ? null : { start, end: cursor, close };
1221
+ }
1222
+ cursor += 1;
1223
+ }
1224
+ return null;
1225
+ }
1226
+ function inlineLinkClose(source, afterDestination) {
1227
+ let cursor = afterDestination;
1228
+ while (/[ \t\r\n]/.test(source[cursor] ?? ""))
1229
+ cursor += 1;
1230
+ if (source[cursor] === ")")
1231
+ return cursor;
1232
+ if (cursor === afterDestination)
1233
+ return null;
1234
+ const opener = source[cursor];
1235
+ const closer = opener === "(" ? ")" : opener;
1236
+ if (opener !== "\"" && opener !== "'" && opener !== "(")
1237
+ return null;
1238
+ cursor += 1;
1239
+ while (cursor < source.length && source[cursor] !== closer) {
1240
+ if (source[cursor] === "\\")
1241
+ cursor += 1;
1242
+ cursor += 1;
1243
+ }
1244
+ if (source[cursor] !== closer)
1245
+ return null;
1246
+ cursor += 1;
1247
+ while (/[ \t\r\n]/.test(source[cursor] ?? ""))
1248
+ cursor += 1;
1249
+ return source[cursor] === ")" ? cursor : null;
1250
+ }
1251
+ function rootedMarkdownTarget(linkingNotePath, { wanted, suffix }) {
1252
+ const target = wanted.slice(1);
1253
+ const relativeDestination = posix.relative(posix.dirname(linkingNotePath), target) || ".";
1254
+ if (target === "" && suffix !== "") {
1255
+ return { wanted: relativeDestination === "." ? "./" : `${relativeDestination}/`, suffix };
1256
+ }
1257
+ return { wanted: relativeDestination, suffix };
1258
+ }
1259
+ function relativeReferenceDestination(linkingNotePath, target) {
1260
+ const linkingDirectory = posix.dirname(linkingNotePath);
1261
+ const destination = posix.relative(linkingDirectory, target);
1262
+ return destination || `../${posix.basename(target)}`;
1263
+ }
1264
+ function splitReferenceTarget(target) {
1265
+ const suffixStart = [target.indexOf("?"), target.indexOf("#")]
1266
+ .filter((index) => index >= 0)
1267
+ .sort((left, right) => left - right)[0] ?? target.length;
1268
+ return { wanted: target.slice(0, suffixStart), suffix: target.slice(suffixStart) };
1269
+ }
1270
+ function isWikilinkContents(contents) {
1271
+ const delimiter = contents.indexOf("|");
1272
+ return contents.length > 0
1273
+ && !/[\r\n]/.test(contents)
1274
+ && !contents.includes("]]")
1275
+ && (delimiter < 0 || contents.indexOf("|", delimiter + 1) < 0);
1276
+ }
1277
+ function isWikilinkComponent(component) {
1278
+ return !/[\r\n]/.test(component) && !component.includes("]]") && !component.includes("|");
1279
+ }
1280
+ function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
1281
+ function isMissing(error) { return isObject(error) && "code" in error && error.code === "ENOENT"; }
1282
+ function hash(bytes) { return createHash("sha256").update(bytes).digest("base64"); }
1283
+ function compareLinks(left, right) {
1284
+ return left.path.localeCompare(right.path) || (left.field ?? "").localeCompare(right.field ?? "");
1285
+ }
1286
+ function noteApiPath(path) { return path.endsWith(".md") ? path.slice(0, -3) : path; }
1287
+ //# sourceMappingURL=notes.js.map