@taprootio/site-authoring 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/workspace.js CHANGED
@@ -4,6 +4,7 @@ import { lstat, mkdir, open, readdir, unlink } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
6
  import { atomicWriteFile } from "./atomic-file.js";
7
+ import { parseAuthorReference } from "./authors-contract.js";
7
8
  import { hasAsciiControl, sanitizeDiagnostic, SiteAuthoringError } from "./errors.js";
8
9
  import {
9
10
  noteEntry,
@@ -290,6 +291,9 @@ export const WORKSPACE_LIMITS = Object.freeze({
290
291
  // untouched. Generous for any real authoring workspace, and an explicit
291
292
  // refusal rather than an unbounded buffer for one that is not.
292
293
  pulledBodyBytes: 64 * 1024 * 1024,
294
+ // The site's authors and the members a page can name: a few hundred people
295
+ // at most, but written by the site and read back, so bounded by bytes.
296
+ authorsBytes: 1024 * 1024,
293
297
  files: 5_000,
294
298
  directoryDepth: 12,
295
299
  relativePathBytes: 1_024,
@@ -341,17 +345,31 @@ export function canonicalDocumentHash(document_) {
341
345
 
342
346
  /**
343
347
  * What a push would send for a page, as one hash: the source's bytes plus the
344
- * title, path and description that travel with it (for a `.pm.json` source they
345
- * come from the manifest, not the file). `pages push` skips a page whose key
348
+ * title, path, description and author that travel with it (for a `.pm.json`
349
+ * source they come from the manifest, not the file). `pages push` skips a page whose key
346
350
  * equals the one recorded when the workspace last agreed with the site, so an
347
351
  * unedited page is never re-sent (an update would turn an approved page back
348
352
  * into a draft). A Markdown source also folds in the hash of the document it
349
353
  * converts to, because its media references resolve through the media manifest:
350
354
  * re-uploading an image changes what is sent without changing the file.
351
355
  */
352
- export function pageContentKey(sourceHash, { title, path, description }, documentHash = "") {
356
+ export function pageContentKey(sourceHash, { title, path, description, author }, documentHash = "") {
357
+ // The author joins the key only when there is one (TR01196). Every page
358
+ // without an author keeps the key it has always had, so an existing workspace
359
+ // does not see each of its pages as changed and resend them all, which would
360
+ // turn approved pages back into drafts.
353
361
  return workspaceContentHash(
354
- Buffer.from(JSON.stringify([sourceHash, title ?? "", path ?? "", description ?? "", documentHash]), "utf8"),
362
+ Buffer.from(
363
+ JSON.stringify([
364
+ sourceHash,
365
+ title ?? "",
366
+ path ?? "",
367
+ description ?? "",
368
+ documentHash,
369
+ ...(typeof author === "string" && author !== "" ? [author] : []),
370
+ ]),
371
+ "utf8",
372
+ ),
355
373
  );
356
374
  }
357
375
 
@@ -359,6 +377,12 @@ function normalizeBaselineHash(value) {
359
377
  return typeof value === "string" && WORKSPACE_CONTENT_HASH.test(value) ? value : undefined;
360
378
  }
361
379
 
380
+ // A folded author reference or "" (no author); anything else is a damaged member.
381
+ function normalizeAuthorValue(value) {
382
+ if (value === undefined || value === "") return value;
383
+ return parseAuthorReference(value)?.value;
384
+ }
385
+
362
386
  // A calendar date or "" (no date). Anything else is a damaged baseline member.
363
387
  function normalizeBaselineDisplayDate(value) {
364
388
  return typeof value === "string" && (value === "" || /^\d{4}-\d{2}-\d{2}$/u.test(value)) ? value : undefined;
@@ -375,7 +399,7 @@ function normalizeBaselineDisplayDate(value) {
375
399
  * dropped here instead of becoming a path refusal inside the verb that exists
376
400
  * to repair the manifest.
377
401
  */
378
- function isDiscoverablePageSource(file) {
402
+ export function isDiscoverablePageSource(file) {
379
403
  if (typeof file !== "string" || !file.startsWith(`${PAGES_DIRECTORY}/`)) return false;
380
404
  const segments = file.split("/");
381
405
  return segments.length > 1
@@ -434,9 +458,10 @@ export function pageSourceRegistry(manifest) {
434
458
  const contentKey = normalizeBaselineHash(declared?.contentKey);
435
459
  const revision = normalizePageBodyRevision(declared?.revision);
436
460
  const displayDate = normalizeBaselineDisplayDate(declared?.displayDate);
461
+ const author = normalizeAuthorValue(declared?.author);
437
462
  const baseline =
438
463
  remoteHash === undefined && sourceHash === undefined && revision === undefined && displayDate === undefined
439
- && contentKey === undefined
464
+ && contentKey === undefined && author === undefined
440
465
  ? undefined
441
466
  : {
442
467
  ...(remoteHash === undefined ? {} : { remoteHash }),
@@ -444,12 +469,31 @@ export function pageSourceRegistry(manifest) {
444
469
  ...(contentKey === undefined ? {} : { contentKey }),
445
470
  ...(revision === undefined ? {} : { revision }),
446
471
  ...(displayDate === undefined ? {} : { displayDate }),
472
+ // The author the site held for the page when this workspace last
473
+ // reconciled with it: what a page's own author reference is
474
+ // compared with before anything is sent (TR01196).
475
+ ...(author === undefined || author === "" ? {} : { author }),
447
476
  };
448
477
  const record = {
449
478
  pageId: entry.pageId,
450
479
  file: entry.file,
451
480
  sourceFormat,
452
481
  baseline,
482
+ // What a generated page was when this workspace last pulled it, kept so a
483
+ // pull that cannot read its body does not forget it (TR01195).
484
+ ...(entry.generated !== null && typeof entry.generated === "object" && !Array.isArray(entry.generated)
485
+ ? { generated: entry.generated }
486
+ : {}),
487
+ // What this workspace says the page's metadata is; a .pm.json page's only
488
+ // record of it (TR01194).
489
+ metadata: {
490
+ title: typeof entry.title === "string" ? entry.title : "",
491
+ path: typeof entry.path === "string" ? entry.path : "",
492
+ description: typeof entry.description === "string" ? entry.description : "",
493
+ // The author reference the site holds for the page, or the one a
494
+ // `pages meta set --author` is waiting to send (TR01196).
495
+ author: normalizeAuthorValue(entry.author) ?? "",
496
+ },
453
497
  baselineDiscarded: declared !== undefined && baseline === undefined,
454
498
  formatMismatch: entry.sourceFormat !== undefined && entry.sourceFormat !== sourceFormat,
455
499
  duplicateSource: false,
@@ -1150,6 +1194,14 @@ export function normalizePagePath(value) {
1150
1194
  return trimmed;
1151
1195
  }
1152
1196
 
1197
+ /**
1198
+ * A page path as results show it: the home page is '/', everything else as
1199
+ * stored. The manifest and the wire keep the empty root path.
1200
+ */
1201
+ export function displayPagePath(value) {
1202
+ return (normalizePagePath(value) ?? value) || "/";
1203
+ }
1204
+
1153
1205
  /**
1154
1206
  * Chooses the workspace file name for a pulled page. The server's path is
1155
1207
  * preferred because it is what an author recognizes, but it is server data