@bendyline/docblocks 2.6.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/THIRD_PARTY_NOTICES.txt +33 -33
  2. package/dist/capabilities-CdQAc745.d.ts +1233 -0
  3. package/dist/catalog-BQK05tne.d.ts +122 -0
  4. package/dist/chunk-245W57QP.js +31 -0
  5. package/dist/{chunk-2WDJ27EH.js → chunk-46AGYHDY.js} +8 -0
  6. package/dist/{chunk-4437YWLE.js → chunk-6UPO2GE6.js} +4 -2
  7. package/dist/{chunk-UMWCTEXW.js → chunk-7SN7YQL4.js} +211 -40
  8. package/dist/{chunk-OQCPBOQA.js → chunk-DAPFSEKY.js} +35 -0
  9. package/dist/{chunk-M62ZGJ5I.js → chunk-HABG2JM3.js} +0 -17
  10. package/dist/{chunk-YM7O3PWO.js → chunk-HKOOHM67.js} +20 -12
  11. package/dist/chunk-IDYE73TU.js +77 -0
  12. package/dist/{chunk-KLQKU74Q.js → chunk-IGB2VSRE.js} +124 -330
  13. package/dist/{chunk-BVY7GGNV.js → chunk-L6PRMR7T.js} +137 -95
  14. package/dist/chunk-LPIJ6HKL.js +19 -0
  15. package/dist/{chunk-SJBB5OD7.js → chunk-QS5WTLUZ.js} +10 -8
  16. package/dist/chunk-R2JX5N3F.js +382 -0
  17. package/dist/{chunk-ZO7G53QP.js → chunk-UDEWLHQI.js} +4 -2
  18. package/dist/chunk-UK7R3RC7.js +514 -0
  19. package/dist/chunk-XTNWCNSK.js +464 -0
  20. package/dist/chunk-YHSFRPBS.js +42 -0
  21. package/dist/chunk-YNUQG4GF.js +1173 -0
  22. package/dist/document/index.d.ts +31 -1
  23. package/dist/document/index.js +7 -6
  24. package/dist/filesystem/electron.d.ts +8 -34
  25. package/dist/filesystem/electron.js +7 -4
  26. package/dist/filesystem/host.d.ts +28 -0
  27. package/dist/filesystem/host.js +103 -0
  28. package/dist/filesystem/index.d.ts +52 -6
  29. package/dist/filesystem/index.js +36 -15
  30. package/dist/filesystem/indexeddb.d.ts +8 -1
  31. package/dist/filesystem/indexeddb.js +7 -6
  32. package/dist/filesystem/memory.d.ts +2 -1
  33. package/dist/filesystem/memory.js +3 -2
  34. package/dist/filesystem/native.d.ts +2 -1
  35. package/dist/filesystem/native.js +3 -2
  36. package/dist/filesystem-v2-C8AdSXUN.d.ts +74 -0
  37. package/dist/host/index.d.ts +127 -5
  38. package/dist/host/index.js +100 -4
  39. package/dist/host/mobile.d.ts +389 -0
  40. package/dist/host/mobile.js +315 -0
  41. package/dist/host-provider-v2-CyLTRKJF.d.ts +51 -0
  42. package/dist/index.d.ts +8 -5
  43. package/dist/index.js +153 -34
  44. package/dist/mcp/index.d.ts +2 -2
  45. package/dist/mcp/zod.d.ts +23 -1
  46. package/dist/mcp/zod.js +1 -0
  47. package/dist/settings-HrrCikVX.d.ts +170 -0
  48. package/dist/types-BvDnr3YU.d.ts +88 -0
  49. package/dist/{types-Oy9U5FST.d.ts → types-d0yvHUJz.d.ts} +6 -0
  50. package/dist/{types-Bb3yccTH.d.ts → v2-BVnZZyd4.d.ts} +1 -86
  51. package/dist/vscode/index.d.ts +43 -1
  52. package/dist/vscode/index.js +67 -1
  53. package/dist/workspace/index.d.ts +10 -6
  54. package/dist/workspace/index.js +4 -2
  55. package/dist/workspace-index/index.d.ts +240 -0
  56. package/dist/workspace-index/index.js +1287 -0
  57. package/dist/workspace-settings/index.d.ts +301 -0
  58. package/dist/workspace-settings/index.js +820 -0
  59. package/package.json +22 -2
  60. package/dist/chunk-6XHOOBYF.js +0 -36
  61. package/dist/chunk-QI6YKTQV.js +0 -110
  62. package/dist/types-DBYraXxE.d.ts +0 -557
@@ -0,0 +1,122 @@
1
+ import { MarkdownDocument } from '@bendyline/squisq/markdown';
2
+ import { C as CatalogSortOrder } from './settings-HrrCikVX.js';
3
+
4
+ /**
5
+ * Synchronous outside-in path layout.
6
+ *
7
+ * The canonical contract is `@bendyline/squisq-formats/outside-in`, but that
8
+ * entry also loads every format runtime. DocBlocks surfaces that only need
9
+ * the path rules — the shell's navigation, the workspace catalog walker, and
10
+ * the VS Code extension host — share this one copy instead.
11
+ */
12
+ declare const OUTSIDE_IN_FORMAT_IDS: readonly ["html", "docx", "pdf", "pptx", "xlsx", "csv"];
13
+ type OutsideInFormatId = (typeof OUTSIDE_IN_FORMAT_IDS)[number];
14
+ interface OutsideInLayout {
15
+ targetPath: string;
16
+ format: OutsideInFormatId;
17
+ parentDirectory: string;
18
+ stem: string;
19
+ companionName: string;
20
+ companionDirectory: string;
21
+ markdownFilename: string;
22
+ markdownPath: string;
23
+ relativeTargetPath: string;
24
+ backupDirectory: string;
25
+ backupFilename: string;
26
+ backupPath: string;
27
+ }
28
+ declare function resolveOutsideInLayout(path: string): OutsideInLayout | null;
29
+ /** Move the companion root while preserving the authored Markdown slug. */
30
+ declare function relocateOutsideInLayout(layout: OutsideInLayout, targetPath: string): OutsideInLayout;
31
+ declare function chooseOutsideInMarkdownPath(layout: OutsideInLayout, paths: readonly string[]): string | null;
32
+
33
+ /**
34
+ * Workspace catalog model — the data behind the generated catalog JSON and
35
+ * catalog HTML page.
36
+ *
37
+ * Everything here is deterministic: identical inputs produce identical bytes
38
+ * on every surface (shell, VS Code extension host, CLI). There are no
39
+ * timestamps, no filesystem mtimes, and no locale-sensitive ordering, so a
40
+ * catalog committed to git only changes when the documents do.
41
+ */
42
+
43
+ declare const CATALOG_GENERATOR = "docblocks-workspace-catalog";
44
+ declare const CATALOG_FORMAT_VERSION = 1;
45
+ declare const CATALOG_LIMITS: {
46
+ readonly maxOutlineEntries: 100;
47
+ readonly maxOutlineTitleCharacters: 256;
48
+ readonly maxExcerptCharacters: 280;
49
+ readonly maxDescriptionCharacters: 1024;
50
+ readonly maxMetadataCharacters: 256;
51
+ readonly maxTags: 32;
52
+ };
53
+ type CatalogDocumentFormat = 'markdown' | OutsideInFormatId;
54
+ interface CatalogOutlineEntry {
55
+ readonly level: number;
56
+ readonly title: string;
57
+ }
58
+ interface CatalogEntry {
59
+ /** The document people open — the rendered file for outside-in documents. */
60
+ readonly path: string;
61
+ /** The Markdown source when it differs from `path` (outside-in companion). */
62
+ readonly source?: string;
63
+ readonly format: CatalogDocumentFormat;
64
+ readonly title: string;
65
+ readonly description?: string;
66
+ readonly author?: string;
67
+ readonly date?: string;
68
+ readonly tags?: readonly string[];
69
+ readonly theme?: string;
70
+ readonly outline: readonly CatalogOutlineEntry[];
71
+ readonly wordCount: number;
72
+ readonly excerpt?: string;
73
+ /** Present only when the catalog is configured with `content: "markdown"`. */
74
+ readonly markdown?: string;
75
+ }
76
+ interface WorkspaceCatalog {
77
+ readonly generator: typeof CATALOG_GENERATOR;
78
+ readonly version: typeof CATALOG_FORMAT_VERSION;
79
+ readonly title: string;
80
+ readonly documents: readonly CatalogEntry[];
81
+ }
82
+ interface CatalogSource {
83
+ readonly path: string;
84
+ readonly source?: string;
85
+ readonly format: CatalogDocumentFormat;
86
+ }
87
+ interface CatalogEntryHints {
88
+ /** Title used when the document has neither a frontmatter title nor a heading. */
89
+ readonly fallbackTitle?: string;
90
+ readonly description?: string;
91
+ }
92
+ /** Build a catalog entry from an already-parsed Markdown document. */
93
+ declare function catalogEntryFromDocument(source: CatalogSource, document: MarkdownDocument, hints?: CatalogEntryHints): Promise<CatalogEntry>;
94
+ /** Parse Markdown source and build its catalog entry. */
95
+ declare function catalogEntryFromMarkdown(source: CatalogSource, markdown: string, options?: {
96
+ readonly includeMarkdown?: boolean;
97
+ }): Promise<CatalogEntry>;
98
+ /** A document whose content cannot be read (e.g. an un-imported DOCX): listed by name only. */
99
+ declare function catalogEntryForUnreadable(source: CatalogSource): CatalogEntry;
100
+ declare function sortCatalogEntries(entries: readonly CatalogEntry[], order: CatalogSortOrder): CatalogEntry[];
101
+ declare function createWorkspaceCatalog(title: string, entries: readonly CatalogEntry[], order: CatalogSortOrder): WorkspaceCatalog;
102
+ /**
103
+ * Deterministic catalog JSON. The fixed leading `"generator"` key doubles as
104
+ * the marker that lets DocBlocks recognize — and only ever replace — a file
105
+ * it generated.
106
+ */
107
+ declare function serializeCatalogJson(catalog: WorkspaceCatalog): string;
108
+ declare function isGeneratedCatalogJson(text: string): boolean;
109
+ /** Insert the generator marker directly after the doctype so it cannot affect quirks mode. */
110
+ declare function stampCatalogHtml(html: string, inputsDigest: string): string;
111
+ declare function readCatalogHtmlDigest(text: string): string | null;
112
+ declare function isGeneratedCatalogHtml(text: string): boolean;
113
+ /** Relative, per-segment-encoded URL from the catalog's directory to a document. */
114
+ declare function relativeCatalogLink(fromDirectory: string, toPath: string): string;
115
+ /**
116
+ * Build the catalog page as a Markdown AST rather than Markdown text, so a
117
+ * document title or description can never inject Markdown or HTML: every
118
+ * value reaches the renderer as an escaped text node.
119
+ */
120
+ declare function buildCatalogMarkdownDocument(catalog: WorkspaceCatalog, outputPath: string): MarkdownDocument;
121
+
122
+ export { type CatalogEntry as C, OUTSIDE_IN_FORMAT_IDS as O, type WorkspaceCatalog as W, CATALOG_FORMAT_VERSION as a, CATALOG_GENERATOR as b, CATALOG_LIMITS as c, type CatalogDocumentFormat as d, type CatalogEntryHints as e, type CatalogOutlineEntry as f, type CatalogSource as g, type OutsideInFormatId as h, type OutsideInLayout as i, buildCatalogMarkdownDocument as j, catalogEntryForUnreadable as k, catalogEntryFromDocument as l, catalogEntryFromMarkdown as m, chooseOutsideInMarkdownPath as n, createWorkspaceCatalog as o, isGeneratedCatalogHtml as p, isGeneratedCatalogJson as q, readCatalogHtmlDigest as r, relativeCatalogLink as s, relocateOutsideInLayout as t, resolveOutsideInLayout as u, serializeCatalogJson as v, sortCatalogEntries as w, stampCatalogHtml as x };
@@ -0,0 +1,31 @@
1
+ // src/filesystem/capabilities.ts
2
+ function parseFileSystemCapabilities(value) {
3
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4
+ throw new TypeError("Invalid filesystem capabilities.");
5
+ }
6
+ const record = value;
7
+ const choices = {
8
+ writeAtomicity: ["none", "process", "cross-context"],
9
+ moveAtomicity: ["none", "process", "cross-context"],
10
+ snapshotAtomicity: ["none", "process", "cross-context"],
11
+ conditionalWrite: ["none", "process", "cross-context", "storage-atomic"],
12
+ recursiveRemove: [true, false],
13
+ watch: [true, false],
14
+ caseSensitivity: ["sensitive", "insensitive", "platform"],
15
+ symlinkPolicy: ["unsupported", "reject", "follow-contained", "preserve"],
16
+ durability: ["volatile", "best-effort", "durable"]
17
+ };
18
+ if (Object.keys(record).length !== Object.keys(choices).length) {
19
+ throw new TypeError("Unexpected filesystem capability fields.");
20
+ }
21
+ for (const [key, values] of Object.entries(choices)) {
22
+ if (!values.includes(record[key])) {
23
+ throw new TypeError(`Invalid filesystem capability: ${key}.`);
24
+ }
25
+ }
26
+ return Object.freeze({ ...record });
27
+ }
28
+
29
+ export {
30
+ parseFileSystemCapabilities
31
+ };
@@ -11,6 +11,13 @@ var HOST_WIRE_LIMITS = Object.freeze({
11
11
  base64Characters: 140 * 1024 * 1024,
12
12
  arrayEntries: 1e4
13
13
  });
14
+ var EXPORT_TRANSFER_LIMITS = Object.freeze({
15
+ fileBytes: 16 * 1024 * 1024 * 1024,
16
+ chunkBytes: 4 * 1024 * 1024,
17
+ transfers: 4,
18
+ idleMs: 12e4,
19
+ lifetimeMs: 60 * 6e4
20
+ });
14
21
  function hasExactKeys(value, keys) {
15
22
  const actual = Object.keys(value);
16
23
  return actual.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
@@ -91,6 +98,7 @@ function isTrustedRendererUrl(value, developmentOrigin) {
91
98
  export {
92
99
  MAX_HOST_PINNED_DOCUMENTS,
93
100
  HOST_WIRE_LIMITS,
101
+ EXPORT_TRANSFER_LIMITS,
94
102
  isBoundedString,
95
103
  isBoundedBytePayload,
96
104
  parseOpenRequest,
@@ -8,9 +8,11 @@ import {
8
8
  bytesEqual,
9
9
  compareSnapshotsByName,
10
10
  compareText,
11
- copyBytes,
11
+ copyBytes
12
+ } from "./chunk-HABG2JM3.js";
13
+ import {
12
14
  parseFileSystemVersion
13
- } from "./chunk-M62ZGJ5I.js";
15
+ } from "./chunk-LPIJ6HKL.js";
14
16
  import {
15
17
  decodeUtf8Text
16
18
  } from "./chunk-M5ZNQGR5.js";
@@ -1,19 +1,26 @@
1
1
  import {
2
2
  guessMimeType
3
- } from "./chunk-YM7O3PWO.js";
3
+ } from "./chunk-HKOOHM67.js";
4
4
  import {
5
5
  bytesEqual,
6
6
  compareSnapshotsByName,
7
7
  compareText,
8
- copyBytes,
8
+ copyBytes
9
+ } from "./chunk-HABG2JM3.js";
10
+ import {
9
11
  parseFileSystemVersion
10
- } from "./chunk-M62ZGJ5I.js";
12
+ } from "./chunk-LPIJ6HKL.js";
11
13
  import {
14
+ DocumentSession,
12
15
  FileSystemMoveRecoveryError,
13
16
  FileSystemPartialMoveError,
17
+ createFileSystemDocumentTarget,
14
18
  describeEntryMove,
15
19
  describePartialMove
16
- } from "./chunk-QI6YKTQV.js";
20
+ } from "./chunk-IGB2VSRE.js";
21
+ import {
22
+ getFileSystemProviderV2
23
+ } from "./chunk-XAMILHQP.js";
17
24
  import {
18
25
  FsError,
19
26
  WORKSPACE_ROOT,
@@ -25,9 +32,6 @@ import {
25
32
  workspacePathJoin,
26
33
  workspacePathToLegacy
27
34
  } from "./chunk-AYWKIZLD.js";
28
- import {
29
- getFileSystemProviderV2
30
- } from "./chunk-XAMILHQP.js";
31
35
 
32
36
  // src/filesystem/legacy-v2-adapter.ts
33
37
  var EPOCH = (/* @__PURE__ */ new Date(0)).toISOString();
@@ -578,6 +582,93 @@ function newestLastModified(children) {
578
582
  return newest ? new Date(newest).toISOString() : EPOCH;
579
583
  }
580
584
 
585
+ // src/filesystem/companion-references.ts
586
+ async function rewriteCompanionReferences(content, oldPath, newPath) {
587
+ const companionName = (path) => path.split("/").pop().replace(/\.[^.]+$/, "") + "_files";
588
+ const oldName = companionName(oldPath);
589
+ const newName = companionName(newPath);
590
+ if (oldName === newName || !content.includes(oldName) && !content.includes(encodeURI(oldName)))
591
+ return content;
592
+ const { parseMarkdown, stringifyMarkdown } = await import("@bendyline/squisq/markdown");
593
+ const document = parseMarkdown(content);
594
+ let changed = false;
595
+ const rewrite = (value) => {
596
+ const relative = value.startsWith("./") ? "./" : "";
597
+ const path = value.slice(relative.length);
598
+ for (const [before, after] of [
599
+ [oldName, newName],
600
+ [encodeURI(oldName), encodeURI(newName)]
601
+ ]) {
602
+ if (path.startsWith(`${before}/`)) {
603
+ changed = true;
604
+ return `${relative}${after}/${path.slice(before.length + 1)}`;
605
+ }
606
+ }
607
+ return value;
608
+ };
609
+ const mediaFields = /* @__PURE__ */ new Set([
610
+ "src",
611
+ "href",
612
+ "poster",
613
+ "audio",
614
+ "video",
615
+ "image",
616
+ "backgroundImage"
617
+ ]);
618
+ const visit = (value) => {
619
+ if (!value || typeof value !== "object") return;
620
+ if (Array.isArray(value)) {
621
+ value.forEach(visit);
622
+ return;
623
+ }
624
+ const node = value;
625
+ if (["image", "link", "definition"].includes(String(node.type)) && typeof node.url === "string") {
626
+ node.url = rewrite(node.url);
627
+ }
628
+ if ((node.type === "htmlBlock" || node.type === "htmlInline") && typeof node.rawHtml === "string") {
629
+ node.rawHtml = node.rawHtml.replace(
630
+ /<(?:img|audio|video|source|a)\b[^>]*>/gi,
631
+ (tag) => tag.replace(
632
+ /(\s)(src|href|poster)(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi,
633
+ (_match, space, name, equals, doubleQuoted, singleQuoted, unquoted) => {
634
+ const quote = doubleQuoted !== void 0 ? '"' : singleQuoted !== void 0 ? "'" : "";
635
+ return `${space}${name}${equals}${quote}${rewrite(doubleQuoted ?? singleQuoted ?? unquoted ?? "")}${quote}`;
636
+ }
637
+ )
638
+ );
639
+ }
640
+ for (const field of ["attributes", "params"]) {
641
+ const attributes = node[field];
642
+ if (attributes && typeof attributes === "object" && !Array.isArray(attributes)) {
643
+ const values = attributes;
644
+ for (const key of mediaFields)
645
+ if (typeof values[key] === "string") values[key] = rewrite(values[key]);
646
+ }
647
+ }
648
+ for (const [key, child] of Object.entries(node)) {
649
+ if (key !== "frontmatter" && key !== "position" && key !== "htmlChildren") visit(child);
650
+ }
651
+ };
652
+ const replacements = [];
653
+ for (const block of document.children) {
654
+ changed = false;
655
+ visit(block);
656
+ if (!changed) continue;
657
+ const start = block.position?.start.offset;
658
+ const end = block.position?.end.offset;
659
+ if (start === void 0 || end === void 0) {
660
+ throw new Error("Cannot locate a media reference in the Markdown source.");
661
+ }
662
+ let text = stringifyMarkdown({ type: "document", children: [block] }).replace(/\n+$/, "");
663
+ if (content.includes("\r\n")) text = text.replace(/\n/g, "\r\n");
664
+ replacements.push({ start, end, text });
665
+ }
666
+ for (const replacement of replacements.reverse()) {
667
+ content = content.slice(0, replacement.start) + replacement.text + content.slice(replacement.end);
668
+ }
669
+ return content;
670
+ }
671
+
581
672
  // src/filesystem/move-entry.ts
582
673
  function normalisePath(path) {
583
674
  return parseWorkspacePath(path);
@@ -600,9 +691,9 @@ function documentCompanionPath(documentPath) {
600
691
  return rooted ? `/${name}_files` : `${name}_files`;
601
692
  }
602
693
  function hasDocumentCompanion(path) {
603
- return /\.(?:md|html?|docx|pdf|pptx|xlsx)$/i.test(basename(path));
694
+ return /\.(?:md|html?|docx|pdf|pptx|xlsx|csv)$/i.test(basename(path));
604
695
  }
605
- async function moveFileSystemEntry(provider, oldPath, newPath, kind) {
696
+ async function moveFileSystemEntry(provider, oldPath, newPath, kind, options = {}) {
606
697
  const providerV2 = getFileSystemProviderV2(provider);
607
698
  const exists = (candidate) => providerV2 ? providerV2.stat(parseWorkspacePath(candidate)).then((entry) => entry !== null) : provider.exists(candidate);
608
699
  const move = (source, destination) => providerV2 ? providerV2.move(parseWorkspacePath(source), parseWorkspacePath(destination), {
@@ -629,6 +720,16 @@ async function moveFileSystemEntry(provider, oldPath, newPath, kind) {
629
720
  }
630
721
  }
631
722
  }
723
+ let markdown;
724
+ if (oldCompanion && options.rewriteMarkdown !== false && /\.md$/i.test(oldPath)) {
725
+ const source = providerV2 ? await providerV2.readFile(parseWorkspacePath(oldPath)).then(
726
+ (read) => read ? new TextDecoder("utf-8", { fatal: true }).decode(read.data) : null
727
+ ) : await provider.readFile(oldPath);
728
+ if (source !== null) {
729
+ const next = await rewriteCompanionReferences(source, oldPath, newPath);
730
+ if (source !== next) markdown = { previous: source, next };
731
+ }
732
+ }
632
733
  try {
633
734
  await move(oldPath, newPath);
634
735
  } catch (error) {
@@ -675,6 +776,26 @@ async function moveFileSystemEntry(provider, oldPath, newPath, kind) {
675
776
  }
676
777
  throw error;
677
778
  }
779
+ if (markdown) {
780
+ const session = new DocumentSession({ autoSaveEnabled: false });
781
+ try {
782
+ const state = await session.transitionTo(
783
+ createFileSystemDocumentTarget(provider, newPath),
784
+ markdown.previous
785
+ );
786
+ session.edit(markdown.next, { targetKey: state.targetKey, generation: state.generation });
787
+ await session.flush("transition");
788
+ } catch (error) {
789
+ throw new FileSystemMoveRecoveryError(
790
+ oldPath,
791
+ newPath,
792
+ await describeEntryMove(provider, oldPath, newPath),
793
+ error
794
+ );
795
+ } finally {
796
+ await session.cancel();
797
+ }
798
+ }
678
799
  }
679
800
 
680
801
  // src/filesystem/dbk-workspace.ts
@@ -734,6 +855,7 @@ async function createDbkWorkspaceSnapshot(source, options) {
734
855
  throw new Error(`The external DBK contains duplicate paths: ${sourcePath}`);
735
856
  }
736
857
  seenSourcePaths.add(sourcePath);
858
+ if (sourcePath.startsWith(".docblocks/")) continue;
737
859
  if (!/\.md$/iu.test(sourcePath)) {
738
860
  throw new Error(
739
861
  `The external DBK is corrupt: only Markdown (.md) files are allowed (${sourcePath}).`
@@ -779,13 +901,33 @@ import { findDocumentPath } from "@bendyline/squisq/storage";
779
901
  function joinPrefix(prefix, p) {
780
902
  return workspacePathJoin(parseWorkspacePath(prefix), p);
781
903
  }
904
+ function canonicalPrefixFolder(prefix) {
905
+ const folder = prefix?.includes("/") ? prefix.slice(0, prefix.lastIndexOf("/")) : "";
906
+ try {
907
+ return folder && parseWorkspacePath(folder) === folder ? folder : "";
908
+ } catch {
909
+ return "";
910
+ }
911
+ }
782
912
  var FileSystemContentContainer = class {
783
- constructor(provider, prefix = ".docblocks/media") {
913
+ constructor(provider, prefix = ".docblocks/media", options = {}) {
784
914
  this.provider = provider;
785
915
  this.prefix = parseWorkspacePath(prefix);
916
+ this.documentMediaPrefix = parseWorkspacePath(options.documentMediaPrefix ?? "");
917
+ }
918
+ /** A document-scoped container also accepts the media references written
919
+ * into its Markdown, e.g. notes_files/audio/take.webm. Version paths and
920
+ * container-relative discovery still live directly under the scoped root. */
921
+ resolve(path) {
922
+ const canonical = parseWorkspacePath(path);
923
+ const alias = this.documentMediaPrefix;
924
+ return joinPrefix(
925
+ this.prefix,
926
+ alias && canonical.startsWith(`${alias}/`) ? canonical.slice(alias.length + 1) : canonical
927
+ );
786
928
  }
787
929
  async readFile(path) {
788
- const full = joinPrefix(this.prefix, path);
930
+ const full = this.resolve(path);
789
931
  const providerV2 = getFileSystemProviderV2(this.provider);
790
932
  if (providerV2) return (await providerV2.readFile(parseWorkspacePath(full)))?.data ?? null;
791
933
  const binary = await this.provider.readBinary(full);
@@ -795,7 +937,7 @@ var FileSystemContentContainer = class {
795
937
  return new TextEncoder().encode(text).buffer;
796
938
  }
797
939
  async writeFile(path, data, _mimeType) {
798
- const full = joinPrefix(this.prefix, path);
940
+ const full = this.resolve(path);
799
941
  const providerV2 = getFileSystemProviderV2(this.provider);
800
942
  if (providerV2) {
801
943
  await providerV2.writeFile(parseWorkspacePath(full), data, {
@@ -807,7 +949,7 @@ var FileSystemContentContainer = class {
807
949
  await this.provider.writeBinary(full, data);
808
950
  }
809
951
  async removeFile(path) {
810
- const full = joinPrefix(this.prefix, path);
952
+ const full = this.resolve(path);
811
953
  const providerV2 = getFileSystemProviderV2(this.provider);
812
954
  if (providerV2) {
813
955
  await providerV2.remove(parseWorkspacePath(full), { missing: "ignore" });
@@ -819,22 +961,26 @@ var FileSystemContentContainer = class {
819
961
  const entries = [];
820
962
  const providerV2 = getFileSystemProviderV2(this.provider);
821
963
  const root = parseWorkspacePath(this.prefix);
964
+ const relativePath = (childPath) => root ? childPath === root ? "" : childPath.slice(root.length + 1) : childPath;
965
+ const prefixFolder = canonicalPrefixFolder(prefix);
966
+ const start = prefixFolder ? workspacePathJoin(root, prefixFolder) : root;
822
967
  const walk = async (dir) => {
823
968
  let children;
824
969
  try {
825
970
  children = providerV2 ? await providerV2.readDirectory(parseWorkspacePath(dir)) : await this.provider.readDirectory(dir);
826
971
  } catch (error) {
827
- if (dir === root && error instanceof FsError && error.code === "not-found") return;
972
+ const absent = error instanceof FsError && (error.code === "not-found" || dir !== root && error.code === "type-mismatch");
973
+ if (dir === start && absent) return;
828
974
  throw error;
829
975
  }
830
976
  for (const child of children) {
977
+ const childPath = parseWorkspacePath(child.path);
831
978
  if (child.kind === "directory") {
979
+ if (prefix && !`${relativePath(childPath)}/`.startsWith(prefix)) continue;
832
980
  await walk(child.path);
833
981
  } else {
834
- const childPath = parseWorkspacePath(child.path);
835
- const prefixPath = parseWorkspacePath(this.prefix);
836
- if (!workspacePathContains(prefixPath, childPath)) continue;
837
- const rel = prefixPath ? childPath === prefixPath ? "" : childPath.slice(prefixPath.length + 1) : childPath;
982
+ if (!workspacePathContains(root, childPath)) continue;
983
+ const rel = relativePath(childPath);
838
984
  if (prefix && !rel.startsWith(prefix)) continue;
839
985
  const size = child.size ?? (await this.provider.stat(child.path))?.size ?? 0;
840
986
  entries.push({
@@ -845,11 +991,11 @@ var FileSystemContentContainer = class {
845
991
  }
846
992
  }
847
993
  };
848
- await walk(root);
994
+ await walk(start);
849
995
  return entries;
850
996
  }
851
997
  async exists(path) {
852
- const full = joinPrefix(this.prefix, path);
998
+ const full = this.resolve(path);
853
999
  const providerV2 = getFileSystemProviderV2(this.provider);
854
1000
  return providerV2 ? await providerV2.stat(parseWorkspacePath(full)) !== null : this.provider.exists(full);
855
1001
  }
@@ -878,24 +1024,46 @@ function createFileMediaProvider(container, markdownBasename) {
878
1024
  const folder = stripExt(markdownBasename) + "_files";
879
1025
  const prefix = folder + "/";
880
1026
  const blobUrlCache = /* @__PURE__ */ new Map();
1027
+ const pending = /* @__PURE__ */ new Map();
1028
+ const generations = /* @__PURE__ */ new Map();
1029
+ let disposed = false;
881
1030
  function toKey(ref) {
882
1031
  const clean = ref.replace(/^\/+/, "");
883
1032
  return clean.startsWith(prefix) ? clean : prefix + clean;
884
1033
  }
885
- return {
886
- async resolveUrl(ref) {
887
- const key = toKey(ref);
888
- const cached = blobUrlCache.get(key);
889
- if (cached) return cached;
1034
+ function invalidate(key) {
1035
+ generations.set(key, (generations.get(key) ?? 0) + 1);
1036
+ pending.delete(key);
1037
+ const cached = blobUrlCache.get(key);
1038
+ if (cached) URL.revokeObjectURL(cached);
1039
+ blobUrlCache.delete(key);
1040
+ }
1041
+ function resolveUrl(ref) {
1042
+ if (disposed) return Promise.resolve(ref);
1043
+ const key = toKey(ref);
1044
+ const cached = blobUrlCache.get(key);
1045
+ if (cached) return Promise.resolve(cached);
1046
+ const existing = pending.get(key);
1047
+ if (existing) {
1048
+ return existing.promise.then((url) => url === existing.ref ? ref : url);
1049
+ }
1050
+ const generation = generations.get(key) ?? 0;
1051
+ const work = (async () => {
890
1052
  const data = await container.readFile(key);
1053
+ if (disposed) return ref;
1054
+ if ((generations.get(key) ?? 0) !== generation) return resolveUrl(ref);
891
1055
  if (!data) return ref;
892
- const entries = await container.listFiles();
893
- const entry = entries.find((e) => e.path === key);
894
- const mimeType = entry?.mimeType ?? "application/octet-stream";
895
- const url = URL.createObjectURL(new Blob([data], { type: mimeType }));
1056
+ const url = URL.createObjectURL(new Blob([data], { type: guessMimeType(key) }));
896
1057
  blobUrlCache.set(key, url);
897
1058
  return url;
898
- },
1059
+ })().finally(() => {
1060
+ if (pending.get(key)?.promise === work) pending.delete(key);
1061
+ });
1062
+ pending.set(key, { ref, promise: work });
1063
+ return work;
1064
+ }
1065
+ return {
1066
+ resolveUrl,
899
1067
  async listMedia() {
900
1068
  const entries = await container.listFiles(prefix);
901
1069
  return entries.filter((e) => !e.path.toLowerCase().endsWith(".md")).map((e) => ({
@@ -906,25 +1074,27 @@ function createFileMediaProvider(container, markdownBasename) {
906
1074
  },
907
1075
  async addMedia(name, data, mimeType) {
908
1076
  const key = toKey(name);
909
- const cached = blobUrlCache.get(key);
910
- if (cached) {
911
- URL.revokeObjectURL(cached);
912
- blobUrlCache.delete(key);
913
- }
1077
+ invalidate(key);
1078
+ const generation = generations.get(key);
914
1079
  const buffer = data instanceof Blob ? new Uint8Array(await data.arrayBuffer()) : data;
915
1080
  await container.writeFile(key, buffer, mimeType);
1081
+ const canCacheUpload = generations.get(key) === generation;
1082
+ invalidate(key);
1083
+ if (!disposed && canCacheUpload) {
1084
+ const bytes = ArrayBuffer.isView(buffer) ? new Uint8Array(buffer).slice().buffer : buffer;
1085
+ blobUrlCache.set(key, URL.createObjectURL(new Blob([bytes], { type: mimeType })));
1086
+ }
916
1087
  return key;
917
1088
  },
918
1089
  async removeMedia(ref) {
919
1090
  const key = toKey(ref);
920
- const cached = blobUrlCache.get(key);
921
- if (cached) {
922
- URL.revokeObjectURL(cached);
923
- blobUrlCache.delete(key);
924
- }
1091
+ invalidate(key);
925
1092
  await container.removeFile(key);
1093
+ invalidate(key);
926
1094
  },
927
1095
  dispose() {
1096
+ disposed = true;
1097
+ pending.clear();
928
1098
  for (const url of blobUrlCache.values()) {
929
1099
  URL.revokeObjectURL(url);
930
1100
  }
@@ -935,6 +1105,7 @@ function createFileMediaProvider(container, markdownBasename) {
935
1105
 
936
1106
  export {
937
1107
  LegacyFileSystemProviderV2Adapter,
1108
+ rewriteCompanionReferences,
938
1109
  documentCompanionPath,
939
1110
  moveFileSystemEntry,
940
1111
  createDbkWorkspaceSnapshot,
@@ -106,7 +106,42 @@ var RawIndexedDBFileSystemTransaction = class {
106
106
  const cursor = await requestResult(this.store.openKeyCursor(IDBKeyRange.lowerBound(prefix)));
107
107
  return cursor !== null && typeof cursor.key === "string" && cursor.key.startsWith(prefix);
108
108
  }
109
+ childKeys(prefix) {
110
+ return new Promise((resolve, reject) => {
111
+ const keys = [];
112
+ const request = this.store.openKeyCursor(IDBKeyRange.lowerBound(prefix));
113
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed."));
114
+ request.onsuccess = () => {
115
+ const cursor = request.result;
116
+ if (!cursor) {
117
+ resolve(keys);
118
+ return;
119
+ }
120
+ const key = cursor.key;
121
+ if (typeof key !== "string") {
122
+ reject(new Error("The DocBlocks filesystem store contains a non-string key."));
123
+ return;
124
+ }
125
+ const step = childKeyStep(prefix, key);
126
+ if (step.kind === "done") {
127
+ resolve(keys);
128
+ return;
129
+ }
130
+ if (step.kind === "child") keys.push(key);
131
+ if (step.kind === "skip") cursor.continue(step.seek);
132
+ else cursor.continue();
133
+ };
134
+ });
135
+ }
109
136
  };
137
+ function childKeyStep(prefix, key) {
138
+ if (!key.startsWith(prefix)) return { kind: "done" };
139
+ const rest = key.slice(prefix.length);
140
+ if (!rest) return { kind: "self" };
141
+ const slash = rest.indexOf("/");
142
+ if (slash === -1) return { kind: "child" };
143
+ return { kind: "skip", seek: `${prefix}${rest.slice(0, slash)}0` };
144
+ }
110
145
  async function openDatabaseWithStore(databaseName, storeName) {
111
146
  const database = await openDatabase(databaseName, void 0, (upgradeDatabase) => {
112
147
  if (!upgradeDatabase.objectStoreNames.contains(storeName)) {
@@ -1,18 +1,3 @@
1
- // src/filesystem/v2.ts
2
- function parseFileSystemVersion(value) {
3
- if (typeof value !== "string" || !value || value.length > 1024 || value.includes("\0")) {
4
- throw new TypeError("Filesystem version tokens must be non-empty bounded strings.");
5
- }
6
- return value;
7
- }
8
- function tryParseFileSystemVersion(value) {
9
- try {
10
- return parseFileSystemVersion(value);
11
- } catch {
12
- return null;
13
- }
14
- }
15
-
16
1
  // src/filesystem/internal/bytes.ts
17
2
  function copyBytes(data) {
18
3
  const source = ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
@@ -41,8 +26,6 @@ function compareSnapshotsByPath(left, right) {
41
26
  }
42
27
 
43
28
  export {
44
- parseFileSystemVersion,
45
- tryParseFileSystemVersion,
46
29
  copyBytes,
47
30
  bytesEqual,
48
31
  compareText,