@taprootio/site-authoring 0.14.1 → 0.15.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 +62 -10
- 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 +58 -1
- package/src/cli.js +147 -22
- package/src/constants.js +6 -0
- package/src/content/components.js +64 -6
- package/src/content/free-form-sections.js +21 -4
- 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/font-catalog.js +87 -0
- package/src/footer-contract.js +1 -1
- package/src/output.js +19 -1
- package/src/presentation-reference.js +21 -7
- package/src/problems.js +116 -0
- package/src/reference-help.js +100 -34
- package/src/session.js +13 -0
- package/src/theme-proposals.js +393 -0
- package/src/theme-validation.js +31 -10
- package/src/typed-page-reference.js +11 -3
- package/src/typed-pages.js +31 -9
- package/src/verbs/index.js +13 -0
- package/src/verbs/media-upload.js +127 -30
- package/src/verbs/nav-push.js +7 -0
- package/src/verbs/pages-push.js +406 -199
- package/src/verbs/places.js +61 -0
- package/src/verbs/plan.js +585 -0
- package/src/verbs/theme-push.js +68 -36
- package/src/verbs/validate.js +264 -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 +47 -4
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,
|
|
@@ -71,7 +80,7 @@ import {
|
|
|
71
80
|
|
|
72
81
|
export { getAppearanceReference, getFooterReference, getThemeReference };
|
|
73
82
|
|
|
74
|
-
export const REFERENCE_VERSION =
|
|
83
|
+
export const REFERENCE_VERSION = 34;
|
|
75
84
|
|
|
76
85
|
// The date-limit examples `help forms` prints; tests run them through the form validator.
|
|
77
86
|
export const FORM_DATE_WINDOW_FIELD = Object.freeze({
|
|
@@ -408,7 +417,7 @@ export const REFERENCE_TOPICS = Object.freeze([
|
|
|
408
417
|
Object.freeze({
|
|
409
418
|
name: "media",
|
|
410
419
|
usage: `${CLI_BINARY_NAME} help media`,
|
|
411
|
-
summary: "Describe media selection, naming, and
|
|
420
|
+
summary: "Describe media selection, naming, and where the uploaded media is recorded.",
|
|
412
421
|
}),
|
|
413
422
|
Object.freeze({
|
|
414
423
|
name: "preview",
|
|
@@ -445,6 +454,11 @@ export const REFERENCE_TOPICS = Object.freeze([
|
|
|
445
454
|
usage: `${CLI_BINARY_NAME} help import`,
|
|
446
455
|
summary: "Import an existing WordPress site from its export: mapping rules, order of work, and what to report.",
|
|
447
456
|
}),
|
|
457
|
+
Object.freeze({
|
|
458
|
+
name: "fonts",
|
|
459
|
+
usage: `${CLI_BINARY_NAME} help fonts [category]`,
|
|
460
|
+
summary: "List the font families a published site can load, for the theme's font fields.",
|
|
461
|
+
}),
|
|
448
462
|
Object.freeze({
|
|
449
463
|
name: "fixture",
|
|
450
464
|
usage: `${CLI_BINARY_NAME} help fixture`,
|
|
@@ -462,15 +476,20 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
462
476
|
+ "has said you may; the export names every media URL but carries no image bytes, so downloading originals from "
|
|
463
477
|
+ "the old site also needs that permission. Work in a fresh workspace: 'use' the site, then 'pull'.",
|
|
464
478
|
"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 "
|
|
479
|
+
+ "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
480
|
+ "--staging, review it; 10 redirects check and delivery check --staging; 11 deploy --production; 12 delivery "
|
|
467
481
|
+ "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
482
|
"Classify. Published post: article (the default); recipe when it has an Ingredients list and a Steps list "
|
|
469
483
|
+ "(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
|
-
+ "
|
|
484
|
+
+ "page at its path, children under their parent's path. Never guess a place review: see Place reviews "
|
|
485
|
+
+ "below. Not imported, and always "
|
|
472
486
|
+ "listed: drafts, pending, private, scheduled and password-protected items; custom post types; comments; the WordPress front "
|
|
473
487
|
+ "page (Taproot seeds its own home page: ask the owner what belongs there).",
|
|
488
|
+
"Place reviews. A post with a rating and a place becomes a place review when the place can be found: run "
|
|
489
|
+
+ "'places search <name and city>', and if one prediction is plainly the same place, 'places select "
|
|
490
|
+
+ "<googlePlaceId> <sessionToken>' gives the placeId for its front matter (rating: will-return, might-return or "
|
|
491
|
+
+ "wont-return, mapped from the post's own scale). When nothing matches, import it as an article and list it. A travel "
|
|
492
|
+
+ "or food article, or an album, about one place can carry place: <placeId> in its front matter.",
|
|
474
493
|
"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
494
|
+ "page's is its post name under its parents. Keep the original publication date in displayDate (the date "
|
|
476
495
|
+ "part of wp:post_date). Categories beyond the first, and all tags, have no Taproot equivalent (tags are "
|
|
@@ -488,13 +507,14 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
488
507
|
+ "album when they are the post), links to content you did not import (keep the absolute old link), and images "
|
|
489
508
|
+ "on other hosts. Every item goes in the account with its page and what happened to it.",
|
|
490
509
|
"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
|
-
+ "
|
|
510
|
+
+ "the -300x200 thumbnail WordPress also stores), then run 'media upload' once for the whole media/ directory (at most 500 "
|
|
511
|
+
+ "files a run). It records every file in .taproot-site-media.json and reports counts and a bounded item list; "
|
|
512
|
+
+ "a run that stopped partway is safe to repeat, because identical files deduplicate.",
|
|
513
|
+
"Check before sending. 'validate' (no credential, no request) checks every page source, media reference and "
|
|
514
|
+
+ "setting in the workspace at once and lists every problem in error.problems with its file, code and field. "
|
|
515
|
+
+ "'pages push --dry-run' then checks the pages against the live site — paths taken, templates, revisions, "
|
|
516
|
+
+ "videos — and sends nothing. 'plan' checks the whole workspace against the site and orders what is left "
|
|
517
|
+
+ "(media, pages, footer, theme, navigation); 'apply --plan <planHash>' runs exactly that plan.",
|
|
498
518
|
"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
519
|
+ "manifest; a custom link is an EXTERNAL_URL item; a category or tag item has no target, so list it).",
|
|
500
520
|
"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 +588,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
568
588
|
`Pull the baseline: ${CLI_BINARY_NAME} pull writes settings/taproot-styles.json with both schemes' complete effective `
|
|
569
589
|
+ "themes (the stored theme resolved over the same defaults every consumer renders) beside brand.json, "
|
|
570
590
|
+ "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
|
|
591
|
+
+ "a theme from memory or from an example. semanticMappings holds only authored pins, listed in explicitMappingTokens "
|
|
592
|
+
+ "at the root; a context's pins need no marker. "
|
|
572
593
|
+ "site-publishing-preferences.json also carries timeZone, the site's IANA zone (UTC until the owner sets it), "
|
|
573
594
|
+ "which decides what \"today\" means for relative form date bounds; it is read-only here, so ask the owner to set "
|
|
574
595
|
+ "it in the editor under Site settings.",
|
|
@@ -579,10 +600,16 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
579
600
|
"Prepare media locally: PNG, JPEG, GIF and WebP raster files only, addressed relative to the workspace root. Make "
|
|
580
601
|
+ "the variants yourself with any image tool (a transparent logo, a @2x copy, a square favicon); there is no "
|
|
581
602
|
+ "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
|
-
+ "
|
|
603
|
+
+ "(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.",
|
|
604
|
+
`Validate offline: ${CLI_BINARY_NAME} validate checks the whole workspace at once — the complete pair, the `
|
|
605
|
+
+ "appearance files, the footer, every page, navigation and redirects — against the contracts the site enforces, "
|
|
606
|
+
+ "lists every problem in error.problems, and warns when a semanticMappings pin repeats the default on a token "
|
|
607
|
+
+ "your roles would have moved (the pin would keep the role from rendering).",
|
|
608
|
+
`Plan a combined change: when pages, the footer, the theme and navigation all changed, ${CLI_BINARY_NAME} plan `
|
|
609
|
+
+ "checks them against the live site and lists the steps in the order they must run, each ready, blocked or "
|
|
610
|
+
+ `nothing to do; ${CLI_BINARY_NAME} apply --plan <planHash> runs them through the ordinary push verbs and refuses `
|
|
611
|
+
+ "a plan the workspace or the site has moved past. After a failed step, plan again: finished steps read as "
|
|
612
|
+
+ "nothing to do.",
|
|
586
613
|
`Push: ${CLI_BINARY_NAME} theme push --dry-run first: it reads the site and lists the JSON paths at which each `
|
|
587
614
|
+ "settings file differs from it, and says whether the baseline pull recorded is still current. Then "
|
|
588
615
|
+ `${CLI_BINARY_NAME} theme push saves both themes, the appearance scalars and the ten footer scheme colours in `
|
|
@@ -829,22 +856,23 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
829
856
|
}),
|
|
830
857
|
media: Object.freeze({
|
|
831
858
|
title: "Media upload contract",
|
|
832
|
-
summary: "Upload workspace-root-relative raster files and videos
|
|
859
|
+
summary: "Upload workspace-root-relative raster files and videos; images are recorded in .taproot-site-media.json and videos report their ids.",
|
|
833
860
|
usage: `${CLI_BINARY_NAME} media upload [path...]`,
|
|
834
861
|
details: Object.freeze([
|
|
835
862
|
"Paths are relative to the configured workspace root, not the shell's current directory.",
|
|
836
863
|
"PNG, JPEG, GIF, and WebP are accepted; retina names such as logo@2x.png and logo@3x.png are supported.",
|
|
837
864
|
"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
865
|
"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
|
|
866
|
+
"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.",
|
|
867
|
+
"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
868
|
"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
869
|
"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
|
-
"
|
|
870
|
+
"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
871
|
]),
|
|
845
872
|
example: Object.freeze({
|
|
846
873
|
command: `${CLI_BINARY_NAME} media upload media/logo@2x.png`,
|
|
847
|
-
|
|
874
|
+
markdownImage: "media/logo@2x.png",
|
|
875
|
+
pmJsonImageRecord: Object.freeze({
|
|
848
876
|
imageId: "<image-uuid>",
|
|
849
877
|
src: "<preferred-delivery-url>",
|
|
850
878
|
urls: Object.freeze([Object.freeze({ minWidth: 640, url: "<responsive-delivery-url>" })]),
|
|
@@ -872,6 +900,10 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
872
900
|
+ "(staging.host_unavailable, staging.authority_denied, staging.surface_refused, or staging.handoff_unavailable) "
|
|
873
901
|
+ "and a recovery step; run staging review to mint another (it works on every surface). Never verify a theme "
|
|
874
902
|
+ "on production instead.",
|
|
903
|
+
"A preview is served on the site's staging address: its own address under Taproot's user-content domain "
|
|
904
|
+
+ "(ontaproot.io in production), or its customer staging domain once that is active. The address has to be "
|
|
905
|
+
+ "live, so a site that has never deployed to staging, or whose staging host is changing, answers "
|
|
906
|
+
+ "preview.staging_unavailable; run 'deploy --staging' once, or wait for the change to finish.",
|
|
875
907
|
"A page path resolves through .taproot-site-manifest.json; a canonical page UUID works directly.",
|
|
876
908
|
"The homepage's manifest path is empty; address it as '/', which resolves to that empty root path.",
|
|
877
909
|
"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 +921,7 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
889
921
|
summary:
|
|
890
922
|
`${FIXTURE_MANIFEST_FILE_NAME} binds a directory laid out like a pulled workspace to deterministic identities, `
|
|
891
923
|
+ "so validate can prove it with no credential, no network, and no write.",
|
|
892
|
-
usage: `${CLI_BINARY_NAME} validate <fixture-directory
|
|
924
|
+
usage: `${CLI_BINARY_NAME} validate [<fixture-directory>]`,
|
|
893
925
|
details: Object.freeze([
|
|
894
926
|
`Required root fields: ${FIXTURE_REQUIRED_ROOT_FIELDS.join(", ")}.`,
|
|
895
927
|
`Optional root fields: ${FIXTURE_OPTIONAL_ROOT_FIELDS.join(", ")}. Appearance and footer metadata, when present, `
|
|
@@ -928,6 +960,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
928
960
|
}.`,
|
|
929
961
|
`The fixture block carries ${FIXTURE_METADATA_FIELDS.join(", ")}. Every image a page references by imageId must `
|
|
930
962
|
+ "be listed in fixture.imageIds, and every absolute delivery URL a page uses must sit on a declared origin. "
|
|
963
|
+
+ "A Markdown media path resolves through the fixture's own .taproot-site-media.json, as in a workspace; "
|
|
964
|
+
+ "without one, no media path resolves. "
|
|
931
965
|
+ "videoIds is optional: list the ready videos a page may place with a video component, and validate refuses a "
|
|
932
966
|
+ "video component whose videoId is not listed. A live push makes the same check against the site's video library.",
|
|
933
967
|
`Each deliveryOrigins entry is an origin-only HTTPS URL (no path, query, fragment, or credentials) on `
|
|
@@ -1011,11 +1045,15 @@ export function listComponentTypeReferences() {
|
|
|
1011
1045
|
return COMPONENT_TYPES.map(componentSummary);
|
|
1012
1046
|
}
|
|
1013
1047
|
|
|
1048
|
+
const EXAMPLE_MEDIA_PATH = "media/photo.webp";
|
|
1049
|
+
|
|
1014
1050
|
export function getComponentReference(type) {
|
|
1015
1051
|
const definition = getComponentDefinition(type);
|
|
1016
1052
|
if (!definition) return undefined;
|
|
1017
1053
|
const example = definition.example;
|
|
1018
|
-
|
|
1054
|
+
// Markdown names each image by its media path; pages push resolves it (TR01187).
|
|
1055
|
+
const markdownData = mapComponentImages(type, example, (value) => (value === null ? null : EXAMPLE_MEDIA_PATH));
|
|
1056
|
+
const markdownExample = `\`\`\`component:${type}\n${JSON.stringify(markdownData, null, 2)}\n\`\`\``;
|
|
1019
1057
|
return {
|
|
1020
1058
|
type,
|
|
1021
1059
|
displayName: definition.displayName,
|
|
@@ -1150,12 +1188,14 @@ function backgroundReference() {
|
|
|
1150
1188
|
}),
|
|
1151
1189
|
scrimOpacityByValue: Object.freeze({ ...BACKGROUND_DEFINITION.fields.scrimStrength.opacityByValue }),
|
|
1152
1190
|
deliveryUrlPolicy:
|
|
1153
|
-
"
|
|
1191
|
+
"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
1192
|
compactBehavior:
|
|
1155
1193
|
"At the shared compact breakpoint, portraitImage and portraitFocus replace the landscape image and focus when supplied.",
|
|
1156
1194
|
example: BACKGROUND_EXAMPLE,
|
|
1157
1195
|
markdownExample: `:::section ${
|
|
1158
|
-
JSON.stringify({
|
|
1196
|
+
JSON.stringify({
|
|
1197
|
+
background: { ...BACKGROUND_EXAMPLE, image: "media/class.webp", portraitImage: "media/class-portrait.webp" },
|
|
1198
|
+
})
|
|
1159
1199
|
}\n## Practice with us\n\nFind your next class.\n:::`,
|
|
1160
1200
|
});
|
|
1161
1201
|
}
|
|
@@ -1185,11 +1225,11 @@ function decorationReference() {
|
|
|
1185
1225
|
additionalProperties: false,
|
|
1186
1226
|
fields: sectionAttributeReference().find((attr) => attr.name === "decoration").fields,
|
|
1187
1227
|
deliveryUrlPolicy:
|
|
1188
|
-
"
|
|
1228
|
+
"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
1229
|
tintTokens: Object.freeze({ ...DECORATION_DEFINITION.fields.tint.tokenByValue }),
|
|
1190
1230
|
example: DECORATION_EXAMPLE,
|
|
1191
1231
|
markdownExample: `:::section ${
|
|
1192
|
-
JSON.stringify({ decoration: DECORATION_EXAMPLE })
|
|
1232
|
+
JSON.stringify({ decoration: { ...DECORATION_EXAMPLE, image: "media/starburst.webp" } })
|
|
1193
1233
|
}\n## Rooted in warmth\n\nMove with confidence.\n:::`,
|
|
1194
1234
|
maskGuidance:
|
|
1195
1235
|
"Use a transparent PNG or WebP whose alpha channel contains only the mark. The active section context supplies the tint in both schemes.",
|
|
@@ -1556,6 +1596,25 @@ const FREE_FORM_REFERENCE = Object.freeze({
|
|
|
1556
1596
|
root: Object.freeze({ type: "doc", content: "array of supported block nodes" }),
|
|
1557
1597
|
nodes: NODE_TYPES,
|
|
1558
1598
|
marks: MARK_TYPES,
|
|
1599
|
+
// TR01190: what a link mark's rel and target publish as.
|
|
1600
|
+
links: Object.freeze({
|
|
1601
|
+
markdown: '[Shop](https://shop.example){rel="sponsored nofollow" target="_blank"}',
|
|
1602
|
+
rel: LINK_REL_TOKENS,
|
|
1603
|
+
target: LINK_TARGETS,
|
|
1604
|
+
default:
|
|
1605
|
+
"A link with no rel is followed, whether it goes to this site or another. Choose nofollow, sponsored "
|
|
1606
|
+
+ "(paid or affiliate), or ugc (written by someone else) for a link that should not pass endorsement; the "
|
|
1607
|
+
+ "choice is per link, internal links included.",
|
|
1608
|
+
markdownRule:
|
|
1609
|
+
'Write {rel="…" target="…"} directly after the link\'s closing parenthesis: either attribute alone or both, '
|
|
1610
|
+
+ 'each double-quoted once. rel is space-separated; target="_blank" opens a new tab.',
|
|
1611
|
+
pmJson:
|
|
1612
|
+
'In .pm.json, a link mark carries attrs.rel (a space-separated string, or null) and attrs.target ("_blank" '
|
|
1613
|
+
+ 'opens a new tab and publishes with noopener noreferrer added, "_self" or null keeps the same tab).',
|
|
1614
|
+
refusals:
|
|
1615
|
+
`Any other rel token, opener included, is refused (${CONTENT_ERROR_CODES.linkRel}); so is another target `
|
|
1616
|
+
+ `(${CONTENT_ERROR_CODES.linkTarget}). The editor's old default, "noopener noreferrer nofollow", reads as no choice.`,
|
|
1617
|
+
}),
|
|
1559
1618
|
componentNode: Object.freeze({
|
|
1560
1619
|
type: "componentBlock",
|
|
1561
1620
|
attrs: Object.freeze({
|
|
@@ -1665,7 +1724,8 @@ function schemaConstraints(schema) {
|
|
|
1665
1724
|
|
|
1666
1725
|
function formatSchemaLine(name, schema, required, indent) {
|
|
1667
1726
|
const constraints = schemaConstraints(schema);
|
|
1668
|
-
|
|
1727
|
+
const label = schema.markdownType ? "media path | null in Markdown; object | null in .pm.json" : schemaLabel(schema);
|
|
1728
|
+
return `${indent}${name.padEnd(Math.max(1, 28 - indent.length))} ${label}; ${
|
|
1669
1729
|
required ? "required" : "optional"
|
|
1670
1730
|
}${constraints.length > 0 ? `; ${constraints.join("; ")}` : ""}`;
|
|
1671
1731
|
}
|
|
@@ -1675,7 +1735,7 @@ function nestedSchemaLines(schema, path, indent = " ") {
|
|
|
1675
1735
|
const valuePath = schema.type === "array" ? `${path}[]` : path;
|
|
1676
1736
|
if (!valueSchema?.properties) return [];
|
|
1677
1737
|
|
|
1678
|
-
const lines = [];
|
|
1738
|
+
const lines = valueSchema.markdownType ? [`${indent}${valuePath} in a .pm.json document:`] : [];
|
|
1679
1739
|
for (const property of valueSchema.properties) {
|
|
1680
1740
|
const propertyPath = `${valuePath}.${property.name}`;
|
|
1681
1741
|
lines.push(formatSchemaLine(propertyPath, property.schema, property.required, indent));
|
|
@@ -1719,9 +1779,11 @@ function formatSectionAttr(attr) {
|
|
|
1719
1779
|
|
|
1720
1780
|
function formatRegistryField(field, indent = " ") {
|
|
1721
1781
|
if (field.type === "processed-image") {
|
|
1722
|
-
return `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))}
|
|
1782
|
+
return `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))} media path${
|
|
1783
|
+
field.nullable ? " | null" : ""
|
|
1784
|
+
} in Markdown; processed-image${
|
|
1723
1785
|
field.nullable ? " | null" : ""
|
|
1724
|
-
}; ${field.required ? "required" : `default ${JSON.stringify(field.default)}`}; required keys ${
|
|
1786
|
+
} in .pm.json; ${field.required ? "required" : `default ${JSON.stringify(field.default)}`}; .pm.json required keys ${
|
|
1725
1787
|
field.requiredKeys.join(", ")
|
|
1726
1788
|
}; optional keys ${
|
|
1727
1789
|
field.optionalKeys.join(", ")
|
|
@@ -1761,6 +1823,8 @@ function formatDesignRecipe(recipe) {
|
|
|
1761
1823
|
|
|
1762
1824
|
export function formatReferenceResult(result) {
|
|
1763
1825
|
switch (result.topic) {
|
|
1826
|
+
case "fonts":
|
|
1827
|
+
return formatFontCatalog(result.fonts);
|
|
1764
1828
|
case "topics":
|
|
1765
1829
|
return `Usage: ${CLI_BINARY_NAME} help <topic> [name] [--json]\n\nReference topics:\n${
|
|
1766
1830
|
result.topics.map((topic) => ` ${topic.usage.padEnd(48)} ${topic.summary}`).join("\n")
|
|
@@ -1808,7 +1872,9 @@ export function formatReferenceResult(result) {
|
|
|
1808
1872
|
page.document.nodes.join(", ")
|
|
1809
1873
|
}\nSupported marks: ${
|
|
1810
1874
|
page.document.marks.join(", ")
|
|
1811
|
-
}\n\
|
|
1875
|
+
}\n\nLinks:\n markdown ${page.document.links.markdown}\n rel ${
|
|
1876
|
+
page.document.links.rel.join(", ")
|
|
1877
|
+
}\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
1878
|
page.document.sectionNode.attrs.map(formatSectionAttr).join("\n")
|
|
1813
1879
|
}\n\nSection photo background (closed object):\n${
|
|
1814
1880
|
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,
|