@taprootio/site-authoring 0.14.1 → 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/README.md +181 -16
- package/examples/riverbend-wellness/.taproot-site-media.json +23 -0
- package/examples/riverbend-wellness/pages/index.md +1 -17
- package/package.json +2 -2
- package/src/api.js +123 -1
- package/src/authors-contract.js +170 -0
- package/src/cli.js +355 -30
- package/src/constants.js +9 -0
- package/src/content/components.js +64 -6
- package/src/content/free-form-sections.js +21 -4
- package/src/content/link-warnings.js +109 -0
- package/src/content/markdown.js +161 -26
- package/src/content/tiptap-prosemirror.ts +66 -8
- package/src/content/validate-document.js +30 -6
- package/src/content/vocabulary.js +26 -0
- package/src/errors.js +22 -0
- package/src/espalier-shared.js +11 -0
- package/src/fixture-init.js +55 -3
- package/src/font-catalog.js +87 -0
- package/src/footer-contract.js +1 -1
- package/src/output.js +19 -1
- package/src/page-metadata.js +92 -0
- package/src/presentation-reference.js +21 -7
- package/src/problems.js +116 -0
- package/src/reference-help.js +157 -41
- package/src/session.js +13 -0
- package/src/staging-check.js +62 -0
- package/src/theme-proposals.js +393 -0
- package/src/theme-validation.js +31 -10
- package/src/typed-page-reference.js +97 -21
- package/src/typed-pages.js +196 -13
- package/src/verbs/approve.js +63 -14
- package/src/verbs/authors.js +167 -0
- package/src/verbs/deploy.js +5 -28
- package/src/verbs/index.js +22 -0
- package/src/verbs/media-upload.js +127 -30
- package/src/verbs/nav-push.js +7 -0
- package/src/verbs/pages-meta-set.js +303 -0
- package/src/verbs/pages-push.js +814 -224
- package/src/verbs/places.js +61 -0
- package/src/verbs/plan.js +595 -0
- package/src/verbs/pull.js +255 -20
- package/src/verbs/redirects-check.js +4 -10
- package/src/verbs/theme-push.js +68 -36
- package/src/verbs/validate.js +309 -217
- package/src/video-prepare.js +48 -12
- package/src/workspace-check.js +199 -0
- package/src/workspace-ledger.js +173 -0
- package/src/workspace.js +105 -10
package/src/problems.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { SiteAuthoringError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collecting problems instead of stopping at the first (TR01002, TR00823).
|
|
5
|
+
*
|
|
6
|
+
* A check that runs over a whole workspace reports every refusal it finds, so
|
|
7
|
+
* an importer fixing hundreds of converted pages learns them all in one run.
|
|
8
|
+
* Only `SiteAuthoringError`s are collected: anything else is a defect, and
|
|
9
|
+
* hiding it inside a list would turn a crash into a refusal.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The parts of a workspace a check reports a problem against. */
|
|
13
|
+
export const CHECK_AREA = Object.freeze({
|
|
14
|
+
media: "media",
|
|
15
|
+
pages: "pages",
|
|
16
|
+
presentation: "presentation",
|
|
17
|
+
footer: "footer",
|
|
18
|
+
navigation: "navigation",
|
|
19
|
+
redirects: "redirects",
|
|
20
|
+
forms: "forms",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Kept off the JSON so a problem serializes as plain data, but remembered so a
|
|
24
|
+
// single refusal can be rethrown exactly as it was raised.
|
|
25
|
+
const SOURCE = Symbol("source error");
|
|
26
|
+
|
|
27
|
+
function withContext(problem, context, error) {
|
|
28
|
+
const located = {
|
|
29
|
+
...(context.area === undefined ? {} : { area: context.area }),
|
|
30
|
+
...(context.file === undefined || problem.file !== undefined ? {} : { file: context.file }),
|
|
31
|
+
...problem,
|
|
32
|
+
};
|
|
33
|
+
if (located.field === located.file) delete located.field;
|
|
34
|
+
Object.defineProperty(located, SOURCE, { value: error });
|
|
35
|
+
return located;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The problems one refusal stands for: its own list when it carries one, otherwise itself. */
|
|
39
|
+
export function problemsOf(error, context = {}) {
|
|
40
|
+
const own = Array.isArray(error.problems) && error.problems.length > 0
|
|
41
|
+
? error.problems
|
|
42
|
+
: [{ code: error.code, ...(error.field === undefined ? {} : { field: error.field }), message: error.message }];
|
|
43
|
+
return own.map((problem) => withContext(problem, context, error));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Runs `action`, adding its refusal to `problems` instead of throwing. Returns
|
|
48
|
+
* the action's value, or undefined when it refused.
|
|
49
|
+
*/
|
|
50
|
+
export async function collectProblems(problems, context, action) {
|
|
51
|
+
try {
|
|
52
|
+
return await action();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!(error instanceof SiteAuthoringError)) throw error;
|
|
55
|
+
problems.push(...problemsOf(error, context));
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One refusal for a list of problems. When they all came from one refusal it
|
|
62
|
+
* is rethrown as raised, alternatives and differences included, so a check
|
|
63
|
+
* that finds one problem fails exactly as it did before problems were
|
|
64
|
+
* collected. Otherwise the code, message and field are the first refusal's and
|
|
65
|
+
* `error.problems` lists every problem.
|
|
66
|
+
*/
|
|
67
|
+
export function problemsError(problems, summary) {
|
|
68
|
+
const [first] = problems;
|
|
69
|
+
if (problems.every((problem) => problem[SOURCE] !== undefined && problem[SOURCE] === first[SOURCE])) {
|
|
70
|
+
return first[SOURCE];
|
|
71
|
+
}
|
|
72
|
+
return new SiteAuthoringError(
|
|
73
|
+
first[SOURCE]?.code ?? first.code,
|
|
74
|
+
`${summary}: ${problems.length} problems. First: ${first[SOURCE]?.message ?? first.message}`,
|
|
75
|
+
{ field: first[SOURCE]?.field ?? first.field ?? first.file },
|
|
76
|
+
).withProblems(problems);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A whole-workspace check can find thousands of problems. A JSON result keeps
|
|
80
|
+
// as many as fit well inside its 64 KiB bound; stderr lists them all.
|
|
81
|
+
const PROBLEM_BUDGET_BYTES = 40 * 1024;
|
|
82
|
+
|
|
83
|
+
/** The leading items whose JSON fits `budget` bytes. */
|
|
84
|
+
export function withinByteBudget(items, budget) {
|
|
85
|
+
const kept = [];
|
|
86
|
+
let bytes = 0;
|
|
87
|
+
for (const item of items) {
|
|
88
|
+
bytes += Buffer.byteLength(JSON.stringify(item), "utf8") + 1;
|
|
89
|
+
if (bytes > budget) break;
|
|
90
|
+
kept.push(item);
|
|
91
|
+
}
|
|
92
|
+
return kept;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The leading problems that fit the result's byte budget, or a smaller one. */
|
|
96
|
+
export function problemsWithinBudget(problems, budget = PROBLEM_BUDGET_BYTES) {
|
|
97
|
+
return withinByteBudget(problems, Math.min(budget, PROBLEM_BUDGET_BYTES));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Under `apply`, a step refuses to send what its plan did not list: an input
|
|
102
|
+
* edited, or a site state changed, while an earlier step ran was never reviewed.
|
|
103
|
+
*/
|
|
104
|
+
export function planStale(subject, field) {
|
|
105
|
+
return new SiteAuthoringError(
|
|
106
|
+
"apply.plan_stale",
|
|
107
|
+
`${subject} changed after the plan was made, so this step stopped before sending it. Run 'taproot-site plan' again and `
|
|
108
|
+
+ "apply the new planHash.",
|
|
109
|
+
{ field },
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Throws when `problems` is not empty. */
|
|
114
|
+
export function refuseProblems(problems, summary) {
|
|
115
|
+
if (problems.length > 0) throw problemsError(problems, summary);
|
|
116
|
+
}
|
package/src/reference-help.js
CHANGED
|
@@ -6,9 +6,18 @@ import {
|
|
|
6
6
|
COMPONENT_TYPES,
|
|
7
7
|
getComponentDefinition,
|
|
8
8
|
getComponentPropertyReference,
|
|
9
|
+
mapComponentImages,
|
|
9
10
|
} from "./content/components.js";
|
|
10
11
|
import { FREE_FORM_SECTION_REGISTRY } from "./content/free-form-sections.js";
|
|
11
|
-
import {
|
|
12
|
+
import { formatFontCatalog } from "./font-catalog.js";
|
|
13
|
+
import {
|
|
14
|
+
CONTENT_ERROR_CODES,
|
|
15
|
+
CONTENT_LIMITS,
|
|
16
|
+
LINK_REL_TOKENS,
|
|
17
|
+
LINK_TARGETS,
|
|
18
|
+
MARK_TYPES,
|
|
19
|
+
NODE_TYPES,
|
|
20
|
+
} from "./content/vocabulary.js";
|
|
12
21
|
import {
|
|
13
22
|
FIXTURE_CONTRACT_VERSION,
|
|
14
23
|
FIXTURE_DELIVERY_ORIGIN_DOMAIN,
|
|
@@ -55,7 +64,12 @@ import {
|
|
|
55
64
|
REDIRECTS_FILE_NAME,
|
|
56
65
|
} from "./redirects-contract.js";
|
|
57
66
|
import { SETTINGS_GROUPS } from "./settings-catalog.js";
|
|
58
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
formatTypedPageReference,
|
|
69
|
+
getTypedPageReference,
|
|
70
|
+
MARKDOWN_PAGE_TYPES,
|
|
71
|
+
TYPED_PAGE_TYPES,
|
|
72
|
+
} from "./typed-page-reference.js";
|
|
59
73
|
import { PAGE_TEMPLATES } from "./typed-pages.js";
|
|
60
74
|
import {
|
|
61
75
|
INTERNAL_PAGE_BASELINE_DIRECTORY,
|
|
@@ -63,6 +77,7 @@ import {
|
|
|
63
77
|
NAVIGATION_FILE_NAME,
|
|
64
78
|
PAGE_SOURCE_EXTENSIONS,
|
|
65
79
|
PAGE_WORKSPACE_MODE_EDITABLE,
|
|
80
|
+
PAGE_WORKSPACE_MODE_METADATA_ONLY,
|
|
66
81
|
PAGES_DIRECTORY,
|
|
67
82
|
pageSourceFormat,
|
|
68
83
|
SETTINGS_DIRECTORY,
|
|
@@ -71,7 +86,7 @@ import {
|
|
|
71
86
|
|
|
72
87
|
export { getAppearanceReference, getFooterReference, getThemeReference };
|
|
73
88
|
|
|
74
|
-
export const REFERENCE_VERSION =
|
|
89
|
+
export const REFERENCE_VERSION = 35;
|
|
75
90
|
|
|
76
91
|
// The date-limit examples `help forms` prints; tests run them through the form validator.
|
|
77
92
|
export const FORM_DATE_WINDOW_FIELD = Object.freeze({
|
|
@@ -408,7 +423,7 @@ export const REFERENCE_TOPICS = Object.freeze([
|
|
|
408
423
|
Object.freeze({
|
|
409
424
|
name: "media",
|
|
410
425
|
usage: `${CLI_BINARY_NAME} help media`,
|
|
411
|
-
summary: "Describe media selection, naming, and
|
|
426
|
+
summary: "Describe media selection, naming, and where the uploaded media is recorded.",
|
|
412
427
|
}),
|
|
413
428
|
Object.freeze({
|
|
414
429
|
name: "preview",
|
|
@@ -445,6 +460,11 @@ export const REFERENCE_TOPICS = Object.freeze([
|
|
|
445
460
|
usage: `${CLI_BINARY_NAME} help import`,
|
|
446
461
|
summary: "Import an existing WordPress site from its export: mapping rules, order of work, and what to report.",
|
|
447
462
|
}),
|
|
463
|
+
Object.freeze({
|
|
464
|
+
name: "fonts",
|
|
465
|
+
usage: `${CLI_BINARY_NAME} help fonts [category]`,
|
|
466
|
+
summary: "List the font families a published site can load, for the theme's font fields.",
|
|
467
|
+
}),
|
|
448
468
|
Object.freeze({
|
|
449
469
|
name: "fixture",
|
|
450
470
|
usage: `${CLI_BINARY_NAME} help fixture`,
|
|
@@ -462,15 +482,20 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
462
482
|
+ "has said you may; the export names every media URL but carries no image bytes, so downloading originals from "
|
|
463
483
|
+ "the old site also needs that permission. Work in a fresh workspace: 'use' the site, then 'pull'.",
|
|
464
484
|
"Order of work: 1 parse the export; 2 classify every item; 3 stage media; 4 write page sources; 5 check every "
|
|
465
|
-
+ "source; 6 pages push; 7 nav.json and nav push; 8 redirects.json and redirects push; 9 approve, deploy "
|
|
485
|
+
+ "source with validate and pages push --dry-run; 6 pages push; 7 nav.json and nav push; 8 redirects.json and redirects push; 9 approve, deploy "
|
|
466
486
|
+ "--staging, review it; 10 redirects check and delivery check --staging; 11 deploy --production; 12 delivery "
|
|
467
487
|
+ "check --production --url <the origin 'sites' reports as primaryDomain>. Do not promote until the account of unmapped content has been read by the owner.",
|
|
468
488
|
"Classify. Published post: article (the default); recipe when it has an Ingredients list and a Steps list "
|
|
469
489
|
+ "(help page recipe); album when it is a gallery with little text (help page album). Published page: a free-form "
|
|
470
|
-
+ "page at its path, children under their parent's path. Never guess a place review:
|
|
471
|
-
+ "
|
|
490
|
+
+ "page at its path, children under their parent's path. Never guess a place review: see Place reviews "
|
|
491
|
+
+ "below. Not imported, and always "
|
|
472
492
|
+ "listed: drafts, pending, private, scheduled and password-protected items; custom post types; comments; the WordPress front "
|
|
473
493
|
+ "page (Taproot seeds its own home page: ask the owner what belongs there).",
|
|
494
|
+
"Place reviews. A post with a rating and a place becomes a place review when the place can be found: run "
|
|
495
|
+
+ "'places search <name and city>', and if one prediction is plainly the same place, 'places select "
|
|
496
|
+
+ "<googlePlaceId> <sessionToken>' gives the placeId for its front matter (rating: will-return, might-return or "
|
|
497
|
+
+ "wont-return, mapped from the post's own scale). When nothing matches, import it as an article and list it. A travel "
|
|
498
|
+
+ "or food article, or an album, about one place can carry place: <placeId> in its front matter.",
|
|
474
499
|
"Paths and dates. A post's path is '<first category slug, ignoring the default Uncategorized>/<post name>' ('journal/' with no category; a post or page whose slug has no ASCII letters gets a name like post-<id>, listed), a "
|
|
475
500
|
+ "page's is its post name under its parents. Keep the original publication date in displayDate (the date "
|
|
476
501
|
+ "part of wp:post_date). Categories beyond the first, and all tags, have no Taproot equivalent (tags are "
|
|
@@ -488,13 +513,14 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
488
513
|
+ "album when they are the post), links to content you did not import (keep the absolute old link), and images "
|
|
489
514
|
+ "on other hosts. Every item goes in the account with its page and what happened to it.",
|
|
490
515
|
"Media. Save each original in the workspace's media/ directory (PNG, JPEG, GIF or WebP up to 32 MiB each; list SVG, AVIF, HEIC and oversized files, which cannot be uploaded; use the original, not "
|
|
491
|
-
+ "the -300x200 thumbnail WordPress also stores), then
|
|
492
|
-
+ "
|
|
493
|
-
+ "
|
|
494
|
-
|
|
495
|
-
"
|
|
496
|
-
+ "
|
|
497
|
-
+ "
|
|
516
|
+
+ "the -300x200 thumbnail WordPress also stores), then run 'media upload' once for the whole media/ directory (at most 500 "
|
|
517
|
+
+ "files a run). It records every file in .taproot-site-media.json and reports counts and a bounded item list; "
|
|
518
|
+
+ "a run that stopped partway is safe to repeat, because identical files deduplicate.",
|
|
519
|
+
"Check before sending. 'validate' (no credential, no request) checks every page source, media reference and "
|
|
520
|
+
+ "setting in the workspace at once and lists every problem in error.problems with its file, code and field. "
|
|
521
|
+
+ "'pages push --dry-run' then checks the pages against the live site — paths taken, templates, revisions, "
|
|
522
|
+
+ "videos — and sends nothing. 'plan' checks the whole workspace against the site and orders what is left "
|
|
523
|
+
+ "(media, pages, footer, theme, navigation); 'apply --plan <planHash>' runs exactly that plan.",
|
|
498
524
|
"Navigation. A site can have several menus (header, footer): ask which is primary and list the rest. Rebuild it as nav.json after 'pages push', at most three levels deep with canonical UUID ids, 1000 items and 200-character titles (help nav); a label-only dropdown heading (a WordPress '#' link) is a GROUP_HEADER, and a child of an item you cannot map is listed, not promoted (page items carry the resourceId from the "
|
|
499
525
|
+ "manifest; a custom link is an EXTERNAL_URL item; a category or tag item has no target, so list it).",
|
|
500
526
|
"Redirects. At most 2000 entries; never redirect to a page you did not push. Every imported page's old path (from its <link>) becomes a 301 to its new path, unless the two "
|
|
@@ -568,7 +594,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
568
594
|
`Pull the baseline: ${CLI_BINARY_NAME} pull writes settings/taproot-styles.json with both schemes' complete effective `
|
|
569
595
|
+ "themes (the stored theme resolved over the same defaults every consumer renders) beside brand.json, "
|
|
570
596
|
+ "site-header.json and site-publishing-preferences.json. Keep that pull as the baseline you edit; never build "
|
|
571
|
-
+ "a theme from memory or from an example. semanticMappings holds only authored pins, listed in explicitMappingTokens
|
|
597
|
+
+ "a theme from memory or from an example. semanticMappings holds only authored pins, listed in explicitMappingTokens "
|
|
598
|
+
+ "at the root; a context's pins need no marker. "
|
|
572
599
|
+ "site-publishing-preferences.json also carries timeZone, the site's IANA zone (UTC until the owner sets it), "
|
|
573
600
|
+ "which decides what \"today\" means for relative form date bounds; it is read-only here, so ask the owner to set "
|
|
574
601
|
+ "it in the editor under Site settings.",
|
|
@@ -579,10 +606,16 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
579
606
|
"Prepare media locally: PNG, JPEG, GIF and WebP raster files only, addressed relative to the workspace root. Make "
|
|
580
607
|
+ "the variants yourself with any image tool (a transparent logo, a @2x copy, a square favicon); there is no "
|
|
581
608
|
+ "recolor, crop or SVG upload command. Upload each with media upload, then assign lightLogoId and darkLogoId "
|
|
582
|
-
+ "(and brand.faviconId) from the
|
|
583
|
-
`Validate offline: ${CLI_BINARY_NAME} validate checks the
|
|
584
|
-
+ "
|
|
585
|
-
+ "
|
|
609
|
+
+ "(and brand.faviconId) from the image ids .taproot-site-media.json records for those files, so each scheme carries the artwork that reads on its header.",
|
|
610
|
+
`Validate offline: ${CLI_BINARY_NAME} validate checks the whole workspace at once — the complete pair, the `
|
|
611
|
+
+ "appearance files, the footer, every page, navigation and redirects — against the contracts the site enforces, "
|
|
612
|
+
+ "lists every problem in error.problems, and warns when a semanticMappings pin repeats the default on a token "
|
|
613
|
+
+ "your roles would have moved (the pin would keep the role from rendering).",
|
|
614
|
+
`Plan a combined change: when pages, the footer, the theme and navigation all changed, ${CLI_BINARY_NAME} plan `
|
|
615
|
+
+ "checks them against the live site and lists the steps in the order they must run, each ready, blocked or "
|
|
616
|
+
+ `nothing to do; ${CLI_BINARY_NAME} apply --plan <planHash> runs them through the ordinary push verbs and refuses `
|
|
617
|
+
+ "a plan the workspace or the site has moved past. After a failed step, plan again: finished steps read as "
|
|
618
|
+
+ "nothing to do.",
|
|
586
619
|
`Push: ${CLI_BINARY_NAME} theme push --dry-run first: it reads the site and lists the JSON paths at which each `
|
|
587
620
|
+ "settings file differs from it, and says whether the baseline pull recorded is still current. Then "
|
|
588
621
|
+ `${CLI_BINARY_NAME} theme push saves both themes, the appearance scalars and the ten footer scheme colours in `
|
|
@@ -645,10 +678,16 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
645
678
|
"Run 'redirects check' after staging, or read deploy --staging's automatic check. The Node CLI consumes "
|
|
646
679
|
+ "a separate staging handoff and checks the current map at the real edge, using an in-memory cookie and "
|
|
647
680
|
+ "manual redirects. It reports each path, HTTP status and Location without following targets.",
|
|
681
|
+
"The result carries routeCheck (resolved or unresolved) and redirects: { revision, revisionUnchanged, "
|
|
682
|
+
+ "verified, total, checked, matched, failed, items: [{ path, status, location, expectedStatus, matches, "
|
|
683
|
+
+ "error? }], itemsTruncated? }, at the top level of both redirects check and deploy --staging. An entry "
|
|
684
|
+
+ "whose request failed carries error: \"request_failed\". A check that cannot finish at all reports "
|
|
685
|
+
+ "verified: false, an empty items list and redirects.error { code, message }.",
|
|
648
686
|
"verified is true only if every entry matched and the map revision stayed unchanged during the check. "
|
|
649
687
|
+ "The check proves the current map at staging, not a historical deployment. A mismatch means the staged "
|
|
650
688
|
+ "release does not carry the entry (yet); re-run redirects check after the deployment completes, before promoting. Requests are bounded to 90 seconds, eight at a time; "
|
|
651
|
-
+ "large JSON lists are truncated explicitly with failed entries first.
|
|
689
|
+
+ "large JSON lists are truncated explicitly with failed entries first (redirects.itemsTruncated). Progress "
|
|
690
|
+
+ "output reports every entry on stderr; --quiet hides it.",
|
|
652
691
|
"stagingPreview.url is a fresh, single-use two-minute handoff, minted after checks. It is emitted only "
|
|
653
692
|
+ "in final JSON, excluded from progress and GITHUB_OUTPUT; keep it private. The staging cookie lasts "
|
|
654
693
|
+ "five minutes and remains subject to current site/host/credential permission.",
|
|
@@ -829,22 +868,23 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
829
868
|
}),
|
|
830
869
|
media: Object.freeze({
|
|
831
870
|
title: "Media upload contract",
|
|
832
|
-
summary: "Upload workspace-root-relative raster files and videos
|
|
871
|
+
summary: "Upload workspace-root-relative raster files and videos; images are recorded in .taproot-site-media.json and videos report their ids.",
|
|
833
872
|
usage: `${CLI_BINARY_NAME} media upload [path...]`,
|
|
834
873
|
details: Object.freeze([
|
|
835
874
|
"Paths are relative to the configured workspace root, not the shell's current directory.",
|
|
836
875
|
"PNG, JPEG, GIF, and WebP are accepted; retina names such as logo@2x.png and logo@3x.png are supported.",
|
|
837
876
|
"For header logos prefer genuine transparent PNG/WebP, check contrast in both themes, and include a simplified square favicon. See help appearance for branding guidance.",
|
|
838
877
|
"Prepare variants locally with any image tool before uploading: a transparent logo, its @2x copy, a scheme-specific dark logo when one asset does not read on both headers, and a square favicon. The CLI uploads what you give it; it has no recolor, crop, or SVG upload command.",
|
|
839
|
-
"Assign both schemes after uploading: lightLogoId and darkLogoId in settings/taproot-styles.json and brand.faviconId in settings/brand.json take the
|
|
840
|
-
"Each result item
|
|
878
|
+
"Assign both schemes after uploading: lightLogoId and darkLogoId in settings/taproot-styles.json and brand.faviconId in settings/brand.json take the image ids .taproot-site-media.json records for those files; theme push writes them. See help walkthrough for the full design pass.",
|
|
879
|
+
"Each result item reports file, imageId, deduplicated, width, height and processingState; the delivery URLs (src, urls) are in .taproot-site-media.json, not the result, so an upload of any size reports within its bound. media.total and videos.total count every file, and media.itemsTruncated or videos.itemsTruncated says that list stopped short.",
|
|
841
880
|
"MP4, MOV, and WebM files are uploaded as videos (up to 250 MB each; the site's licence sets the cap, and the container is read from the file's bytes, not its name). The CLI reads each file with mediabunny. A file that is H.264 and AAC but not already an MP4 with its index first is rewritten as one without re-encoding; anything else is sent as it is and judged by the server. When the server refuses a file, its one-line message is printed as media.video_refused. A video is ready to play as soon as the upload is confirmed. The CLI sets no poster, because Node has no video decoder: the Videos page picks one automatically for a video uploaded there, and lets the owner choose another frame, so set a CLI-uploaded video's poster there, with its title and caption. The command reports videos.items: { file, videoId, title, caption, fileName, contentType, byteLength, deduplicated, durationMilliseconds, component }, where component.markdown is the component:video fence and component.block the componentBlock node that place it. Needs the site's licence to include video.",
|
|
842
881
|
"Place an uploaded video on a page with a video component: a component:video fence in Markdown or a componentBlock of type video, with the returned videoId and optional maxHeight and borderWidth; the poster, caption and shape are the video's own. See help component video. pages push refuses a video that is not in the site's library; validate checks it against fixture.videoIds.",
|
|
843
|
-
"
|
|
882
|
+
"In Markdown, name an uploaded image by its path (such as media/photo.webp) wherever an image goes: body images, coverImage, album images, component image fields and section images. pages push resolves each path through .taproot-site-media.json, which also keeps the src/urls delivery fields a .pm.json image record carries.",
|
|
844
883
|
]),
|
|
845
884
|
example: Object.freeze({
|
|
846
885
|
command: `${CLI_BINARY_NAME} media upload media/logo@2x.png`,
|
|
847
|
-
|
|
886
|
+
markdownImage: "media/logo@2x.png",
|
|
887
|
+
pmJsonImageRecord: Object.freeze({
|
|
848
888
|
imageId: "<image-uuid>",
|
|
849
889
|
src: "<preferred-delivery-url>",
|
|
850
890
|
urls: Object.freeze([Object.freeze({ minWidth: 640, url: "<responsive-delivery-url>" })]),
|
|
@@ -872,6 +912,10 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
872
912
|
+ "(staging.host_unavailable, staging.authority_denied, staging.surface_refused, or staging.handoff_unavailable) "
|
|
873
913
|
+ "and a recovery step; run staging review to mint another (it works on every surface). Never verify a theme "
|
|
874
914
|
+ "on production instead.",
|
|
915
|
+
"A preview is served on the site's staging address: its own address under Taproot's user-content domain "
|
|
916
|
+
+ "(ontaproot.io in production), or its customer staging domain once that is active. The address has to be "
|
|
917
|
+
+ "live, so a site that has never deployed to staging, or whose staging host is changing, answers "
|
|
918
|
+
+ "preview.staging_unavailable; run 'deploy --staging' once, or wait for the change to finish.",
|
|
875
919
|
"A page path resolves through .taproot-site-manifest.json; a canonical page UUID works directly.",
|
|
876
920
|
"The homepage's manifest path is empty; address it as '/', which resolves to that empty root path.",
|
|
877
921
|
"Preview before approving: approve consumes the draft, so an approved page has no draft left to render and answers preview.no_draft. Review it on staging after a deploy instead.",
|
|
@@ -889,7 +933,7 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
889
933
|
summary:
|
|
890
934
|
`${FIXTURE_MANIFEST_FILE_NAME} binds a directory laid out like a pulled workspace to deterministic identities, `
|
|
891
935
|
+ "so validate can prove it with no credential, no network, and no write.",
|
|
892
|
-
usage: `${CLI_BINARY_NAME} validate <fixture-directory
|
|
936
|
+
usage: `${CLI_BINARY_NAME} validate [<fixture-directory>]`,
|
|
893
937
|
details: Object.freeze([
|
|
894
938
|
`Required root fields: ${FIXTURE_REQUIRED_ROOT_FIELDS.join(", ")}.`,
|
|
895
939
|
`Optional root fields: ${FIXTURE_OPTIONAL_ROOT_FIELDS.join(", ")}. Appearance and footer metadata, when present, `
|
|
@@ -905,7 +949,9 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
905
949
|
+ `sourceFormat, and workspaceMode ${PAGE_WORKSPACE_MODE_EDITABLE}. Album images and the cover image must be `
|
|
906
950
|
+ "listed in fixture.imageIds. "
|
|
907
951
|
+ "status, hasDraft, isGenerated, and description are recorded by pull and not read: the page key set "
|
|
908
|
-
+ "stays open so a fixture from a newer pull is not refused. The homepage's path is the empty string."
|
|
952
|
+
+ "stays open so a fixture from a newer pull is not refused. The homepage's path is the empty string. "
|
|
953
|
+
+ `An entry with workspaceMode ${PAGE_WORKSPACE_MODE_METADATA_ONLY} names a page the fixture has no source for, `
|
|
954
|
+
+ "such as a generated section page navigation links to: pageId, resourceId and path, and no file.",
|
|
909
955
|
`file lives under '${PAGES_DIRECTORY}/' and ends in ${PAGE_SOURCE_EXTENSIONS.join(" or ")}; sourceFormat must be `
|
|
910
956
|
+ `the format that extension implies (${
|
|
911
957
|
PAGE_SOURCE_EXTENSIONS.map((extension) => `${extension} is '${pageSourceFormat(`page${extension}`)}'`).join(
|
|
@@ -928,6 +974,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
928
974
|
}.`,
|
|
929
975
|
`The fixture block carries ${FIXTURE_METADATA_FIELDS.join(", ")}. Every image a page references by imageId must `
|
|
930
976
|
+ "be listed in fixture.imageIds, and every absolute delivery URL a page uses must sit on a declared origin. "
|
|
977
|
+
+ "A Markdown media path resolves through the fixture's own .taproot-site-media.json, as in a workspace; "
|
|
978
|
+
+ "without one, no media path resolves. "
|
|
931
979
|
+ "videoIds is optional: list the ready videos a page may place with a video component, and validate refuses a "
|
|
932
980
|
+ "video component whose videoId is not listed. A live push makes the same check against the site's video library.",
|
|
933
981
|
`Each deliveryOrigins entry is an origin-only HTTPS URL (no path, query, fragment, or credentials) on `
|
|
@@ -937,8 +985,9 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
937
985
|
"validate reads the fixture and writes nothing to it. Copy the directory somewhere writable before editing it.",
|
|
938
986
|
"validate --init <new-directory> exports the current pulled workspace as a validated version-7 fixture with "
|
|
939
987
|
+ "appearance and footer metadata. Run from the workspace or its configured project; --config may select the source.",
|
|
940
|
-
"Initialization keeps editable pages of every template as workspace documents
|
|
941
|
-
+ "
|
|
988
|
+
"Initialization keeps editable pages of every template as workspace documents. A metadata-only page that "
|
|
989
|
+
+ "navigation, the footer or a page links to is kept as a metadata-only entry (initialized.linkedPages); other "
|
|
990
|
+
+ "metadata-only pages are left out (initialized.skippedPages). It never copies credentials, "
|
|
942
991
|
+ "internal reconciliation state, or deployment receipts. UUIDs and HTTP(S) origins are replaced with deterministic "
|
|
943
992
|
+ "fixture identities and example.test origins; URL credentials, queries and fragments are removed. Authored prose is retained.",
|
|
944
993
|
"The destination must be outside the source workspace and must not exist; its parent must already exist without symlinks. Exports are bounded to 64 MiB. "
|
|
@@ -1011,11 +1060,15 @@ export function listComponentTypeReferences() {
|
|
|
1011
1060
|
return COMPONENT_TYPES.map(componentSummary);
|
|
1012
1061
|
}
|
|
1013
1062
|
|
|
1063
|
+
const EXAMPLE_MEDIA_PATH = "media/photo.webp";
|
|
1064
|
+
|
|
1014
1065
|
export function getComponentReference(type) {
|
|
1015
1066
|
const definition = getComponentDefinition(type);
|
|
1016
1067
|
if (!definition) return undefined;
|
|
1017
1068
|
const example = definition.example;
|
|
1018
|
-
|
|
1069
|
+
// Markdown names each image by its media path; pages push resolves it (TR01187).
|
|
1070
|
+
const markdownData = mapComponentImages(type, example, (value) => (value === null ? null : EXAMPLE_MEDIA_PATH));
|
|
1071
|
+
const markdownExample = `\`\`\`component:${type}\n${JSON.stringify(markdownData, null, 2)}\n\`\`\``;
|
|
1019
1072
|
return {
|
|
1020
1073
|
type,
|
|
1021
1074
|
displayName: definition.displayName,
|
|
@@ -1150,12 +1203,14 @@ function backgroundReference() {
|
|
|
1150
1203
|
}),
|
|
1151
1204
|
scrimOpacityByValue: Object.freeze({ ...BACKGROUND_DEFINITION.fields.scrimStrength.opacityByValue }),
|
|
1152
1205
|
deliveryUrlPolicy:
|
|
1153
|
-
"
|
|
1206
|
+
"In Markdown, give image and portraitImage as media paths recorded by media upload (such as \"media/photo.webp\"); pages push stores the image records. In a .pm.json document they are complete site-owned processed-image records: src and every urls[].url must be HTTPS or non-protocol-relative root-relative delivery URLs; credentials, whitespace, controls, and backslashes are rejected.",
|
|
1154
1207
|
compactBehavior:
|
|
1155
1208
|
"At the shared compact breakpoint, portraitImage and portraitFocus replace the landscape image and focus when supplied.",
|
|
1156
1209
|
example: BACKGROUND_EXAMPLE,
|
|
1157
1210
|
markdownExample: `:::section ${
|
|
1158
|
-
JSON.stringify({
|
|
1211
|
+
JSON.stringify({
|
|
1212
|
+
background: { ...BACKGROUND_EXAMPLE, image: "media/class.webp", portraitImage: "media/class-portrait.webp" },
|
|
1213
|
+
})
|
|
1159
1214
|
}\n## Practice with us\n\nFind your next class.\n:::`,
|
|
1160
1215
|
});
|
|
1161
1216
|
}
|
|
@@ -1185,11 +1240,11 @@ function decorationReference() {
|
|
|
1185
1240
|
additionalProperties: false,
|
|
1186
1241
|
fields: sectionAttributeReference().find((attr) => attr.name === "decoration").fields,
|
|
1187
1242
|
deliveryUrlPolicy:
|
|
1188
|
-
"
|
|
1243
|
+
"In Markdown, give image as a media path recorded by media upload (such as \"media/starburst.webp\"); pages push stores the image record. In a .pm.json document it is the complete site-owned processed-image record: src and every urls[].url must be HTTPS or non-protocol-relative root-relative delivery URLs; credentials, whitespace, controls, and backslashes are rejected.",
|
|
1189
1244
|
tintTokens: Object.freeze({ ...DECORATION_DEFINITION.fields.tint.tokenByValue }),
|
|
1190
1245
|
example: DECORATION_EXAMPLE,
|
|
1191
1246
|
markdownExample: `:::section ${
|
|
1192
|
-
JSON.stringify({ decoration: DECORATION_EXAMPLE })
|
|
1247
|
+
JSON.stringify({ decoration: { ...DECORATION_EXAMPLE, image: "media/starburst.webp" } })
|
|
1193
1248
|
}\n## Rooted in warmth\n\nMove with confidence.\n:::`,
|
|
1194
1249
|
maskGuidance:
|
|
1195
1250
|
"Use a transparent PNG or WebP whose alpha channel contains only the mark. The active section context supplies the tint in both schemes.",
|
|
@@ -1478,6 +1533,31 @@ const FREE_FORM_REFERENCE = Object.freeze({
|
|
|
1478
1533
|
+ "refuses before changing anything under pages/ and preserves the site's version as internal state. A "
|
|
1479
1534
|
+ "locally edited .pm.json is kept rather than overwritten, and only collides when the site changed too.",
|
|
1480
1535
|
conflictError: "pages.pull_conflict",
|
|
1536
|
+
metadata:
|
|
1537
|
+
"A .pm.json page's title, path and description live in the manifest, not the file. Change the title or "
|
|
1538
|
+
+ "description with pages meta set <path> --title/--description; a Markdown page keeps them in its front "
|
|
1539
|
+
+ "matter. pull keeps such an edit until pages push sends it, and a site edit to the same page is a "
|
|
1540
|
+
+ "pages.pull_conflict. A description over 160 characters is warned about (search results cut it off); "
|
|
1541
|
+
+ "over 1000 is refused.",
|
|
1542
|
+
authorship:
|
|
1543
|
+
"Pages are authorless by default: a page created without an author publishes with no byline, and pages push "
|
|
1544
|
+
+ "lists the pages it created that way in pages.authorless. Name an author with author: in the front matter "
|
|
1545
|
+
+ "(every template) or, for a .pm.json page, pages meta set <path> --author. An author is a site author "
|
|
1546
|
+
+ "without a Taproot account, named by handle (jane-doe), or a site member who can create pages, named by "
|
|
1547
|
+
+ "email address. authors list shows who can be named and rewrites authors.json; authors add <handle> "
|
|
1548
|
+
+ "--name <name> [--email <address>] creates a site author, whose address stays private. A site author's "
|
|
1549
|
+
+ "name appears on the byline and in structured data, with no profile link or follow button. authors add "
|
|
1550
|
+
+ "again with the same handle and name succeeds (existing: true); another name for a taken handle is "
|
|
1551
|
+
+ "authors.handle_taken. Only the CLI assigns authors, and only to a page that has none. authors.json is as old "
|
|
1552
|
+
+ "as the last pull or authors list, so offline commands (validate, pages meta set) only warn "
|
|
1553
|
+
+ "(pages.author_unverified) about a name it lacks; pages push and plan read the site's current list once on a "
|
|
1554
|
+
+ "miss and refuse only a name the site lacks. The CLI only fills a page that has no author: a different one "
|
|
1555
|
+
+ "already on the page is pages.author_conflict, which names the way out (pages meta set <path> --author "
|
|
1556
|
+
+ "<the site's author> for a .pm.json page, the author: line for Markdown, or pages meta set <path> --author "
|
|
1557
|
+
+ "\"\" to drop an author that was never sent), naming the author a page already has changes nothing, and a "
|
|
1558
|
+
+ "name that is unknown or belongs to a member who cannot create pages is pages.author_unknown either way. "
|
|
1559
|
+
+ "Text that is neither a handle nor an address is pages.author_invalid.",
|
|
1560
|
+
authorshipErrors: Object.freeze(["pages.author_invalid", "pages.author_unknown", "pages.author_conflict"]),
|
|
1481
1561
|
conflictDetail:
|
|
1482
1562
|
"A reported conflict names the JSON paths at which the site's document differs from the copy this workspace "
|
|
1483
1563
|
+ "last reconciled with, in error.differences, which carries three states. Absent: the two documents were "
|
|
@@ -1529,13 +1609,23 @@ const FREE_FORM_REFERENCE = Object.freeze({
|
|
|
1529
1609
|
inheritedForTracked: true,
|
|
1530
1610
|
defaultForNew: "",
|
|
1531
1611
|
}),
|
|
1612
|
+
Object.freeze({
|
|
1613
|
+
name: "author",
|
|
1614
|
+
type: "string",
|
|
1615
|
+
requiredForNew: false,
|
|
1616
|
+
inheritedForTracked: true,
|
|
1617
|
+
defaultForNew: "",
|
|
1618
|
+
description:
|
|
1619
|
+
"A site author's handle (jane-doe) or a member's email address. Omit it for an authorless page; it "
|
|
1620
|
+
+ "only fills a page that has none, and never replaces one.",
|
|
1621
|
+
}),
|
|
1532
1622
|
Object.freeze({
|
|
1533
1623
|
name: "template",
|
|
1534
1624
|
type: "string",
|
|
1535
1625
|
requiredForNew: false,
|
|
1536
1626
|
inheritedForTracked: false,
|
|
1537
1627
|
defaultForNew: "free-form",
|
|
1538
|
-
description: `Other templates: ${
|
|
1628
|
+
description: `Other templates: ${MARKDOWN_PAGE_TYPES.join(", ")}; see '${CLI_BINARY_NAME} help pages'.`,
|
|
1539
1629
|
}),
|
|
1540
1630
|
]),
|
|
1541
1631
|
}),
|
|
@@ -1556,6 +1646,25 @@ const FREE_FORM_REFERENCE = Object.freeze({
|
|
|
1556
1646
|
root: Object.freeze({ type: "doc", content: "array of supported block nodes" }),
|
|
1557
1647
|
nodes: NODE_TYPES,
|
|
1558
1648
|
marks: MARK_TYPES,
|
|
1649
|
+
// TR01190: what a link mark's rel and target publish as.
|
|
1650
|
+
links: Object.freeze({
|
|
1651
|
+
markdown: '[Shop](https://shop.example){rel="sponsored nofollow" target="_blank"}',
|
|
1652
|
+
rel: LINK_REL_TOKENS,
|
|
1653
|
+
target: LINK_TARGETS,
|
|
1654
|
+
default:
|
|
1655
|
+
"A link with no rel is followed, whether it goes to this site or another. Choose nofollow, sponsored "
|
|
1656
|
+
+ "(paid or affiliate), or ugc (written by someone else) for a link that should not pass endorsement; the "
|
|
1657
|
+
+ "choice is per link, internal links included.",
|
|
1658
|
+
markdownRule:
|
|
1659
|
+
'Write {rel="…" target="…"} directly after the link\'s closing parenthesis: either attribute alone or both, '
|
|
1660
|
+
+ 'each double-quoted once. rel is space-separated; target="_blank" opens a new tab.',
|
|
1661
|
+
pmJson:
|
|
1662
|
+
'In .pm.json, a link mark carries attrs.rel (a space-separated string, or null) and attrs.target ("_blank" '
|
|
1663
|
+
+ 'opens a new tab and publishes with noopener noreferrer added, "_self" or null keeps the same tab).',
|
|
1664
|
+
refusals:
|
|
1665
|
+
`Any other rel token, opener included, is refused (${CONTENT_ERROR_CODES.linkRel}); so is another target `
|
|
1666
|
+
+ `(${CONTENT_ERROR_CODES.linkTarget}). The editor's old default, "noopener noreferrer nofollow", reads as no choice.`,
|
|
1667
|
+
}),
|
|
1559
1668
|
componentNode: Object.freeze({
|
|
1560
1669
|
type: "componentBlock",
|
|
1561
1670
|
attrs: Object.freeze({
|
|
@@ -1665,7 +1774,8 @@ function schemaConstraints(schema) {
|
|
|
1665
1774
|
|
|
1666
1775
|
function formatSchemaLine(name, schema, required, indent) {
|
|
1667
1776
|
const constraints = schemaConstraints(schema);
|
|
1668
|
-
|
|
1777
|
+
const label = schema.markdownType ? "media path | null in Markdown; object | null in .pm.json" : schemaLabel(schema);
|
|
1778
|
+
return `${indent}${name.padEnd(Math.max(1, 28 - indent.length))} ${label}; ${
|
|
1669
1779
|
required ? "required" : "optional"
|
|
1670
1780
|
}${constraints.length > 0 ? `; ${constraints.join("; ")}` : ""}`;
|
|
1671
1781
|
}
|
|
@@ -1675,7 +1785,7 @@ function nestedSchemaLines(schema, path, indent = " ") {
|
|
|
1675
1785
|
const valuePath = schema.type === "array" ? `${path}[]` : path;
|
|
1676
1786
|
if (!valueSchema?.properties) return [];
|
|
1677
1787
|
|
|
1678
|
-
const lines = [];
|
|
1788
|
+
const lines = valueSchema.markdownType ? [`${indent}${valuePath} in a .pm.json document:`] : [];
|
|
1679
1789
|
for (const property of valueSchema.properties) {
|
|
1680
1790
|
const propertyPath = `${valuePath}.${property.name}`;
|
|
1681
1791
|
lines.push(formatSchemaLine(propertyPath, property.schema, property.required, indent));
|
|
@@ -1719,9 +1829,11 @@ function formatSectionAttr(attr) {
|
|
|
1719
1829
|
|
|
1720
1830
|
function formatRegistryField(field, indent = " ") {
|
|
1721
1831
|
if (field.type === "processed-image") {
|
|
1722
|
-
return `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))}
|
|
1832
|
+
return `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))} media path${
|
|
1833
|
+
field.nullable ? " | null" : ""
|
|
1834
|
+
} in Markdown; processed-image${
|
|
1723
1835
|
field.nullable ? " | null" : ""
|
|
1724
|
-
}; ${field.required ? "required" : `default ${JSON.stringify(field.default)}`}; required keys ${
|
|
1836
|
+
} in .pm.json; ${field.required ? "required" : `default ${JSON.stringify(field.default)}`}; .pm.json required keys ${
|
|
1725
1837
|
field.requiredKeys.join(", ")
|
|
1726
1838
|
}; optional keys ${
|
|
1727
1839
|
field.optionalKeys.join(", ")
|
|
@@ -1761,6 +1873,8 @@ function formatDesignRecipe(recipe) {
|
|
|
1761
1873
|
|
|
1762
1874
|
export function formatReferenceResult(result) {
|
|
1763
1875
|
switch (result.topic) {
|
|
1876
|
+
case "fonts":
|
|
1877
|
+
return formatFontCatalog(result.fonts);
|
|
1764
1878
|
case "topics":
|
|
1765
1879
|
return `Usage: ${CLI_BINARY_NAME} help <topic> [name] [--json]\n\nReference topics:\n${
|
|
1766
1880
|
result.topics.map((topic) => ` ${topic.usage.padEnd(48)} ${topic.summary}`).join("\n")
|
|
@@ -1802,13 +1916,15 @@ export function formatReferenceResult(result) {
|
|
|
1802
1916
|
page.workspace.formats.map((format) => ` ${format.extension.padEnd(9)} ${format.purpose}`).join("\n")
|
|
1803
1917
|
}\n\nOne source per page:\n rule ${sourceRule.rule}\n manifest fields ${
|
|
1804
1918
|
sourceRule.manifestFields.join(", ")
|
|
1805
|
-
}\n pull ${sourceRule.pull}\n internal state ${sourceRule.internalState}\n format change ${sourceRule.formatChange} (${sourceRule.formatChangeError})\n renamed source ${sourceRule.renamedSource} (${sourceRule.renamedSourceError})\n conflicts ${sourceRule.conflict} (${sourceRule.conflictError})\n conflict detail ${sourceRule.conflictDetail}\n push conflicts ${sourceRule.pushConflict} (${sourceRule.pushConflictError})\n revision ${sourceRule.revisionSource}\n recovery ${sourceRule.conflictRecovery}\n push selection ${sourceRule.pushSelection}\n\nSystem pages: ${page.workspace.systemPages}\n\nMarkdown front matter:\n${
|
|
1919
|
+
}\n pull ${sourceRule.pull}\n internal state ${sourceRule.internalState}\n format change ${sourceRule.formatChange} (${sourceRule.formatChangeError})\n renamed source ${sourceRule.renamedSource} (${sourceRule.renamedSourceError})\n conflicts ${sourceRule.conflict} (${sourceRule.conflictError})\n conflict detail ${sourceRule.conflictDetail}\n push conflicts ${sourceRule.pushConflict} (${sourceRule.pushConflictError})\n revision ${sourceRule.revisionSource}\n recovery ${sourceRule.conflictRecovery}\n push selection ${sourceRule.pushSelection}\n authors ${sourceRule.authorship} (${sourceRule.authorshipErrors.join(", ")})\n\nSystem pages: ${page.workspace.systemPages}\n\nMarkdown front matter:\n${
|
|
1806
1920
|
markdownFormat.metadata.map(formatMetadata).join("\n")
|
|
1807
1921
|
}\n\nDocument root: { "type": "doc", "content": [...] }\nSupported nodes: ${
|
|
1808
1922
|
page.document.nodes.join(", ")
|
|
1809
1923
|
}\nSupported marks: ${
|
|
1810
1924
|
page.document.marks.join(", ")
|
|
1811
|
-
}\n\
|
|
1925
|
+
}\n\nLinks:\n markdown ${page.document.links.markdown}\n rel ${
|
|
1926
|
+
page.document.links.rel.join(", ")
|
|
1927
|
+
}\n default ${page.document.links.default}\n in Markdown ${page.document.links.markdownRule}\n in .pm.json ${page.document.links.pmJson}\n refusals ${page.document.links.refusals}\n\nTop-level section container:\n${page.sections.example}\n\nSection attributes:\n${
|
|
1812
1928
|
page.document.sectionNode.attrs.map(formatSectionAttr).join("\n")
|
|
1813
1929
|
}\n\nSection photo background (closed object):\n${
|
|
1814
1930
|
page.sections.background.fields.map((field) => formatRegistryField(field)).join("\n")
|
package/src/session.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
updateCredentialMetadata,
|
|
22
22
|
} from "./credentials.js";
|
|
23
23
|
import { SiteAuthoringError } from "./errors.js";
|
|
24
|
+
import { withinByteBudget } from "./problems.js";
|
|
24
25
|
import { exchangeSiteAuthoringToken, withRefusalGuidance } from "./api.js";
|
|
25
26
|
import { ApiError, SiteApiClient } from "./transport.js";
|
|
26
27
|
import { readStoredApiBaseUrl } from "./settings.js";
|
|
@@ -125,6 +126,9 @@ function siteMissingError() {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
async function loadConfig(invocation) {
|
|
129
|
+
// apply hands its steps the configuration it planned with, so a
|
|
130
|
+
// taproot-site.json edited mid-apply is not read by a later step.
|
|
131
|
+
if (invocation.resolvedConfig !== undefined) return invocation.resolvedConfig;
|
|
128
132
|
return await loadSiteConfig({
|
|
129
133
|
cwd: invocation.cwd ?? process.cwd(),
|
|
130
134
|
configPath: invocation.configPath,
|
|
@@ -552,6 +556,15 @@ export function boundedList(values, maximum) {
|
|
|
552
556
|
: { items, count: values.length, truncated: true };
|
|
553
557
|
}
|
|
554
558
|
|
|
559
|
+
/**
|
|
560
|
+
* The leading values whose JSON fits `budget` bytes, for a list whose entries
|
|
561
|
+
* vary in length: a count bound alone can still pass the 64 KiB result bound.
|
|
562
|
+
*/
|
|
563
|
+
export function boundedByBytes(values, budget) {
|
|
564
|
+
const items = withinByteBudget(values, budget);
|
|
565
|
+
return items.length === values.length ? { items } : { items, truncated: true };
|
|
566
|
+
}
|
|
567
|
+
|
|
555
568
|
export function successResult(verb, siteId, payload = {}) {
|
|
556
569
|
return {
|
|
557
570
|
schemaVersion: RESULT_SCHEMA_VERSION,
|