@bendyline/docblocks 2.7.8 → 2.7.10

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.
@@ -14,8 +14,8 @@ This notice is included in the published npm tarball. Transitive packages instal
14
14
 
15
15
  | Package | Version | License | Source |
16
16
  | --- | --- | --- | --- |
17
- | @bendyline/squisq | 2.13.6 | MIT | https://github.com/bendyline/squisq |
18
- | @bendyline/squisq-graph | 2.13.0 | MIT | https://github.com/bendyline/squisq |
17
+ | @bendyline/squisq | 2.13.9 | MIT | https://github.com/bendyline/squisq |
18
+ | @bendyline/squisq-graph | 2.13.1 | MIT | https://github.com/bendyline/squisq |
19
19
  | @types/debug | 4.1.12 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped |
20
20
  | @types/hast | 3.0.4 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped |
21
21
  | @types/katex | 0.16.8 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped |
@@ -133,7 +133,7 @@ This notice is included in the published npm tarball. Transitive packages instal
133
133
  Package-local license, copying, and notice files are reproduced below. Where a published package omits its repository license, the reviewed aggregate or README identified in `Source files` supplies the retained material.
134
134
 
135
135
  ==============================================================================
136
- Components: @bendyline/squisq-graph@2.13.0, @bendyline/squisq@2.13.6
136
+ Components: @bendyline/squisq-graph@2.13.1, @bendyline/squisq@2.13.9
137
137
  Source files: node_modules/@bendyline/squisq-graph/LICENSE, node_modules/@bendyline/squisq/LICENSE
138
138
  ==============================================================================
139
139
  MIT License
@@ -159,7 +159,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
159
159
  SOFTWARE.
160
160
 
161
161
  ==============================================================================
162
- Components: @bendyline/squisq-graph@2.13.0
162
+ Components: @bendyline/squisq-graph@2.13.1
163
163
  Source files: node_modules/@bendyline/squisq-graph/NOTICE.md
164
164
  ==============================================================================
165
165
  # Third-Party Notices for @bendyline/squisq-graph
@@ -177,7 +177,7 @@ This package has no third-party runtime dependencies. The layout engine and its
177
177
  worker entry are Squisq-authored (MIT).
178
178
 
179
179
  ==============================================================================
180
- Components: @bendyline/squisq-graph@2.13.0
180
+ Components: @bendyline/squisq-graph@2.13.1
181
181
  Source files: node_modules/@bendyline/squisq-graph/THIRD_PARTY_LICENSES.txt
182
182
  ==============================================================================
183
183
  THIRD-PARTY LICENSES FOR @bendyline/squisq-graph
@@ -189,7 +189,7 @@ repository license, the pinned upstream copy is vendored and identified below.
189
189
  COMPONENTS
190
190
 
191
191
  ==============================================================================
192
- Components: @bendyline/squisq@2.13.6
192
+ Components: @bendyline/squisq@2.13.9
193
193
  Source files: node_modules/@bendyline/squisq/NOTICE.md
194
194
  ==============================================================================
195
195
  # Third-Party Notices for @bendyline/squisq
@@ -220,7 +220,7 @@ third-party license texts for bundled code and data are shipped in
220
220
  THIRD_PARTY_LICENSES.txt.
221
221
 
222
222
  ==============================================================================
223
- Components: @bendyline/squisq@2.13.6
223
+ Components: @bendyline/squisq@2.13.9
224
224
  Source files: node_modules/@bendyline/squisq/THIRD_PARTY_LICENSES.txt
225
225
  ==============================================================================
226
226
  THIRD-PARTY LICENSES FOR @bendyline/squisq
@@ -1,5 +1,5 @@
1
1
  import { MarkdownDocument } from '@bendyline/squisq/markdown';
2
- import { C as CatalogSortOrder } from './settings-HrrCikVX.js';
2
+ import { C as CatalogSortOrder } from './settings-D_CL7AJA.js';
3
3
 
4
4
  /**
5
5
  * Synchronous outside-in path layout.
@@ -36,8 +36,11 @@ declare function chooseOutsideInMarkdownPath(layout: OutsideInLayout, paths: rea
36
36
  *
37
37
  * Everything here is deterministic: identical inputs produce identical bytes
38
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.
39
+ * filesystem mtimes and no locale-sensitive ordering. The only clock reading
40
+ * is a document's first-published and last-modified dates, carried forward from
41
+ * the previous catalog JSON and stamped only when a document first appears or
42
+ * its content changes — so a catalog committed to git still only changes when
43
+ * the documents do.
41
44
  */
42
45
 
43
46
  declare const CATALOG_GENERATOR = "docblocks-workspace-catalog";
@@ -65,11 +68,17 @@ interface CatalogEntry {
65
68
  readonly description?: string;
66
69
  readonly author?: string;
67
70
  readonly date?: string;
71
+ /** When the document first appeared in the catalog JSON (ISO 8601, UTC). */
72
+ readonly firstPublishedDate?: string;
73
+ /** When the catalog JSON last picked up a change to the document's content. */
74
+ readonly lastModifiedDate?: string;
68
75
  readonly tags?: readonly string[];
69
76
  readonly theme?: string;
70
77
  readonly outline: readonly CatalogOutlineEntry[];
71
78
  readonly wordCount: number;
72
79
  readonly excerpt?: string;
80
+ /** SHA-256 hex of the source text with normalized line endings; absent when unread. */
81
+ readonly contentHash?: string;
73
82
  /** Present only when the catalog is configured with `content: "markdown"`. */
74
83
  readonly markdown?: string;
75
84
  }
@@ -106,6 +115,23 @@ declare function createWorkspaceCatalog(title: string, entries: readonly Catalog
106
115
  */
107
116
  declare function serializeCatalogJson(catalog: WorkspaceCatalog): string;
108
117
  declare function isGeneratedCatalogJson(text: string): boolean;
118
+ /** The dates a previously generated catalog recorded for one document. */
119
+ interface CatalogDateRecord {
120
+ readonly firstPublishedDate: string;
121
+ readonly lastModifiedDate: string;
122
+ readonly contentHash?: string;
123
+ }
124
+ /**
125
+ * Per-path date history from a previously generated catalog JSON. Anything
126
+ * malformed is skipped, so the worst case is a document dated as new.
127
+ */
128
+ declare function readCatalogDateHistory(text: string): Map<string, CatalogDateRecord>;
129
+ /**
130
+ * Date each entry. A document keeps the dates `history` holds for its path;
131
+ * it is stamped `now` when it first appears, and its `lastModifiedDate`
132
+ * moves to `now` when its content hash changes.
133
+ */
134
+ declare function stampCatalogDates(catalog: WorkspaceCatalog, history: ReadonlyMap<string, CatalogDateRecord>, now: Date): WorkspaceCatalog;
109
135
  /** Insert the generator marker directly after the doctype so it cannot affect quirks mode. */
110
136
  declare function stampCatalogHtml(html: string, inputsDigest: string): string;
111
137
  declare function readCatalogHtmlDigest(text: string): string | null;
@@ -119,4 +145,4 @@ declare function relativeCatalogLink(fromDirectory: string, toPath: string): str
119
145
  */
120
146
  declare function buildCatalogMarkdownDocument(catalog: WorkspaceCatalog, outputPath: string): MarkdownDocument;
121
147
 
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 };
148
+ export { stampCatalogHtml as A, 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 CatalogDateRecord as d, type CatalogDocumentFormat as e, type CatalogEntryHints as f, type CatalogOutlineEntry as g, type CatalogSource as h, type OutsideInFormatId as i, type OutsideInLayout as j, buildCatalogMarkdownDocument as k, catalogEntryForUnreadable as l, catalogEntryFromDocument as m, catalogEntryFromMarkdown as n, chooseOutsideInMarkdownPath as o, createWorkspaceCatalog as p, isGeneratedCatalogHtml as q, isGeneratedCatalogJson as r, readCatalogDateHistory as s, readCatalogHtmlDigest as t, relativeCatalogLink as u, relocateOutsideInLayout as v, resolveOutsideInLayout as w, serializeCatalogJson as x, sortCatalogEntries as y, stampCatalogDates as z };
@@ -1,3 +1,128 @@
1
+ // src/workspace-settings/sha256.ts
2
+ var ROUND_CONSTANTS = new Uint32Array([
3
+ 1116352408,
4
+ 1899447441,
5
+ 3049323471,
6
+ 3921009573,
7
+ 961987163,
8
+ 1508970993,
9
+ 2453635748,
10
+ 2870763221,
11
+ 3624381080,
12
+ 310598401,
13
+ 607225278,
14
+ 1426881987,
15
+ 1925078388,
16
+ 2162078206,
17
+ 2614888103,
18
+ 3248222580,
19
+ 3835390401,
20
+ 4022224774,
21
+ 264347078,
22
+ 604807628,
23
+ 770255983,
24
+ 1249150122,
25
+ 1555081692,
26
+ 1996064986,
27
+ 2554220882,
28
+ 2821834349,
29
+ 2952996808,
30
+ 3210313671,
31
+ 3336571891,
32
+ 3584528711,
33
+ 113926993,
34
+ 338241895,
35
+ 666307205,
36
+ 773529912,
37
+ 1294757372,
38
+ 1396182291,
39
+ 1695183700,
40
+ 1986661051,
41
+ 2177026350,
42
+ 2456956037,
43
+ 2730485921,
44
+ 2820302411,
45
+ 3259730800,
46
+ 3345764771,
47
+ 3516065817,
48
+ 3600352804,
49
+ 4094571909,
50
+ 275423344,
51
+ 430227734,
52
+ 506948616,
53
+ 659060556,
54
+ 883997877,
55
+ 958139571,
56
+ 1322822218,
57
+ 1537002063,
58
+ 1747873779,
59
+ 1955562222,
60
+ 2024104815,
61
+ 2227730452,
62
+ 2361852424,
63
+ 2428436474,
64
+ 2756734187,
65
+ 3204031479,
66
+ 3329325298
67
+ ]);
68
+ var INITIAL_HASH = [
69
+ 1779033703,
70
+ 3144134277,
71
+ 1013904242,
72
+ 2773480762,
73
+ 1359893119,
74
+ 2600822924,
75
+ 528734635,
76
+ 1541459225
77
+ ];
78
+ function rotateRight(value, bits) {
79
+ return value >>> bits | value << 32 - bits;
80
+ }
81
+ function sha256Bytes(input) {
82
+ const blocks = new Uint8Array(Math.ceil((input.length + 9) / 64) * 64);
83
+ blocks.set(input);
84
+ blocks[input.length] = 128;
85
+ const view = new DataView(blocks.buffer);
86
+ const bitLength = input.length * 8;
87
+ view.setUint32(blocks.length - 8, Math.floor(bitLength / 4294967296));
88
+ view.setUint32(blocks.length - 4, bitLength >>> 0);
89
+ const hash = [...INITIAL_HASH];
90
+ const schedule = new Uint32Array(64);
91
+ for (let offset = 0; offset < blocks.length; offset += 64) {
92
+ for (let i = 0; i < 16; i += 1) schedule[i] = view.getUint32(offset + i * 4);
93
+ for (let i = 16; i < 64; i += 1) {
94
+ const early = schedule[i - 15];
95
+ const late = schedule[i - 2];
96
+ const s0 = rotateRight(early, 7) ^ rotateRight(early, 18) ^ early >>> 3;
97
+ const s1 = rotateRight(late, 17) ^ rotateRight(late, 19) ^ late >>> 10;
98
+ schedule[i] = schedule[i - 16] + s0 + schedule[i - 7] + s1;
99
+ }
100
+ let [a, b, c, d, e, f, g, h] = hash;
101
+ for (let i = 0; i < 64; i += 1) {
102
+ const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
103
+ const choice = e & f ^ ~e & g;
104
+ const temp1 = h + s1 + choice + ROUND_CONSTANTS[i] + schedule[i] >>> 0;
105
+ const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
106
+ const majority = a & b ^ a & c ^ b & c;
107
+ const temp2 = s0 + majority >>> 0;
108
+ h = g;
109
+ g = f;
110
+ f = e;
111
+ e = d + temp1 >>> 0;
112
+ d = c;
113
+ c = b;
114
+ b = a;
115
+ a = temp1 + temp2 >>> 0;
116
+ }
117
+ const round = [a, b, c, d, e, f, g, h];
118
+ for (let i = 0; i < 8; i += 1) hash[i] = hash[i] + round[i] >>> 0;
119
+ }
120
+ const digest = new Uint8Array(32);
121
+ const out = new DataView(digest.buffer);
122
+ for (let i = 0; i < 8; i += 1) out.setUint32(i * 4, hash[i]);
123
+ return digest;
124
+ }
125
+
1
126
  // src/workspace-settings/catalog.ts
2
127
  var CATALOG_GENERATOR = "docblocks-workspace-catalog";
3
128
  var CATALOG_FORMAT_VERSION = 1;
@@ -168,11 +293,14 @@ function orderedEntry(entry) {
168
293
  "description",
169
294
  "author",
170
295
  "date",
296
+ "firstPublishedDate",
297
+ "lastModifiedDate",
171
298
  "tags",
172
299
  "theme",
173
300
  "outline",
174
301
  "wordCount",
175
302
  "excerpt",
303
+ "contentHash",
176
304
  "markdown"
177
305
  ];
178
306
  for (const key of keys) {
@@ -202,6 +330,50 @@ var CATALOG_JSON_MARKER = /^\uFEFF?\{\s*"generator"\s*:\s*"docblocks-workspace-c
202
330
  function isGeneratedCatalogJson(text) {
203
331
  return CATALOG_JSON_MARKER.test(text.slice(0, 256));
204
332
  }
333
+ var CATALOG_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2}))?$/;
334
+ var CONTENT_HASH_PATTERN = /^[0-9a-f]{64}$/;
335
+ function catalogDate(value) {
336
+ return typeof value === "string" && CATALOG_DATE_PATTERN.test(value) && Number.isFinite(Date.parse(value)) ? value : void 0;
337
+ }
338
+ function readCatalogDateHistory(text) {
339
+ const history = /* @__PURE__ */ new Map();
340
+ if (!isGeneratedCatalogJson(text)) return history;
341
+ let parsed;
342
+ try {
343
+ parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
344
+ } catch {
345
+ return history;
346
+ }
347
+ const documents = parsed?.documents;
348
+ if (!Array.isArray(documents)) return history;
349
+ for (const document of documents) {
350
+ if (!document || typeof document !== "object") continue;
351
+ const record = document;
352
+ const first = catalogDate(record.firstPublishedDate);
353
+ if (typeof record.path !== "string" || !first) continue;
354
+ const hash = record.contentHash;
355
+ history.set(record.path, {
356
+ firstPublishedDate: first,
357
+ lastModifiedDate: catalogDate(record.lastModifiedDate) ?? first,
358
+ ...typeof hash === "string" && CONTENT_HASH_PATTERN.test(hash) ? { contentHash: hash } : {}
359
+ });
360
+ }
361
+ return history;
362
+ }
363
+ function stampCatalogDates(catalog, history, now) {
364
+ const timestamp = now.toISOString().replace(/\.\d{3}Z$/, "Z");
365
+ return {
366
+ ...catalog,
367
+ documents: catalog.documents.map((entry) => {
368
+ const previous = history.get(entry.path);
369
+ return {
370
+ ...entry,
371
+ firstPublishedDate: previous?.firstPublishedDate ?? timestamp,
372
+ lastModifiedDate: previous && previous.contentHash === entry.contentHash ? previous.lastModifiedDate : timestamp
373
+ };
374
+ })
375
+ };
376
+ }
205
377
  var CATALOG_HTML_MARKER = /<!-- docblocks-workspace-catalog v1 inputs=sha256:([0-9a-f]{64}) -->/;
206
378
  var CATALOG_HTML_MARKER_SCAN_CHARACTERS = 2048;
207
379
  function stampCatalogHtml(html, inputsDigest) {
@@ -277,8 +449,10 @@ function buildCatalogMarkdownDocument(catalog, outputPath) {
277
449
  return { type: "document", children };
278
450
  }
279
451
  async function sha256Hex(text) {
280
- const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
281
- return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
452
+ const bytes = new TextEncoder().encode(text);
453
+ const subtle = globalThis.crypto?.subtle;
454
+ const digest = subtle ? new Uint8Array(await subtle.digest("SHA-256", bytes)) : sha256Bytes(bytes);
455
+ return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
282
456
  }
283
457
 
284
458
  // src/workspace-settings/outside-in-layout.ts
@@ -369,6 +543,8 @@ export {
369
543
  createWorkspaceCatalog,
370
544
  serializeCatalogJson,
371
545
  isGeneratedCatalogJson,
546
+ readCatalogDateHistory,
547
+ stampCatalogDates,
372
548
  stampCatalogHtml,
373
549
  readCatalogHtmlDigest,
374
550
  isGeneratedCatalogHtml,
@@ -13,6 +13,7 @@ var WORKSPACE_SETTINGS_LIMITS = {
13
13
  maxFileBytes: 64 * 1024,
14
14
  maxTitleCharacters: 256,
15
15
  maxPathCharacters: 1024,
16
+ maxUrlCharacters: 2048,
16
17
  maxSchemaCharacters: 2048,
17
18
  maxExcludes: 64,
18
19
  minKeep: 1,
@@ -21,6 +22,7 @@ var WORKSPACE_SETTINGS_LIMITS = {
21
22
  var WORKSPACE_THEME_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
22
23
  var CATALOG_SORT_ORDERS = ["title", "path", "date"];
23
24
  var CATALOG_JSON_CONTENT_MODES = ["metadata", "markdown"];
25
+ var WORKSPACE_OUTPUT_TEMPLATE_FORMATS = ["html"];
24
26
  var EMPTY_WORKSPACE_SETTINGS = Object.freeze({
25
27
  version: WORKSPACE_SETTINGS_VERSION
26
28
  });
@@ -85,6 +87,63 @@ function boundedPath(value, label) {
85
87
  if (!path) fail(`${label} must not be the workspace root.`);
86
88
  return path;
87
89
  }
90
+ var OUTPUT_TEMPLATE_EXTENSIONS = {
91
+ html: [".html", ".htm"]
92
+ };
93
+ function validateOutputTemplatePath(value, format) {
94
+ const label = `documents.outputTemplates.${format}`;
95
+ const path = boundedPath(value, label);
96
+ const lower = path.toLowerCase();
97
+ if (!OUTPUT_TEMPLATE_EXTENSIONS[format].some((extension) => lower.endsWith(extension))) {
98
+ fail(`${label} must end with ${OUTPUT_TEMPLATE_EXTENSIONS[format].join(" or ")}.`);
99
+ }
100
+ return path;
101
+ }
102
+ function checkOutputTemplatePath(value, format) {
103
+ try {
104
+ return { ok: true, path: validateOutputTemplatePath(value, format) };
105
+ } catch (error) {
106
+ return { ok: false, message: shapeMessage(error) };
107
+ }
108
+ }
109
+ function validateSiteUrl(value) {
110
+ const label = "documents.siteUrl";
111
+ if (typeof value !== "string" || value.length === 0 || value.length > WORKSPACE_SETTINGS_LIMITS.maxUrlCharacters) {
112
+ fail(
113
+ `${label} must be a URL of at most ${WORKSPACE_SETTINGS_LIMITS.maxUrlCharacters} characters.`
114
+ );
115
+ }
116
+ let url;
117
+ try {
118
+ url = new URL(value);
119
+ } catch {
120
+ fail(`${label} must be an absolute URL, such as "https://example.com/posts/".`);
121
+ }
122
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
123
+ fail(`${label} must be an http or https URL.`);
124
+ }
125
+ if (url.search || url.hash || value.includes("?") || value.includes("#")) {
126
+ fail(`${label} must not have a query or fragment.`);
127
+ }
128
+ return value;
129
+ }
130
+ function optionalSiteUrl(value) {
131
+ return value === void 0 ? void 0 : validateSiteUrl(value);
132
+ }
133
+ function checkSiteUrl(value) {
134
+ try {
135
+ return { ok: true, url: validateSiteUrl(value) };
136
+ } catch (error) {
137
+ return { ok: false, message: shapeMessage(error) };
138
+ }
139
+ }
140
+ function parseOutputTemplates(value) {
141
+ if (value === void 0) return void 0;
142
+ const record = exactRecord(value, "documents.outputTemplates", WORKSPACE_OUTPUT_TEMPLATE_FORMATS);
143
+ return compact({
144
+ html: record.html === void 0 ? void 0 : validateOutputTemplatePath(record.html, "html")
145
+ });
146
+ }
88
147
  var OUTPUT_EXTENSIONS = {
89
148
  html: [".html", ".htm"],
90
149
  json: [".json"]
@@ -146,6 +205,7 @@ function compact(value) {
146
205
  const entries = Object.entries(value).filter(([, field]) => field !== void 0);
147
206
  return entries.length > 0 ? Object.fromEntries(entries) : void 0;
148
207
  }
208
+ var DOCUMENT_SETTINGS_KEYS = ["defaultTheme", "outputTemplates", "siteUrl"];
149
209
  function parseWorkspaceSettings(value) {
150
210
  const root = isRecord(value) ? value : fail("Workspace settings must be a JSON object.");
151
211
  const version = root.version;
@@ -169,9 +229,11 @@ function parseWorkspaceSettings(value) {
169
229
  }
170
230
  let documents;
171
231
  if (root.documents !== void 0) {
172
- const record = exactRecord(root.documents, "documents", ["defaultTheme"]);
232
+ const record = exactRecord(root.documents, "documents", DOCUMENT_SETTINGS_KEYS);
173
233
  documents = compact({
174
- defaultTheme: optionalTheme(record.defaultTheme, "documents.defaultTheme")
234
+ defaultTheme: optionalTheme(record.defaultTheme, "documents.defaultTheme"),
235
+ outputTemplates: parseOutputTemplates(record.outputTemplates),
236
+ siteUrl: optionalSiteUrl(record.siteUrl)
175
237
  });
176
238
  }
177
239
  let versionHistory;
@@ -278,7 +340,12 @@ function serializeWorkspaceSettings(settings) {
278
340
  if (normalized.$schema !== void 0) ordered.$schema = normalized.$schema;
279
341
  ordered.version = normalized.version;
280
342
  if (normalized.documents) {
281
- ordered.documents = pick(normalized.documents, ["defaultTheme"]);
343
+ const { outputTemplates } = normalized.documents;
344
+ ordered.documents = {
345
+ ...pick(normalized.documents, ["defaultTheme"]),
346
+ ...outputTemplates ? { outputTemplates: pick(outputTemplates, WORKSPACE_OUTPUT_TEMPLATE_FORMATS) } : {},
347
+ ...pick(normalized.documents, ["siteUrl"])
348
+ };
282
349
  }
283
350
  if (normalized.versionHistory) {
284
351
  ordered.versionHistory = pick(normalized.versionHistory, ["enabled", "keep"]);
@@ -340,7 +407,7 @@ function optionalSection(key, value) {
340
407
  }
341
408
  function diffWorkspaceSettings(before, after) {
342
409
  const base = before ?? EMPTY_WORKSPACE_SETTINGS;
343
- const documents = diffFields(base.documents, after.documents, ["defaultTheme"]);
410
+ const documents = diffFields(base.documents, after.documents, DOCUMENT_SETTINGS_KEYS);
344
411
  const versionHistory = diffFields(base.versionHistory, after.versionHistory, ["enabled", "keep"]);
345
412
  const top = diffFields(
346
413
  base.catalog,
@@ -378,12 +445,14 @@ function parseWorkspaceSettingsPatch(value) {
378
445
  const nullable = (field, parse) => field === null ? null : parse(field);
379
446
  const result = {};
380
447
  if (root.documents !== void 0) {
381
- const record = exactRecord(root.documents, "documents", ["defaultTheme"]);
448
+ const record = exactRecord(root.documents, "documents", DOCUMENT_SETTINGS_KEYS);
382
449
  result.documents = compact({
383
450
  defaultTheme: nullable(
384
451
  record.defaultTheme,
385
452
  (input) => optionalTheme(input, "documents.defaultTheme")
386
- )
453
+ ),
454
+ outputTemplates: nullable(record.outputTemplates, parseOutputTemplates),
455
+ siteUrl: nullable(record.siteUrl, optionalSiteUrl)
387
456
  }) ?? {};
388
457
  }
389
458
  if (root.versionHistory !== void 0) {
@@ -451,11 +520,20 @@ function tryApplyWorkspaceSettingsPatch(base, patch) {
451
520
  }
452
521
  }
453
522
  function isDefaultWorkspaceSettings(settings) {
454
- return settings?.documents?.defaultTheme === void 0 && settings?.versionHistory?.enabled === void 0 && settings?.versionHistory?.keep === void 0 && settings?.catalog?.html?.enabled !== true && settings?.catalog?.json?.enabled !== true;
523
+ return settings?.documents?.defaultTheme === void 0 && settings?.documents?.outputTemplates === void 0 && settings?.documents?.siteUrl === void 0 && settings?.versionHistory?.enabled === void 0 && settings?.versionHistory?.keep === void 0 && settings?.catalog?.html?.enabled !== true && settings?.catalog?.json?.enabled !== true;
455
524
  }
456
525
  function resolveWorkspaceDefaultThemeId(settings) {
457
526
  return settings?.documents?.defaultTheme;
458
527
  }
528
+ function resolveWorkspaceOutputTemplate(settings, format) {
529
+ const templates = settings?.documents?.outputTemplates;
530
+ return format === "html" ? templates?.html : void 0;
531
+ }
532
+ function resolveWorkspaceSiteUrl(settings) {
533
+ const url = settings?.documents?.siteUrl;
534
+ if (url === void 0) return void 0;
535
+ return url.endsWith("/") ? url : `${url}/`;
536
+ }
459
537
  function resolveFallbackThemeId(frontmatterThemeId, settings) {
460
538
  if (frontmatterThemeId) return void 0;
461
539
  return resolveWorkspaceDefaultThemeId(settings);
@@ -494,7 +572,12 @@ export {
494
572
  WORKSPACE_THEME_ID_PATTERN,
495
573
  CATALOG_SORT_ORDERS,
496
574
  CATALOG_JSON_CONTENT_MODES,
575
+ WORKSPACE_OUTPUT_TEMPLATE_FORMATS,
497
576
  EMPTY_WORKSPACE_SETTINGS,
577
+ validateOutputTemplatePath,
578
+ checkOutputTemplatePath,
579
+ validateSiteUrl,
580
+ checkSiteUrl,
498
581
  validateCatalogOutputPath,
499
582
  checkCatalogOutputPath,
500
583
  parseWorkspaceSettings,
@@ -508,6 +591,8 @@ export {
508
591
  tryApplyWorkspaceSettingsPatch,
509
592
  isDefaultWorkspaceSettings,
510
593
  resolveWorkspaceDefaultThemeId,
594
+ resolveWorkspaceOutputTemplate,
595
+ resolveWorkspaceSiteUrl,
511
596
  resolveFallbackThemeId,
512
597
  resolveWorkspaceVersioningEnabled,
513
598
  resolveCatalogOutputs
@@ -21,6 +21,7 @@ declare const WORKSPACE_SETTINGS_LIMITS: {
21
21
  readonly maxFileBytes: number;
22
22
  readonly maxTitleCharacters: 256;
23
23
  readonly maxPathCharacters: 1024;
24
+ readonly maxUrlCharacters: 2048;
24
25
  readonly maxSchemaCharacters: 2048;
25
26
  readonly maxExcludes: 64;
26
27
  readonly minKeep: 1;
@@ -32,9 +33,28 @@ declare const CATALOG_SORT_ORDERS: readonly ["title", "path", "date"];
32
33
  type CatalogSortOrder = (typeof CATALOG_SORT_ORDERS)[number];
33
34
  declare const CATALOG_JSON_CONTENT_MODES: readonly ["metadata", "markdown"];
34
35
  type CatalogJsonContent = (typeof CATALOG_JSON_CONTENT_MODES)[number];
36
+ /** Rendered formats a workspace can name a default page template for. */
37
+ declare const WORKSPACE_OUTPUT_TEMPLATE_FORMATS: readonly ["html"];
38
+ type WorkspaceOutputTemplateFormat = (typeof WORKSPACE_OUTPUT_TEMPLATE_FORMATS)[number];
39
+ /** Workspace-relative page template path, by rendered output format. */
40
+ type WorkspaceOutputTemplates = {
41
+ readonly [Format in WorkspaceOutputTemplateFormat]?: string;
42
+ };
35
43
  interface WorkspaceDocumentSettings {
36
44
  /** Theme for documents whose frontmatter names none. */
37
45
  readonly defaultTheme?: string;
46
+ /**
47
+ * Page template for rendered documents whose frontmatter names none
48
+ * (`squisq-output-template`). A document's own choice, including `none`,
49
+ * always wins.
50
+ */
51
+ readonly outputTemplates?: WorkspaceOutputTemplates;
52
+ /**
53
+ * Public `http(s)` address the workspace root is published at, such as
54
+ * `https://example.com/posts/`. Rendered Web pages use it to give their
55
+ * social preview pictures the absolute URLs social sites require.
56
+ */
57
+ readonly siteUrl?: string;
38
58
  }
39
59
  interface WorkspaceVersionHistorySettings {
40
60
  /** Absent inherits the app-wide preference. */
@@ -76,6 +96,26 @@ type WorkspaceSettingsParseResult = {
76
96
  readonly status: 'invalid' | 'unsupported-version';
77
97
  readonly message: string;
78
98
  };
99
+ /** Validate a configured page template path for one rendered format. */
100
+ declare function validateOutputTemplatePath(value: unknown, format: WorkspaceOutputTemplateFormat): WorkspacePath;
101
+ /** Try-form for UI validation messages. */
102
+ declare function checkOutputTemplatePath(value: unknown, format: WorkspaceOutputTemplateFormat): {
103
+ ok: true;
104
+ path: WorkspacePath;
105
+ } | {
106
+ ok: false;
107
+ message: string;
108
+ };
109
+ /** Validate a configured site address: an absolute `http(s)` URL with no query or fragment. */
110
+ declare function validateSiteUrl(value: unknown): string;
111
+ /** Try-form for UI validation messages. */
112
+ declare function checkSiteUrl(value: unknown): {
113
+ ok: true;
114
+ url: string;
115
+ } | {
116
+ ok: false;
117
+ message: string;
118
+ };
79
119
  type CatalogOutputKind = 'html' | 'json';
80
120
  /**
81
121
  * Validate a configured catalog output path. Outputs may never land inside
@@ -135,7 +175,8 @@ declare function tryApplyWorkspaceSettingsPatch(base: WorkspaceSettings | null,
135
175
  };
136
176
  /**
137
177
  * True when settings change nothing about how DocBlocks treats the folder: no
138
- * default theme, no version-history choice, and no catalog output turned on.
178
+ * default theme, page template, or site address, no version-history choice,
179
+ * and no catalog output turned on.
139
180
  * Fields that only configure a disabled output (a catalog title, path, order,
140
181
  * or page theme) are inert. Such settings are never worth creating a
141
182
  * `.docblocks/` folder for.
@@ -143,6 +184,13 @@ declare function tryApplyWorkspaceSettingsPatch(base: WorkspaceSettings | null,
143
184
  declare function isDefaultWorkspaceSettings(settings: WorkspaceSettings | null | undefined): boolean;
144
185
  /** The workspace default theme, or undefined to use the built-in default. */
145
186
  declare function resolveWorkspaceDefaultThemeId(settings: WorkspaceSettings | null | undefined): string | undefined;
187
+ /** The workspace default page template for a rendered format, workspace-relative. */
188
+ declare function resolveWorkspaceOutputTemplate(settings: WorkspaceSettings | null | undefined, format: string): string | undefined;
189
+ /**
190
+ * The address the workspace root is published at, as a folder URL (ending in
191
+ * `/`) that workspace paths resolve against, or undefined when none is set.
192
+ */
193
+ declare function resolveWorkspaceSiteUrl(settings: WorkspaceSettings | null | undefined): string | undefined;
146
194
  /**
147
195
  * The theme a renderer should be told about explicitly. Every Squisq
148
196
  * exporter lets an explicit theme beat the document's own frontmatter, so a
@@ -167,4 +215,4 @@ interface ResolvedCatalogOutputs {
167
215
  /** Effective catalog configuration, or null when no output is enabled. */
168
216
  declare function resolveCatalogOutputs(settings: WorkspaceSettings | null | undefined): ResolvedCatalogOutputs | null;
169
217
 
170
- export { resolveCatalogOutputs as A, resolveFallbackThemeId as B, type CatalogSortOrder as C, DEFAULT_CATALOG_HTML_PATH as D, EMPTY_WORKSPACE_SETTINGS as E, resolveWorkspaceDefaultThemeId as F, resolveWorkspaceVersioningEnabled as G, serializeWorkspaceSettings as H, tryApplyWorkspaceSettingsPatch as I, validateCatalogOutputPath as J, type ResolvedCatalogOutputs as R, UnsupportedWorkspaceSettingsVersionError as U, type WorkspaceSettings as W, type WorkspaceSettingsPatch as a, CATALOG_JSON_CONTENT_MODES as b, CATALOG_SORT_ORDERS as c, type CatalogJsonContent as d, type CatalogOutputKind as e, DEFAULT_CATALOG_JSON_PATH as f, DEFAULT_CATALOG_TITLE as g, WORKSPACE_SETTINGS_DIRECTORY as h, WORKSPACE_SETTINGS_LIMITS as i, WORKSPACE_SETTINGS_PATH as j, WORKSPACE_SETTINGS_VERSION as k, WORKSPACE_THEME_ID_PATTERN as l, type WorkspaceCatalogHtmlSettings as m, type WorkspaceCatalogJsonSettings as n, type WorkspaceCatalogSettings as o, type WorkspaceDocumentSettings as p, type WorkspaceSettingsParseResult as q, type WorkspaceVersionHistorySettings as r, applyWorkspaceSettingsPatch as s, checkCatalogOutputPath as t, diffWorkspaceSettings as u, isDefaultWorkspaceSettings as v, isEmptyWorkspaceSettingsPatch as w, parseWorkspaceSettings as x, parseWorkspaceSettingsPatch as y, parseWorkspaceSettingsText as z };
218
+ export { isDefaultWorkspaceSettings as A, isEmptyWorkspaceSettingsPatch as B, type CatalogSortOrder as C, DEFAULT_CATALOG_HTML_PATH as D, EMPTY_WORKSPACE_SETTINGS as E, parseWorkspaceSettings as F, parseWorkspaceSettingsPatch as G, parseWorkspaceSettingsText as H, resolveCatalogOutputs as I, resolveFallbackThemeId as J, resolveWorkspaceDefaultThemeId as K, resolveWorkspaceOutputTemplate as L, resolveWorkspaceSiteUrl as M, resolveWorkspaceVersioningEnabled as N, serializeWorkspaceSettings as O, tryApplyWorkspaceSettingsPatch as P, validateCatalogOutputPath as Q, type ResolvedCatalogOutputs as R, validateOutputTemplatePath as S, validateSiteUrl as T, UnsupportedWorkspaceSettingsVersionError as U, type WorkspaceSettings as W, type WorkspaceSettingsPatch as a, CATALOG_JSON_CONTENT_MODES as b, CATALOG_SORT_ORDERS as c, type CatalogJsonContent as d, type CatalogOutputKind as e, DEFAULT_CATALOG_JSON_PATH as f, DEFAULT_CATALOG_TITLE as g, WORKSPACE_OUTPUT_TEMPLATE_FORMATS as h, WORKSPACE_SETTINGS_DIRECTORY as i, WORKSPACE_SETTINGS_LIMITS as j, WORKSPACE_SETTINGS_PATH as k, WORKSPACE_SETTINGS_VERSION as l, WORKSPACE_THEME_ID_PATTERN as m, type WorkspaceCatalogHtmlSettings as n, type WorkspaceCatalogJsonSettings as o, type WorkspaceCatalogSettings as p, type WorkspaceDocumentSettings as q, type WorkspaceOutputTemplateFormat as r, type WorkspaceOutputTemplates as s, type WorkspaceSettingsParseResult as t, type WorkspaceVersionHistorySettings as u, applyWorkspaceSettingsPatch as v, checkCatalogOutputPath as w, checkOutputTemplatePath as x, checkSiteUrl as y, diffWorkspaceSettings as z };
@@ -1,4 +1,4 @@
1
- import { W as WorkspaceSettings, a as WorkspaceSettingsPatch } from '../settings-HrrCikVX.js';
1
+ import { W as WorkspaceSettings, a as WorkspaceSettingsPatch } from '../settings-D_CL7AJA.js';
2
2
  import '../workspace-path-CWVrcuPL.js';
3
3
 
4
4
  /**
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseWorkspaceSettings,
3
3
  parseWorkspaceSettingsPatch
4
- } from "../chunk-BC42VRYW.js";
4
+ } from "../chunk-Q7DU2EEB.js";
5
5
  import {
6
6
  HOST_WIRE_LIMITS,
7
7
  isBoundedString
@@ -1,8 +1,8 @@
1
1
  import { o as FileSystemProviderV2, n as FileSystemProviderCapabilities } from '../v2-DwxWjFC0.js';
2
2
  import { a as WorkspacePath } from '../workspace-path-CWVrcuPL.js';
3
- import { d as CatalogDocumentFormat, f as CatalogOutlineEntry } from '../catalog-BQK05tne.js';
3
+ import { e as CatalogDocumentFormat, g as CatalogOutlineEntry } from '../catalog-flfSruLW.js';
4
4
  import '@bendyline/squisq/markdown';
5
- import '../settings-HrrCikVX.js';
5
+ import '../settings-D_CL7AJA.js';
6
6
 
7
7
  /**
8
8
  * What the index reads from. Only read methods are reachable, so the index
@@ -3,7 +3,7 @@ import {
3
3
  chooseOutsideInMarkdownPath,
4
4
  normalizeLineEndings,
5
5
  resolveOutsideInLayout
6
- } from "../chunk-R2JX5N3F.js";
6
+ } from "../chunk-3CCINQX5.js";
7
7
  import {
8
8
  isCompanionDirectoryName,
9
9
  isDiscoverableWorkspaceEntry,
@@ -1,7 +1,7 @@
1
- import { R as ResolvedCatalogOutputs, W as WorkspaceSettings, a as WorkspaceSettingsPatch } from '../settings-HrrCikVX.js';
2
- export { b as CATALOG_JSON_CONTENT_MODES, c as CATALOG_SORT_ORDERS, d as CatalogJsonContent, e as CatalogOutputKind, C as CatalogSortOrder, D as DEFAULT_CATALOG_HTML_PATH, f as DEFAULT_CATALOG_JSON_PATH, g as DEFAULT_CATALOG_TITLE, E as EMPTY_WORKSPACE_SETTINGS, U as UnsupportedWorkspaceSettingsVersionError, h as WORKSPACE_SETTINGS_DIRECTORY, i as WORKSPACE_SETTINGS_LIMITS, j as WORKSPACE_SETTINGS_PATH, k as WORKSPACE_SETTINGS_VERSION, l as WORKSPACE_THEME_ID_PATTERN, m as WorkspaceCatalogHtmlSettings, n as WorkspaceCatalogJsonSettings, o as WorkspaceCatalogSettings, p as WorkspaceDocumentSettings, q as WorkspaceSettingsParseResult, r as WorkspaceVersionHistorySettings, s as applyWorkspaceSettingsPatch, t as checkCatalogOutputPath, u as diffWorkspaceSettings, v as isDefaultWorkspaceSettings, w as isEmptyWorkspaceSettingsPatch, x as parseWorkspaceSettings, y as parseWorkspaceSettingsPatch, z as parseWorkspaceSettingsText, A as resolveCatalogOutputs, B as resolveFallbackThemeId, F as resolveWorkspaceDefaultThemeId, G as resolveWorkspaceVersioningEnabled, H as serializeWorkspaceSettings, I as tryApplyWorkspaceSettingsPatch, J as validateCatalogOutputPath } from '../settings-HrrCikVX.js';
3
- import { C as CatalogEntry, W as WorkspaceCatalog } from '../catalog-BQK05tne.js';
4
- export { a as CATALOG_FORMAT_VERSION, b as CATALOG_GENERATOR, c as CATALOG_LIMITS, d as CatalogDocumentFormat, e as CatalogEntryHints, f as CatalogOutlineEntry, g as CatalogSource, O as OUTSIDE_IN_FORMAT_IDS, h as OutsideInFormatId, i as OutsideInLayout, j as buildCatalogMarkdownDocument, k as catalogEntryForUnreadable, l as catalogEntryFromDocument, m as catalogEntryFromMarkdown, n as chooseOutsideInMarkdownPath, o as createWorkspaceCatalog, p as isGeneratedCatalogHtml, q as isGeneratedCatalogJson, r as readCatalogHtmlDigest, s as relativeCatalogLink, t as relocateOutsideInLayout, u as resolveOutsideInLayout, v as serializeCatalogJson, w as sortCatalogEntries, x as stampCatalogHtml } from '../catalog-BQK05tne.js';
1
+ import { R as ResolvedCatalogOutputs, W as WorkspaceSettings, a as WorkspaceSettingsPatch } from '../settings-D_CL7AJA.js';
2
+ export { b as CATALOG_JSON_CONTENT_MODES, c as CATALOG_SORT_ORDERS, d as CatalogJsonContent, e as CatalogOutputKind, C as CatalogSortOrder, D as DEFAULT_CATALOG_HTML_PATH, f as DEFAULT_CATALOG_JSON_PATH, g as DEFAULT_CATALOG_TITLE, E as EMPTY_WORKSPACE_SETTINGS, U as UnsupportedWorkspaceSettingsVersionError, h as WORKSPACE_OUTPUT_TEMPLATE_FORMATS, i as WORKSPACE_SETTINGS_DIRECTORY, j as WORKSPACE_SETTINGS_LIMITS, k as WORKSPACE_SETTINGS_PATH, l as WORKSPACE_SETTINGS_VERSION, m as WORKSPACE_THEME_ID_PATTERN, n as WorkspaceCatalogHtmlSettings, o as WorkspaceCatalogJsonSettings, p as WorkspaceCatalogSettings, q as WorkspaceDocumentSettings, r as WorkspaceOutputTemplateFormat, s as WorkspaceOutputTemplates, t as WorkspaceSettingsParseResult, u as WorkspaceVersionHistorySettings, v as applyWorkspaceSettingsPatch, w as checkCatalogOutputPath, x as checkOutputTemplatePath, y as checkSiteUrl, z as diffWorkspaceSettings, A as isDefaultWorkspaceSettings, B as isEmptyWorkspaceSettingsPatch, F as parseWorkspaceSettings, G as parseWorkspaceSettingsPatch, H as parseWorkspaceSettingsText, I as resolveCatalogOutputs, J as resolveFallbackThemeId, K as resolveWorkspaceDefaultThemeId, L as resolveWorkspaceOutputTemplate, M as resolveWorkspaceSiteUrl, N as resolveWorkspaceVersioningEnabled, O as serializeWorkspaceSettings, P as tryApplyWorkspaceSettingsPatch, Q as validateCatalogOutputPath, S as validateOutputTemplatePath, T as validateSiteUrl } from '../settings-D_CL7AJA.js';
3
+ import { C as CatalogEntry, W as WorkspaceCatalog } from '../catalog-flfSruLW.js';
4
+ export { a as CATALOG_FORMAT_VERSION, b as CATALOG_GENERATOR, c as CATALOG_LIMITS, d as CatalogDateRecord, e as CatalogDocumentFormat, f as CatalogEntryHints, g as CatalogOutlineEntry, h as CatalogSource, O as OUTSIDE_IN_FORMAT_IDS, i as OutsideInFormatId, j as OutsideInLayout, k as buildCatalogMarkdownDocument, l as catalogEntryForUnreadable, m as catalogEntryFromDocument, n as catalogEntryFromMarkdown, o as chooseOutsideInMarkdownPath, p as createWorkspaceCatalog, q as isGeneratedCatalogHtml, r as isGeneratedCatalogJson, s as readCatalogDateHistory, t as readCatalogHtmlDigest, u as relativeCatalogLink, v as relocateOutsideInLayout, w as resolveOutsideInLayout, x as serializeCatalogJson, y as sortCatalogEntries, z as stampCatalogDates, A as stampCatalogHtml } from '../catalog-flfSruLW.js';
5
5
  import { MarkdownDocument } from '@bendyline/squisq/markdown';
6
6
  import { v as FileSystemVersion, o as FileSystemProviderV2 } from '../v2-DwxWjFC0.js';
7
7
  import { a as WorkspacePath } from '../workspace-path-CWVrcuPL.js';
@@ -37,6 +37,27 @@ declare const WORKSPACE_SETTINGS_JSON_SCHEMA: {
37
37
  readonly type: "string";
38
38
  readonly pattern: string;
39
39
  };
40
+ readonly outputTemplates: {
41
+ readonly type: "object";
42
+ readonly additionalProperties: false;
43
+ readonly description: "Page templates for rendered documents whose frontmatter names none (squisq-output-template). A document’s own choice, including \"none\", always wins.";
44
+ readonly properties: {
45
+ readonly html: {
46
+ readonly pattern: "\\.html?$";
47
+ readonly description: "Workspace-relative HTML page template for Web pages, such as \"_templates/post.html\". Its unnamed <slot> receives the page content.";
48
+ readonly type: "string";
49
+ readonly minLength: 1;
50
+ readonly maxLength: 1024;
51
+ };
52
+ };
53
+ };
54
+ readonly siteUrl: {
55
+ readonly type: "string";
56
+ readonly format: "uri";
57
+ readonly pattern: "^https?://[^?#]+$";
58
+ readonly maxLength: 2048;
59
+ readonly description: "Public address this folder is published at, such as \"https://example.com/posts/\". Gives rendered Web pages absolute social preview image URLs.";
60
+ };
40
61
  };
41
62
  };
42
63
  readonly versionHistory: {
@@ -154,6 +175,11 @@ interface WorkspaceOutputsRenderer {
154
175
  }): string | Promise<string>;
155
176
  /** Parse rendered HTML that has no Markdown companion yet. Optional. */
156
177
  importHtml?(html: string): MarkdownDocument | Promise<MarkdownDocument>;
178
+ /**
179
+ * True for an HTML page template — the chrome other pages are generated
180
+ * through, not a document of its own — which the catalog leaves out. Optional.
181
+ */
182
+ isPageTemplate?(html: string): boolean;
157
183
  }
158
184
  declare const WORKSPACE_OUTPUT_LIMITS: {
159
185
  readonly maxDocuments: 5000;
@@ -188,6 +214,8 @@ interface RefreshWorkspaceOutputsOptions {
188
214
  /** Compute everything, write nothing. */
189
215
  readonly dryRun?: boolean;
190
216
  readonly limits?: Partial<WorkspaceOutputLimits>;
217
+ /** Time stamped on new or changed documents in the catalog JSON. Defaults to now. */
218
+ readonly now?: Date;
191
219
  }
192
220
  type WorkspaceOutputStatus = 'written' | 'unchanged' | 'blocked' | 'would-write';
193
221
  interface WorkspaceOutputResult {
@@ -211,7 +239,7 @@ declare function isCatalogRelevantPath(path: string, settings: WorkspaceSettings
211
239
  /** True for the workspace settings directory or anything inside it. */
212
240
  declare function isWorkspaceSettingsPath(path: string): boolean;
213
241
  /** Walk the workspace and build its catalog without writing anything. */
214
- declare function collectWorkspaceCatalog(io: WorkspaceOutputsIO, resolved: ResolvedCatalogOutputs, renderer: Pick<WorkspaceOutputsRenderer, 'importHtml'>, options?: Omit<RefreshWorkspaceOutputsOptions, 'force' | 'dryRun'>): Promise<WorkspaceCatalog>;
242
+ declare function collectWorkspaceCatalog(io: WorkspaceOutputsIO, resolved: ResolvedCatalogOutputs, renderer: Pick<WorkspaceOutputsRenderer, 'importHtml' | 'isPageTemplate'>, options?: Omit<RefreshWorkspaceOutputsOptions, 'force' | 'dryRun' | 'now'>): Promise<WorkspaceCatalog>;
215
243
  /**
216
244
  * Regenerate every enabled catalog output for one workspace. Throws
217
245
  * {@link WorkspaceOutputsLimitError} when over budget, `FsError('aborted')`
@@ -11,6 +11,8 @@ import {
11
11
  createWorkspaceCatalog,
12
12
  isGeneratedCatalogHtml,
13
13
  isGeneratedCatalogJson,
14
+ normalizeLineEndings,
15
+ readCatalogDateHistory,
14
16
  readCatalogHtmlDigest,
15
17
  relativeCatalogLink,
16
18
  relocateOutsideInLayout,
@@ -18,8 +20,9 @@ import {
18
20
  serializeCatalogJson,
19
21
  sha256Hex,
20
22
  sortCatalogEntries,
23
+ stampCatalogDates,
21
24
  stampCatalogHtml
22
- } from "../chunk-R2JX5N3F.js";
25
+ } from "../chunk-3CCINQX5.js";
23
26
  import {
24
27
  CATALOG_JSON_CONTENT_MODES,
25
28
  CATALOG_SORT_ORDERS,
@@ -28,6 +31,7 @@ import {
28
31
  DEFAULT_CATALOG_TITLE,
29
32
  EMPTY_WORKSPACE_SETTINGS,
30
33
  UnsupportedWorkspaceSettingsVersionError,
34
+ WORKSPACE_OUTPUT_TEMPLATE_FORMATS,
31
35
  WORKSPACE_SETTINGS_DIRECTORY,
32
36
  WORKSPACE_SETTINGS_LIMITS,
33
37
  WORKSPACE_SETTINGS_PATH,
@@ -35,6 +39,8 @@ import {
35
39
  WORKSPACE_THEME_ID_PATTERN,
36
40
  applyWorkspaceSettingsPatch,
37
41
  checkCatalogOutputPath,
42
+ checkOutputTemplatePath,
43
+ checkSiteUrl,
38
44
  diffWorkspaceSettings,
39
45
  isDefaultWorkspaceSettings,
40
46
  isEmptyWorkspaceSettingsPatch,
@@ -44,11 +50,15 @@ import {
44
50
  resolveCatalogOutputs,
45
51
  resolveFallbackThemeId,
46
52
  resolveWorkspaceDefaultThemeId,
53
+ resolveWorkspaceOutputTemplate,
54
+ resolveWorkspaceSiteUrl,
47
55
  resolveWorkspaceVersioningEnabled,
48
56
  serializeWorkspaceSettings,
49
57
  tryApplyWorkspaceSettingsPatch,
50
- validateCatalogOutputPath
51
- } from "../chunk-BC42VRYW.js";
58
+ validateCatalogOutputPath,
59
+ validateOutputTemplatePath,
60
+ validateSiteUrl
61
+ } from "../chunk-Q7DU2EEB.js";
52
62
  import {
53
63
  SQUISQ_RUNTIME_DIRECTORY_NAME,
54
64
  isDiscoverableWorkspaceEntry,
@@ -92,6 +102,25 @@ var WORKSPACE_SETTINGS_JSON_SCHEMA = {
92
102
  defaultTheme: {
93
103
  ...themeId,
94
104
  description: "Theme for documents whose frontmatter does not name one. A document\u2019s own theme always wins."
105
+ },
106
+ outputTemplates: {
107
+ type: "object",
108
+ additionalProperties: false,
109
+ description: 'Page templates for rendered documents whose frontmatter names none (squisq-output-template). A document\u2019s own choice, including "none", always wins.',
110
+ properties: {
111
+ html: {
112
+ ...relativePath,
113
+ pattern: "\\.html?$",
114
+ description: 'Workspace-relative HTML page template for Web pages, such as "_templates/post.html". Its unnamed <slot> receives the page content.'
115
+ }
116
+ }
117
+ },
118
+ siteUrl: {
119
+ type: "string",
120
+ format: "uri",
121
+ pattern: "^https?://[^?#]+$",
122
+ maxLength: WORKSPACE_SETTINGS_LIMITS.maxUrlCharacters,
123
+ description: 'Public address this folder is published at, such as "https://example.com/posts/". Gives rendered Web pages absolute social preview image URLs.'
95
124
  }
96
125
  }
97
126
  },
@@ -351,7 +380,7 @@ async function collectWorkspaceCatalog(io, resolved, renderer, options = {}) {
351
380
  const text = new TextDecoder("utf-8").decode(read.data);
352
381
  if (target.mode === "markdown") {
353
382
  entry = await catalogEntryFromMarkdown(target.source, text, { includeMarkdown });
354
- } else if (isGeneratedCatalogHtml(text)) {
383
+ } else if (isGeneratedCatalogHtml(text) || renderer.isPageTemplate?.(text)) {
355
384
  entry = null;
356
385
  } else if (renderer.importHtml) {
357
386
  const head = text.slice(0, 64 * 1024);
@@ -362,6 +391,7 @@ async function collectWorkspaceCatalog(io, resolved, renderer, options = {}) {
362
391
  } else {
363
392
  entry = catalogEntryForUnreadable(target.source);
364
393
  }
394
+ if (entry) entry = { ...entry, contentHash: await sha256Hex(normalizeLineEndings(text)) };
365
395
  }
366
396
  cache?.set(target.source.path, {
367
397
  readPath: target.readPath,
@@ -426,7 +456,9 @@ async function writeOutput(io, request, limits, options) {
426
456
  return blocked(`${path} is an edited document. Choose another catalog path.`);
427
457
  }
428
458
  }
429
- const bytes = new TextEncoder().encode(await request.produce());
459
+ const bytes = new TextEncoder().encode(
460
+ await request.produce(existing ? new TextDecoder("utf-8").decode(existing.data) : null)
461
+ );
430
462
  if (bytes.byteLength > limits.maxOutputBytes) {
431
463
  return blocked(
432
464
  `The catalog would exceed ${Math.round(limits.maxOutputBytes / 1024 / 1024)} MiB and was not written.`
@@ -451,13 +483,20 @@ async function refreshWorkspaceOutputs(io, settings, renderer, options = {}) {
451
483
  if (resolved.json) {
452
484
  throwIfAborted(options.signal);
453
485
  const content = resolved.json.content;
486
+ const now = options.now ?? /* @__PURE__ */ new Date();
454
487
  outputs.push(
455
488
  await writeOutput(
456
489
  io,
457
490
  {
458
491
  kind: "json",
459
492
  path: resolved.json.path,
460
- produce: async () => serializeCatalogJson(content === "markdown" ? catalog : metadataCatalog)
493
+ produce: async (previous) => serializeCatalogJson(
494
+ stampCatalogDates(
495
+ content === "markdown" ? catalog : metadataCatalog,
496
+ readCatalogDateHistory(previous ?? ""),
497
+ now
498
+ )
499
+ )
461
500
  },
462
501
  limits,
463
502
  options
@@ -771,6 +810,7 @@ export {
771
810
  WORKSPACE_OUTPUTS_DEBOUNCE_MS,
772
811
  WORKSPACE_OUTPUTS_MAX_WAIT_MS,
773
812
  WORKSPACE_OUTPUT_LIMITS,
813
+ WORKSPACE_OUTPUT_TEMPLATE_FORMATS,
774
814
  WORKSPACE_SETTINGS_DIRECTORY,
775
815
  WORKSPACE_SETTINGS_JSON_SCHEMA,
776
816
  WORKSPACE_SETTINGS_LIMITS,
@@ -784,6 +824,8 @@ export {
784
824
  catalogEntryFromDocument,
785
825
  catalogEntryFromMarkdown,
786
826
  checkCatalogOutputPath,
827
+ checkOutputTemplatePath,
828
+ checkSiteUrl,
787
829
  chooseOutsideInMarkdownPath,
788
830
  collectWorkspaceCatalog,
789
831
  createWorkspaceCatalog,
@@ -800,6 +842,7 @@ export {
800
842
  parseWorkspaceSettings,
801
843
  parseWorkspaceSettingsPatch,
802
844
  parseWorkspaceSettingsText,
845
+ readCatalogDateHistory,
803
846
  readCatalogHtmlDigest,
804
847
  readWorkspaceSettings,
805
848
  refreshWorkspaceOutputs,
@@ -809,12 +852,17 @@ export {
809
852
  resolveFallbackThemeId,
810
853
  resolveOutsideInLayout,
811
854
  resolveWorkspaceDefaultThemeId,
855
+ resolveWorkspaceOutputTemplate,
856
+ resolveWorkspaceSiteUrl,
812
857
  resolveWorkspaceVersioningEnabled,
813
858
  saveWorkspaceSettingsPatch,
814
859
  serializeCatalogJson,
815
860
  serializeWorkspaceSettings,
816
861
  sortCatalogEntries,
862
+ stampCatalogDates,
817
863
  stampCatalogHtml,
818
864
  tryApplyWorkspaceSettingsPatch,
819
- validateCatalogOutputPath
865
+ validateCatalogOutputPath,
866
+ validateOutputTemplatePath,
867
+ validateSiteUrl
820
868
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/docblocks",
3
- "version": "2.7.8",
3
+ "version": "2.7.10",
4
4
  "description": "Core data structures and filesystem abstractions for DocBlocks",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -126,7 +126,7 @@
126
126
  "typecheck": "tsc --noEmit"
127
127
  },
128
128
  "dependencies": {
129
- "@bendyline/squisq": "2.13.6",
129
+ "@bendyline/squisq": "2.13.9",
130
130
  "zod": "3.25.76"
131
131
  }
132
132
  }