@taprootio/site-authoring 0.1.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/LICENSE +15 -0
- package/README.md +239 -0
- package/bin/taproot-site.js +5 -0
- package/package.json +40 -0
- package/src/api.js +1507 -0
- package/src/appearance-contract.js +311 -0
- package/src/atomic-file.js +136 -0
- package/src/bounded-file.js +59 -0
- package/src/capabilities.js +29 -0
- package/src/cli.js +872 -0
- package/src/config.js +412 -0
- package/src/constants.js +173 -0
- package/src/content/components.js +870 -0
- package/src/content/free-form-section-registry.json +524 -0
- package/src/content/free-form-sections.js +582 -0
- package/src/content/index.js +29 -0
- package/src/content/inline-facts.js +97 -0
- package/src/content/markdown.js +1625 -0
- package/src/content/tiptap-prosemirror.ts +1266 -0
- package/src/content/validate-document.js +1079 -0
- package/src/content/vocabulary.js +397 -0
- package/src/credentials.js +544 -0
- package/src/errors.js +133 -0
- package/src/footer-contract.js +687 -0
- package/src/footer-draft-hash.js +203 -0
- package/src/footer-workspace.js +136 -0
- package/src/image-metadata.js +169 -0
- package/src/index.js +16 -0
- package/src/json-guard.js +111 -0
- package/src/output.js +157 -0
- package/src/presentation-reference.js +600 -0
- package/src/reference-help.js +927 -0
- package/src/session.js +316 -0
- package/src/settings-catalog.js +187 -0
- package/src/settings.js +157 -0
- package/src/theme-validation.js +344 -0
- package/src/transport.js +777 -0
- package/src/verbs/approve.js +178 -0
- package/src/verbs/deploy.js +327 -0
- package/src/verbs/env.js +89 -0
- package/src/verbs/footer-push.js +115 -0
- package/src/verbs/index.js +125 -0
- package/src/verbs/login.js +529 -0
- package/src/verbs/logout.js +39 -0
- package/src/verbs/media-upload.js +319 -0
- package/src/verbs/nav-push.js +333 -0
- package/src/verbs/pages-push.js +990 -0
- package/src/verbs/preview-page.js +107 -0
- package/src/verbs/preview-revoke.js +56 -0
- package/src/verbs/pull.js +727 -0
- package/src/verbs/sites.js +57 -0
- package/src/verbs/status.js +94 -0
- package/src/verbs/theme-push.js +197 -0
- package/src/verbs/use.js +129 -0
- package/src/verbs/validate.js +477 -0
- package/src/verbs/whoami.js +108 -0
- package/src/workspace.js +1085 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
import { CLI_BINARY_NAME } from "./constants.js";
|
|
2
|
+
import {
|
|
3
|
+
canonicalizeComponentData,
|
|
4
|
+
COMPONENT_TYPES,
|
|
5
|
+
getComponentDefinition,
|
|
6
|
+
getComponentPropertyReference,
|
|
7
|
+
} from "./content/components.js";
|
|
8
|
+
import { FREE_FORM_SECTION_REGISTRY } from "./content/free-form-sections.js";
|
|
9
|
+
import {
|
|
10
|
+
CONTENT_ERROR_CODES,
|
|
11
|
+
CONTENT_LIMITS,
|
|
12
|
+
MARK_TYPES,
|
|
13
|
+
NODE_TYPES,
|
|
14
|
+
} from "./content/vocabulary.js";
|
|
15
|
+
import {
|
|
16
|
+
formatPresentationReference,
|
|
17
|
+
getAppearanceReference,
|
|
18
|
+
getFooterReference,
|
|
19
|
+
getThemeReference,
|
|
20
|
+
} from "./presentation-reference.js";
|
|
21
|
+
import {
|
|
22
|
+
INTERNAL_PAGE_BASELINE_DIRECTORY,
|
|
23
|
+
PAGE_READ_ONLY_REASON_SYSTEM_404,
|
|
24
|
+
PAGE_WORKSPACE_MODE_READ_ONLY,
|
|
25
|
+
SYSTEM_PAGE_NOT_FOUND_PATH,
|
|
26
|
+
} from "./workspace.js";
|
|
27
|
+
|
|
28
|
+
export { getAppearanceReference, getFooterReference, getThemeReference };
|
|
29
|
+
|
|
30
|
+
export const REFERENCE_VERSION = 13;
|
|
31
|
+
export const PAGE_TYPES = Object.freeze(["free-form"]);
|
|
32
|
+
export const REFERENCE_TOPICS = Object.freeze([
|
|
33
|
+
Object.freeze({ name: "pages", usage: `${CLI_BINARY_NAME} help pages`, summary: "List authorable page types." }),
|
|
34
|
+
Object.freeze({
|
|
35
|
+
name: "page",
|
|
36
|
+
usage: `${CLI_BINARY_NAME} help page <page-type>`,
|
|
37
|
+
summary: "Describe one page type.",
|
|
38
|
+
}),
|
|
39
|
+
Object.freeze({
|
|
40
|
+
name: "components",
|
|
41
|
+
usage: `${CLI_BINARY_NAME} help components`,
|
|
42
|
+
summary: "List free-form components.",
|
|
43
|
+
}),
|
|
44
|
+
Object.freeze({
|
|
45
|
+
name: "component",
|
|
46
|
+
usage: `${CLI_BINARY_NAME} help component <component-type>`,
|
|
47
|
+
summary: "Describe one component schema and show a valid example.",
|
|
48
|
+
}),
|
|
49
|
+
Object.freeze({ name: "nav", usage: `${CLI_BINARY_NAME} help nav`, summary: "Describe nav.json item shapes." }),
|
|
50
|
+
Object.freeze({
|
|
51
|
+
name: "media",
|
|
52
|
+
usage: `${CLI_BINARY_NAME} help media`,
|
|
53
|
+
summary: "Describe media selection, naming, and component-ready output.",
|
|
54
|
+
}),
|
|
55
|
+
Object.freeze({
|
|
56
|
+
name: "preview",
|
|
57
|
+
usage: `${CLI_BINARY_NAME} help preview`,
|
|
58
|
+
summary: "Describe authoring preview creation, waiting, and recovery.",
|
|
59
|
+
}),
|
|
60
|
+
Object.freeze({
|
|
61
|
+
name: "theme",
|
|
62
|
+
usage: `${CLI_BINARY_NAME} help theme`,
|
|
63
|
+
summary: "Design and validate a complete Espalier light/dark theme pair.",
|
|
64
|
+
}),
|
|
65
|
+
Object.freeze({
|
|
66
|
+
name: "appearance",
|
|
67
|
+
usage: `${CLI_BINARY_NAME} help appearance`,
|
|
68
|
+
summary: "Map site presentation decisions to theme-push workspace fields.",
|
|
69
|
+
}),
|
|
70
|
+
Object.freeze({
|
|
71
|
+
name: "footer",
|
|
72
|
+
usage: `${CLI_BINARY_NAME} help footer`,
|
|
73
|
+
summary: "Author and concurrency-safely push the complete footer document.",
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const WORKFLOW_REFERENCES = Object.freeze({
|
|
78
|
+
nav: Object.freeze({
|
|
79
|
+
title: "Navigation workspace contract",
|
|
80
|
+
summary: "nav push replaces the complete tree in nav.json; item IDs are author-minted canonical lowercase UUIDs.",
|
|
81
|
+
usage: `${CLI_BINARY_NAME} nav push`,
|
|
82
|
+
details: Object.freeze([
|
|
83
|
+
"nav.json is { siteId, navItems }, not a bare array.",
|
|
84
|
+
"kind is NAV_ITEM_KIND_PAGE, NAV_ITEM_KIND_EXTERNAL_URL, or NAV_ITEM_KIND_GROUP_HEADER.",
|
|
85
|
+
"PAGE uses resourceId from .taproot-site-manifest.json, never pageId or path.",
|
|
86
|
+
"EXTERNAL_URL uses externalUrl; GROUP_HEADER has no target; every kind may carry children.",
|
|
87
|
+
]),
|
|
88
|
+
example: Object.freeze({
|
|
89
|
+
siteId: "<site-uuid>",
|
|
90
|
+
navItems: Object.freeze([Object.freeze({
|
|
91
|
+
id: "<author-minted-lowercase-uuid>",
|
|
92
|
+
kind: "NAV_ITEM_KIND_PAGE",
|
|
93
|
+
title: "Classes",
|
|
94
|
+
resourceId: "<resourceId-from-manifest>",
|
|
95
|
+
children: Object.freeze([]),
|
|
96
|
+
})]),
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
media: Object.freeze({
|
|
100
|
+
title: "Media upload contract",
|
|
101
|
+
summary: "Upload workspace-root-relative raster files and receive component-ready media objects.",
|
|
102
|
+
usage: `${CLI_BINARY_NAME} media upload [path...]`,
|
|
103
|
+
details: Object.freeze([
|
|
104
|
+
"Paths are relative to the configured workspace root, not the shell's current directory.",
|
|
105
|
+
"PNG, JPEG, GIF, and WebP are accepted; retina names such as logo@2x.png and logo@3x.png are supported.",
|
|
106
|
+
"Each result item includes media: { imageId, src, urls, width, height, alt }.",
|
|
107
|
+
"The same src/urls delivery fields are saved in .taproot-site-media.json for page and component authoring.",
|
|
108
|
+
]),
|
|
109
|
+
example: Object.freeze({
|
|
110
|
+
command: `${CLI_BINARY_NAME} media upload media/logo@2x.png`,
|
|
111
|
+
componentMedia: Object.freeze({
|
|
112
|
+
imageId: "<image-uuid>",
|
|
113
|
+
src: "<preferred-delivery-url>",
|
|
114
|
+
urls: Object.freeze([Object.freeze({ minWidth: 640, url: "<responsive-delivery-url>" })]),
|
|
115
|
+
width: 1200,
|
|
116
|
+
height: 800,
|
|
117
|
+
alt: "",
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
}),
|
|
121
|
+
preview: Object.freeze({
|
|
122
|
+
title: "Authoring preview workflow",
|
|
123
|
+
summary: "Render one persisted draft behind the normal staging gate.",
|
|
124
|
+
usage: `${CLI_BINARY_NAME} preview page <page-path-or-id>`,
|
|
125
|
+
details: Object.freeze([
|
|
126
|
+
"A page path resolves through .taproot-site-manifest.json; a canonical page UUID works directly.",
|
|
127
|
+
"The homepage's manifest path is empty; address it as '/', which resolves to that empty root path.",
|
|
128
|
+
"Creation prints pageId, snapshotId, and expiresAt before the bounded render wait starts.",
|
|
129
|
+
"The default stored-preview cap is 10 per authoring key and site; operators can tune it.",
|
|
130
|
+
"Success returns READY with url, snapshotId, expiresAt, storedPreviewCap, storedPreviewCount, and evictedPreviews.",
|
|
131
|
+
"The url is a single-use handoff that expires two minutes after it is minted: opening it consumes it, and a reused, shared, or bookmarked URL answers Not found. Run preview page again for another.",
|
|
132
|
+
"At the configured cap, creation revokes the oldest snapshot for the same page first, then the oldest snapshot held by the same key.",
|
|
133
|
+
`A stalled preview can be released with ${CLI_BINARY_NAME} preview revoke <page-id> <snapshot-id>.`,
|
|
134
|
+
]),
|
|
135
|
+
example: Object.freeze({ command: `${CLI_BINARY_NAME} preview page classes` }),
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export function getWorkflowReference(topic) {
|
|
140
|
+
return WORKFLOW_REFERENCES[topic];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function componentSummary(type) {
|
|
144
|
+
const definition = getComponentDefinition(type);
|
|
145
|
+
return {
|
|
146
|
+
type,
|
|
147
|
+
displayName: definition.displayName,
|
|
148
|
+
summary: definition.summary,
|
|
149
|
+
helpCommand: `${CLI_BINARY_NAME} help component ${type}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function listComponentTypeReferences() {
|
|
154
|
+
return COMPONENT_TYPES.map(componentSummary);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getComponentReference(type) {
|
|
158
|
+
const definition = getComponentDefinition(type);
|
|
159
|
+
if (!definition) return undefined;
|
|
160
|
+
const example = definition.example;
|
|
161
|
+
const markdownExample = `\`\`\`component:${type}\n${JSON.stringify(example, null, 2)}\n\`\`\``;
|
|
162
|
+
return {
|
|
163
|
+
type,
|
|
164
|
+
displayName: definition.displayName,
|
|
165
|
+
summary: definition.summary,
|
|
166
|
+
dataEncoding:
|
|
167
|
+
"componentData is a JSON string containing an object; do not place an object directly in the attribute.",
|
|
168
|
+
componentDataMaxUtf8Bytes: CONTENT_LIMITS.componentDataBytes,
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
properties: getComponentPropertyReference(type),
|
|
171
|
+
editorInitialData: definition.defaultData,
|
|
172
|
+
accessibility: definition.accessibility,
|
|
173
|
+
example,
|
|
174
|
+
markdownExample,
|
|
175
|
+
componentBlockExample: {
|
|
176
|
+
type: "componentBlock",
|
|
177
|
+
attrs: {
|
|
178
|
+
componentType: type,
|
|
179
|
+
componentData: canonicalizeComponentData(type, example),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function registryFieldReference(name, definition) {
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
name,
|
|
188
|
+
type: definition.type,
|
|
189
|
+
required: definition.required,
|
|
190
|
+
...(Object.hasOwn(definition, "default") ? { default: definition.default } : {}),
|
|
191
|
+
...(definition.nullable === true ? { nullable: true } : {}),
|
|
192
|
+
...(definition.values ? { values: [...definition.values] } : {}),
|
|
193
|
+
...(definition.pattern ? { pattern: definition.pattern } : {}),
|
|
194
|
+
...(definition.maximumLength ? { maximumLength: definition.maximumLength } : {}),
|
|
195
|
+
...(definition.minimum !== undefined ? { minimum: definition.minimum } : {}),
|
|
196
|
+
...(definition.maximum !== undefined ? { maximum: definition.maximum } : {}),
|
|
197
|
+
...(definition.minItems !== undefined ? { minItems: definition.minItems } : {}),
|
|
198
|
+
...(definition.maxItems !== undefined ? { maxItems: definition.maxItems } : {}),
|
|
199
|
+
...(definition.minScalars !== undefined ? { minScalars: definition.minScalars } : {}),
|
|
200
|
+
...(definition.maxScalars !== undefined ? { maxScalars: definition.maxScalars } : {}),
|
|
201
|
+
...(definition.nonWhitespace === true ? { nonWhitespace: true } : {}),
|
|
202
|
+
...(definition.schemes ? { schemes: [...definition.schemes] } : {}),
|
|
203
|
+
...(definition.closed === true ? { additionalProperties: false } : {}),
|
|
204
|
+
...(definition.closedItems === true ? { itemAdditionalProperties: false } : {}),
|
|
205
|
+
...(definition.opacityByValue ? { opacityByValue: { ...definition.opacityByValue } } : {}),
|
|
206
|
+
...(definition.urls
|
|
207
|
+
? {
|
|
208
|
+
responsiveUrls: Object.freeze({
|
|
209
|
+
minItems: definition.urls.minItems,
|
|
210
|
+
maxItems: definition.urls.maxItems,
|
|
211
|
+
itemAdditionalProperties: !definition.urls.closedItems,
|
|
212
|
+
fields: Object.freeze(Object.entries(definition.urls.fields).map(([fieldName, field]) =>
|
|
213
|
+
registryFieldReference(fieldName, field)
|
|
214
|
+
)),
|
|
215
|
+
}),
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
...(definition.srcMustMatchUrls === true ? { srcMustMatchUrls: true } : {}),
|
|
219
|
+
...(definition.fields
|
|
220
|
+
? {
|
|
221
|
+
fields: Object.freeze(Object.entries(definition.fields).map(([fieldName, field]) => Object.freeze({
|
|
222
|
+
...registryFieldReference(fieldName, field),
|
|
223
|
+
...(field.requiredKeys ? { requiredKeys: [...field.requiredKeys] } : {}),
|
|
224
|
+
...(field.optionalKeys ? { optionalKeys: [...field.optionalKeys] } : {}),
|
|
225
|
+
...(field.tokenByValue ? { tokenByValue: { ...field.tokenByValue } } : {}),
|
|
226
|
+
}))),
|
|
227
|
+
}
|
|
228
|
+
: {}),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function sectionAttributeReference() {
|
|
233
|
+
return Object.entries(FREE_FORM_SECTION_REGISTRY.section.attrs).map(([name, definition]) => Object.freeze({
|
|
234
|
+
...registryFieldReference(name, definition),
|
|
235
|
+
label: definition.label,
|
|
236
|
+
help: definition.help,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const SECTION_IMAGE_EXAMPLE = Object.freeze({
|
|
241
|
+
imageId: "3f1c2b4a-5d6e-4f70-8a91-b2c3d4e5f607",
|
|
242
|
+
src: "https://static.example.test/site/img/riverbend-class-1280.webp",
|
|
243
|
+
urls: Object.freeze([
|
|
244
|
+
Object.freeze({ minWidth: 640, url: "https://static.example.test/site/img/riverbend-class-640.webp" }),
|
|
245
|
+
Object.freeze({ minWidth: 1280, url: "https://static.example.test/site/img/riverbend-class-1280.webp" }),
|
|
246
|
+
]),
|
|
247
|
+
width: 1920,
|
|
248
|
+
height: 1080,
|
|
249
|
+
alt: "A welcoming hot yoga class at Riverbend Wellness",
|
|
250
|
+
});
|
|
251
|
+
const SECTION_PORTRAIT_IMAGE_EXAMPLE = Object.freeze({
|
|
252
|
+
imageId: "9a8b7c6d-5e4f-4321-a098-76543210fedc",
|
|
253
|
+
src: "https://static.example.test/site/img/riverbend-class-portrait-480.webp",
|
|
254
|
+
urls: Object.freeze([
|
|
255
|
+
Object.freeze({ minWidth: 480, url: "https://static.example.test/site/img/riverbend-class-portrait-480.webp" }),
|
|
256
|
+
]),
|
|
257
|
+
width: 900,
|
|
258
|
+
height: 1200,
|
|
259
|
+
alt: "A welcoming hot yoga class at Riverbend Wellness",
|
|
260
|
+
});
|
|
261
|
+
const BACKGROUND_DEFINITION = FREE_FORM_SECTION_REGISTRY.section.attrs.background;
|
|
262
|
+
const BACKGROUND_EXAMPLE = Object.freeze({
|
|
263
|
+
image: SECTION_IMAGE_EXAMPLE,
|
|
264
|
+
portraitImage: SECTION_PORTRAIT_IMAGE_EXAMPLE,
|
|
265
|
+
focus: Object.freeze({ x: 0.56, y: 0.42 }),
|
|
266
|
+
portraitFocus: Object.freeze({ x: 0.5, y: 0.32 }),
|
|
267
|
+
scrimStrength: "medium",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
function backgroundReference() {
|
|
271
|
+
return Object.freeze({
|
|
272
|
+
additionalProperties: false,
|
|
273
|
+
fields: sectionAttributeReference().find((attr) => attr.name === "background").fields,
|
|
274
|
+
defaults: Object.freeze({
|
|
275
|
+
portraitImage: BACKGROUND_DEFINITION.fields.portraitImage.default,
|
|
276
|
+
focus: Object.freeze({ ...BACKGROUND_DEFINITION.fields.focus.default }),
|
|
277
|
+
portraitFocus: BACKGROUND_DEFINITION.fields.portraitFocus.default,
|
|
278
|
+
scrimStrength: BACKGROUND_DEFINITION.fields.scrimStrength.default,
|
|
279
|
+
}),
|
|
280
|
+
scrimOpacityByValue: Object.freeze({ ...BACKGROUND_DEFINITION.fields.scrimStrength.opacityByValue }),
|
|
281
|
+
deliveryUrlPolicy:
|
|
282
|
+
"Use complete site-owned processed-image results for image and portraitImage. src and every urls[].url must be HTTPS or non-protocol-relative root-relative delivery URLs; credentials, whitespace, controls, and backslashes are rejected.",
|
|
283
|
+
compactBehavior:
|
|
284
|
+
"At the shared compact breakpoint, portraitImage and portraitFocus replace the landscape image and focus when supplied.",
|
|
285
|
+
example: BACKGROUND_EXAMPLE,
|
|
286
|
+
markdownExample: `:::section ${JSON.stringify({ background: BACKGROUND_EXAMPLE })}\n## Practice with us\n\nFind your next class.\n:::`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const DECORATION_DEFINITION = FREE_FORM_SECTION_REGISTRY.section.attrs.decoration;
|
|
291
|
+
const DECORATION_EXAMPLE = Object.freeze({
|
|
292
|
+
image: Object.freeze({
|
|
293
|
+
imageId: "3f1c2b4a-5d6e-4f70-8a91-b2c3d4e5f607",
|
|
294
|
+
src: "https://static.example.test/site/img/starburst-640.webp",
|
|
295
|
+
urls: Object.freeze([
|
|
296
|
+
Object.freeze({ minWidth: 640, url: "https://static.example.test/site/img/starburst-640.webp" }),
|
|
297
|
+
]),
|
|
298
|
+
width: 1200,
|
|
299
|
+
height: 1200,
|
|
300
|
+
alt: "",
|
|
301
|
+
}),
|
|
302
|
+
anchor: "top-end",
|
|
303
|
+
inlineOffsetPercent: 18,
|
|
304
|
+
blockOffsetPercent: -12,
|
|
305
|
+
inlineSizePercent: 62,
|
|
306
|
+
opacity: 0.09,
|
|
307
|
+
tint: "heading",
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
function decorationReference() {
|
|
311
|
+
return Object.freeze({
|
|
312
|
+
additionalProperties: false,
|
|
313
|
+
fields: sectionAttributeReference().find((attr) => attr.name === "decoration").fields,
|
|
314
|
+
deliveryUrlPolicy:
|
|
315
|
+
"Use the complete site-owned processed-image result from media upload. src and every urls[].url must be HTTPS or non-protocol-relative root-relative delivery URLs; credentials, whitespace, controls, and backslashes are rejected.",
|
|
316
|
+
tintTokens: Object.freeze({ ...DECORATION_DEFINITION.fields.tint.tokenByValue }),
|
|
317
|
+
example: DECORATION_EXAMPLE,
|
|
318
|
+
markdownExample: `:::section ${JSON.stringify({ decoration: DECORATION_EXAMPLE })}\n## Rooted in warmth\n\nMove with confidence.\n:::`,
|
|
319
|
+
maskGuidance:
|
|
320
|
+
"Use a transparent PNG or WebP whose alpha channel contains only the mark. The active section context supplies the tint in both schemes.",
|
|
321
|
+
opaqueWarning:
|
|
322
|
+
"An opaque rectangle uses its whole alpha channel and will render as a tinted rectangle, not as a cut-out mark.",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const INLINE_FACTS_DEFINITION = FREE_FORM_SECTION_REGISTRY.inlineFacts;
|
|
327
|
+
const INLINE_FACT_ITEMS_EXAMPLE = Object.freeze([
|
|
328
|
+
Object.freeze({ value: "4.9 ★", label: "Community rating" }),
|
|
329
|
+
Object.freeze({
|
|
330
|
+
value: "100 Riverside Way, Suite A",
|
|
331
|
+
label: "Elm Harbor",
|
|
332
|
+
url: "https://maps.example.test/?q=100%20Riverside%20Way%20Elm%20Harbor",
|
|
333
|
+
}),
|
|
334
|
+
Object.freeze({ value: "(555) 013-7788", label: "Call the studio", url: "tel:+15550137788" }),
|
|
335
|
+
Object.freeze({ value: "Open today until 8:30 PM", label: "Today's hours", url: "/classes" }),
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
function inlineFactsReference() {
|
|
339
|
+
const items = INLINE_FACTS_DEFINITION.attrs.items;
|
|
340
|
+
const proseMirror = Object.freeze({
|
|
341
|
+
type: INLINE_FACTS_DEFINITION.nodeType,
|
|
342
|
+
attrs: Object.freeze({ items: INLINE_FACT_ITEMS_EXAMPLE }),
|
|
343
|
+
});
|
|
344
|
+
return Object.freeze({
|
|
345
|
+
nodeType: INLINE_FACTS_DEFINITION.nodeType,
|
|
346
|
+
label: INLINE_FACTS_DEFINITION.label,
|
|
347
|
+
help: INLINE_FACTS_DEFINITION.help,
|
|
348
|
+
additionalProperties: false,
|
|
349
|
+
attrs: Object.freeze({
|
|
350
|
+
items: Object.freeze({
|
|
351
|
+
...registryFieldReference("items", items),
|
|
352
|
+
itemFieldOrder: Object.freeze(Object.keys(items.fields)),
|
|
353
|
+
}),
|
|
354
|
+
}),
|
|
355
|
+
placement: Object.freeze({ ...INLINE_FACTS_DEFINITION.placement }),
|
|
356
|
+
urlPolicy:
|
|
357
|
+
"Optional safe HTTP(S), mailto, tel, non-protocol-relative root, fragment, query, or relative URL. Empty/whitespace-only values, other schemes, backslashes, and ASCII controls are rejected; tel: remains a native link.",
|
|
358
|
+
markdown: Object.freeze({
|
|
359
|
+
fence: INLINE_FACTS_DEFINITION.markdown.fence,
|
|
360
|
+
body: INLINE_FACTS_DEFINITION.markdown.body,
|
|
361
|
+
example: `\`\`\`${INLINE_FACTS_DEFINITION.markdown.fence}\n${JSON.stringify(INLINE_FACT_ITEMS_EXAMPLE, null, 2)}\n\`\`\``,
|
|
362
|
+
}),
|
|
363
|
+
examples: Object.freeze({ items: INLINE_FACT_ITEMS_EXAMPLE, proseMirror }),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function placementMatrix() {
|
|
368
|
+
const nodes = Object.entries(FREE_FORM_SECTION_REGISTRY.rootNodes).map(([type, definition]) => {
|
|
369
|
+
const explicit = definition.placement === "explicit-band";
|
|
370
|
+
return Object.freeze({
|
|
371
|
+
kind: "node",
|
|
372
|
+
type,
|
|
373
|
+
label: definition.label,
|
|
374
|
+
rootPlacement: definition.placement,
|
|
375
|
+
fullBleed: explicit,
|
|
376
|
+
contentPadding: explicit ? "standard by default; section may choose none" : "implicit section: standard",
|
|
377
|
+
well: explicit ? "shared site well by default; none removes the cap" : "shared site well",
|
|
378
|
+
measure: type === FREE_FORM_SECTION_REGISTRY.section.nodeType ? "child-defined" : definition.measure,
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
const components = Object.entries(FREE_FORM_SECTION_REGISTRY.components).map(([type, definition]) => {
|
|
382
|
+
const rootBand = definition.rootPlacement === "root-band";
|
|
383
|
+
return Object.freeze({
|
|
384
|
+
kind: "component",
|
|
385
|
+
type,
|
|
386
|
+
label: definition.label,
|
|
387
|
+
rootPlacement: definition.rootPlacement,
|
|
388
|
+
fullBleed: rootBand,
|
|
389
|
+
contentPadding: rootBand ? "component-owned" : "implicit section: standard",
|
|
390
|
+
well: rootBand ? "none at root" : "shared site well",
|
|
391
|
+
measure: definition.measure,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
return Object.freeze([...nodes, ...components]);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const TABLE_DEFINITION = FREE_FORM_SECTION_REGISTRY.table;
|
|
398
|
+
const RATE_TABLE_EXAMPLE = "Table: Drop-in rates\n"
|
|
399
|
+
+ "| Plan | Rate | Details |\n"
|
|
400
|
+
+ "| --- | --- | --- |\n"
|
|
401
|
+
+ "| Single class | **$24** | [Book now](/classes) |\n"
|
|
402
|
+
+ "| Five-pack | $100 | Save $20 \\| valid for 90 days |";
|
|
403
|
+
const COMPARISON_TABLE_EXAMPLE = "Feature | Starter | Studio\n"
|
|
404
|
+
+ "--- | --- | ---\n"
|
|
405
|
+
+ "Classes | 4 / month | **Unlimited**\n"
|
|
406
|
+
+ "Support | [Email](/support) | Email \\| phone";
|
|
407
|
+
const ALIGNMENT_TABLE_EXAMPLE = "| Feature | Price |\n| :--- | ---: |\n| Class | $24 |";
|
|
408
|
+
const TABLE_JSON_EXAMPLE = Object.freeze({
|
|
409
|
+
type: TABLE_DEFINITION.nodeTypes.table,
|
|
410
|
+
attrs: Object.freeze({ caption: "Drop-in rates" }),
|
|
411
|
+
content: Object.freeze([
|
|
412
|
+
Object.freeze({
|
|
413
|
+
type: TABLE_DEFINITION.nodeTypes.row,
|
|
414
|
+
content: Object.freeze([
|
|
415
|
+
Object.freeze({
|
|
416
|
+
type: TABLE_DEFINITION.nodeTypes.header,
|
|
417
|
+
content: Object.freeze([Object.freeze({
|
|
418
|
+
type: "paragraph",
|
|
419
|
+
content: Object.freeze([Object.freeze({ type: "text", text: "Plan" })]),
|
|
420
|
+
})]),
|
|
421
|
+
}),
|
|
422
|
+
Object.freeze({
|
|
423
|
+
type: TABLE_DEFINITION.nodeTypes.header,
|
|
424
|
+
content: Object.freeze([Object.freeze({
|
|
425
|
+
type: "paragraph",
|
|
426
|
+
content: Object.freeze([Object.freeze({ type: "text", text: "Rate" })]),
|
|
427
|
+
})]),
|
|
428
|
+
}),
|
|
429
|
+
]),
|
|
430
|
+
}),
|
|
431
|
+
Object.freeze({
|
|
432
|
+
type: TABLE_DEFINITION.nodeTypes.row,
|
|
433
|
+
content: Object.freeze([
|
|
434
|
+
Object.freeze({
|
|
435
|
+
type: TABLE_DEFINITION.nodeTypes.cell,
|
|
436
|
+
content: Object.freeze([Object.freeze({
|
|
437
|
+
type: "paragraph",
|
|
438
|
+
content: Object.freeze([Object.freeze({ type: "text", text: "Single class" })]),
|
|
439
|
+
})]),
|
|
440
|
+
}),
|
|
441
|
+
Object.freeze({
|
|
442
|
+
type: TABLE_DEFINITION.nodeTypes.cell,
|
|
443
|
+
content: Object.freeze([Object.freeze({
|
|
444
|
+
type: "paragraph",
|
|
445
|
+
content: Object.freeze([Object.freeze({ type: "text", text: "$24" })]),
|
|
446
|
+
})]),
|
|
447
|
+
}),
|
|
448
|
+
]),
|
|
449
|
+
}),
|
|
450
|
+
]),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
function tableReference() {
|
|
454
|
+
return Object.freeze({
|
|
455
|
+
nodeTypes: Object.freeze({ ...TABLE_DEFINITION.nodeTypes }),
|
|
456
|
+
attrs: Object.freeze({
|
|
457
|
+
caption: Object.freeze({ ...TABLE_DEFINITION.attrs.caption }),
|
|
458
|
+
}),
|
|
459
|
+
structure: Object.freeze({
|
|
460
|
+
headerRows: TABLE_DEFINITION.rows.header,
|
|
461
|
+
headerCellType: TABLE_DEFINITION.nodeTypes.header,
|
|
462
|
+
dataCellType: TABLE_DEFINITION.nodeTypes.cell,
|
|
463
|
+
paragraphChildrenPerCell: TABLE_DEFINITION.cells.paragraphs,
|
|
464
|
+
paragraphAttrsSupported: TABLE_DEFINITION.cells.paragraphAttrs,
|
|
465
|
+
inlineNodes: Object.freeze([...TABLE_DEFINITION.cells.inlineNodes]),
|
|
466
|
+
marks: Object.freeze([...TABLE_DEFINITION.cells.marks]),
|
|
467
|
+
headerMustContainNonWhitespace: TABLE_DEFINITION.cells.headerNonWhitespace,
|
|
468
|
+
spansSupported: TABLE_DEFINITION.cells.spans,
|
|
469
|
+
unsupportedSpanAttrs: Object.freeze([...TABLE_DEFINITION.cells.spanAttrs]),
|
|
470
|
+
}),
|
|
471
|
+
limits: Object.freeze({
|
|
472
|
+
captionScalars: Object.freeze({
|
|
473
|
+
min: TABLE_DEFINITION.attrs.caption.minScalars,
|
|
474
|
+
max: TABLE_DEFINITION.attrs.caption.maxScalars,
|
|
475
|
+
unit: "Unicode scalar values",
|
|
476
|
+
nonWhitespace: true,
|
|
477
|
+
}),
|
|
478
|
+
dataRows: Object.freeze({ min: TABLE_DEFINITION.rows.minData, max: TABLE_DEFINITION.rows.maxData }),
|
|
479
|
+
columns: Object.freeze({
|
|
480
|
+
min: TABLE_DEFINITION.columns.min,
|
|
481
|
+
max: TABLE_DEFINITION.columns.max,
|
|
482
|
+
everyRowMatchesHeader: !TABLE_DEFINITION.columns.ragged,
|
|
483
|
+
}),
|
|
484
|
+
cellTextScalars: Object.freeze({
|
|
485
|
+
max: TABLE_DEFINITION.cells.maxTextScalars,
|
|
486
|
+
unit: "Unicode scalar values across text descendants; marks and hard breaks add zero",
|
|
487
|
+
}),
|
|
488
|
+
}),
|
|
489
|
+
placement: Object.freeze({ ...TABLE_DEFINITION.placement }),
|
|
490
|
+
captionGuidance:
|
|
491
|
+
"Use a caption when surrounding prose does not already name the table; omit it when an immediately preceding heading already names the table.",
|
|
492
|
+
markdown: Object.freeze({
|
|
493
|
+
captionSyntax: `${TABLE_DEFINITION.markdown.captionPrefix}Caption text`,
|
|
494
|
+
captionMustImmediatelyPrecedeHeader: TABLE_DEFINITION.markdown.captionImmediate,
|
|
495
|
+
outerPipes: TABLE_DEFINITION.markdown.outerPipes,
|
|
496
|
+
literalPipeEscape: TABLE_DEFINITION.markdown.literalPipeEscape,
|
|
497
|
+
delimiter: `one or more hyphens per column (minimum ${TABLE_DEFINITION.markdown.delimiterMinHyphens}); no other text`,
|
|
498
|
+
alignment: TABLE_DEFINITION.markdown.alignment,
|
|
499
|
+
blankLineTerminates: TABLE_DEFINITION.markdown.blankLineTerminates,
|
|
500
|
+
}),
|
|
501
|
+
examples: Object.freeze({
|
|
502
|
+
rateTable: RATE_TABLE_EXAMPLE,
|
|
503
|
+
comparisonTable: COMPARISON_TABLE_EXAMPLE,
|
|
504
|
+
rejectedAlignment: ALIGNMENT_TABLE_EXAMPLE,
|
|
505
|
+
proseMirror: TABLE_JSON_EXAMPLE,
|
|
506
|
+
}),
|
|
507
|
+
corrections: Object.freeze([
|
|
508
|
+
Object.freeze({
|
|
509
|
+
code: CONTENT_ERROR_CODES.childNotAllowed,
|
|
510
|
+
correction: "Move a table to the document root or directly into a top-level section; cells contain a paragraph, never another block.",
|
|
511
|
+
}),
|
|
512
|
+
Object.freeze({
|
|
513
|
+
code: CONTENT_ERROR_CODES.attrUnknown,
|
|
514
|
+
correction: "Keep only the optional caption attribute on table; tableRow, tableHeader, and tableCell have no attributes or appearance controls.",
|
|
515
|
+
}),
|
|
516
|
+
Object.freeze({
|
|
517
|
+
code: CONTENT_ERROR_CODES.tableHeader,
|
|
518
|
+
correction: "Put exactly one tableRow first, use only tableHeader cells in it, and give every header non-whitespace text.",
|
|
519
|
+
}),
|
|
520
|
+
Object.freeze({
|
|
521
|
+
code: CONTENT_ERROR_CODES.tableShape,
|
|
522
|
+
correction: "Use tableRow children, tableHeader only in the first row, tableCell after it, and keep tables at root or directly in a section.",
|
|
523
|
+
}),
|
|
524
|
+
Object.freeze({
|
|
525
|
+
code: CONTENT_ERROR_CODES.tableRagged,
|
|
526
|
+
correction: "Add or remove cells so the delimiter and every data row have exactly the header row's column count.",
|
|
527
|
+
}),
|
|
528
|
+
Object.freeze({
|
|
529
|
+
code: CONTENT_ERROR_CODES.tableBounds,
|
|
530
|
+
correction: `Use ${TABLE_DEFINITION.rows.minData}-${TABLE_DEFINITION.rows.maxData} data rows, ${TABLE_DEFINITION.columns.min}-${TABLE_DEFINITION.columns.max} columns, captions up to ${TABLE_DEFINITION.attrs.caption.maxScalars} scalars, and cells up to ${TABLE_DEFINITION.cells.maxTextScalars} text scalars.`,
|
|
531
|
+
}),
|
|
532
|
+
Object.freeze({
|
|
533
|
+
code: CONTENT_ERROR_CODES.tableCellContent,
|
|
534
|
+
correction: "Give every tableHeader and tableCell exactly one paragraph containing only text, hardBreak, and the supported marks/links.",
|
|
535
|
+
}),
|
|
536
|
+
Object.freeze({
|
|
537
|
+
code: CONTENT_ERROR_CODES.tableSpan,
|
|
538
|
+
correction: "Remove colspan, rowspan, and colwidth; express every column with one explicit cell in every row.",
|
|
539
|
+
}),
|
|
540
|
+
Object.freeze({
|
|
541
|
+
code: CONTENT_ERROR_CODES.markdownTable,
|
|
542
|
+
correction: "Use a pipe header, a matching hyphen-only delimiter, at least one data row, and a blank line before the next block.",
|
|
543
|
+
}),
|
|
544
|
+
Object.freeze({
|
|
545
|
+
code: CONTENT_ERROR_CODES.markdownTableAlignment,
|
|
546
|
+
correction: "Remove alignment colons from delimiter cells; alignment is not part of this contract.",
|
|
547
|
+
}),
|
|
548
|
+
]),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const TABLE_REFERENCE = tableReference();
|
|
553
|
+
const INLINE_FACTS_REFERENCE = inlineFactsReference();
|
|
554
|
+
const BACKGROUND_REFERENCE = backgroundReference();
|
|
555
|
+
|
|
556
|
+
const FREE_FORM_REFERENCE = Object.freeze({
|
|
557
|
+
type: "free-form",
|
|
558
|
+
displayName: "Free-form page",
|
|
559
|
+
summary: "A page whose body is authored as validated ProseMirror, directly or through the supported Markdown subset.",
|
|
560
|
+
workspace: Object.freeze({
|
|
561
|
+
directory: "pages/",
|
|
562
|
+
sourceRule: Object.freeze({
|
|
563
|
+
rule:
|
|
564
|
+
"One page path has exactly one authoritative editable source. The manifest records its file and sourceFormat.",
|
|
565
|
+
manifestFields: Object.freeze(["file", "sourceFormat", "baseline"]),
|
|
566
|
+
pull:
|
|
567
|
+
"pull keeps a tracked source that is still on disk and never writes a second editable file for the same page.",
|
|
568
|
+
internalState:
|
|
569
|
+
`${INTERNAL_PAGE_BASELINE_DIRECTORY}/<pageId>.pm.json holds the site's document for a page tracked as Markdown. `
|
|
570
|
+
+ "It is internal state: it is never discovered as a page source and is never pushed, and any pull rewrites "
|
|
571
|
+
+ "it, so it is safe to ignore in version control.",
|
|
572
|
+
formatChange:
|
|
573
|
+
"Remove the tracked source, then author the other format beside it. Two editable sources for one path is a "
|
|
574
|
+
+ "refusal, not a guess.",
|
|
575
|
+
formatChangeError: "pages.path_conflict",
|
|
576
|
+
renamedSource:
|
|
577
|
+
"A tracked source renamed onto the workspace file another page falls back to leaves that page nowhere to be "
|
|
578
|
+
+ "written. Rename or remove it, then pull again.",
|
|
579
|
+
renamedSourceError: "pages.source_conflict",
|
|
580
|
+
conflict:
|
|
581
|
+
"Markdown is one-way, so a page edited on the site since the last pull cannot be rewritten as Markdown. pull "
|
|
582
|
+
+ "refuses before changing anything under pages/ and preserves the site's version as internal state. A "
|
|
583
|
+
+ "locally edited .pm.json is kept rather than overwritten, and only collides when the site changed too.",
|
|
584
|
+
conflictError: "pages.pull_conflict",
|
|
585
|
+
conflictRecovery:
|
|
586
|
+
"Either push the local source to make the site match it, or delete the local source and pull again to adopt "
|
|
587
|
+
+ "the site's document as that page's source.",
|
|
588
|
+
pushSelection:
|
|
589
|
+
"pages push <path> validates the selected page exactly as a whole push would, including every cross-page "
|
|
590
|
+
+ "guard that can affect it. It does not convert or validate pages it is not sending; pages push with no "
|
|
591
|
+
+ "path is the command that reports every authored page.",
|
|
592
|
+
}),
|
|
593
|
+
formats: Object.freeze([
|
|
594
|
+
Object.freeze({
|
|
595
|
+
extension: ".md",
|
|
596
|
+
purpose: "Author a page as Markdown with YAML-style front matter.",
|
|
597
|
+
metadata: Object.freeze([
|
|
598
|
+
Object.freeze({
|
|
599
|
+
name: "title",
|
|
600
|
+
type: "string",
|
|
601
|
+
requiredForNew: true,
|
|
602
|
+
inheritedForTracked: true,
|
|
603
|
+
}),
|
|
604
|
+
Object.freeze({
|
|
605
|
+
name: "path",
|
|
606
|
+
type: "string",
|
|
607
|
+
requiredForNew: true,
|
|
608
|
+
inheritedForTracked: true,
|
|
609
|
+
description: "Use an empty value for the home page.",
|
|
610
|
+
}),
|
|
611
|
+
Object.freeze({
|
|
612
|
+
name: "description",
|
|
613
|
+
type: "string",
|
|
614
|
+
requiredForNew: false,
|
|
615
|
+
inheritedForTracked: true,
|
|
616
|
+
defaultForNew: "",
|
|
617
|
+
}),
|
|
618
|
+
]),
|
|
619
|
+
}),
|
|
620
|
+
Object.freeze({
|
|
621
|
+
extension: ".pm.json",
|
|
622
|
+
purpose:
|
|
623
|
+
"Edit a pulled page as a raw ProseMirror document. The page must already have a manifest entry for metadata.",
|
|
624
|
+
metadata: Object.freeze([]),
|
|
625
|
+
metadataSource:
|
|
626
|
+
"Read from .taproot-site-manifest.json at the workspace root; the document file contains only the ProseMirror root.",
|
|
627
|
+
}),
|
|
628
|
+
]),
|
|
629
|
+
systemPages: Object.freeze([
|
|
630
|
+
Object.freeze({
|
|
631
|
+
path: SYSTEM_PAGE_NOT_FOUND_PATH,
|
|
632
|
+
mode: PAGE_WORKSPACE_MODE_READ_ONLY,
|
|
633
|
+
reason: PAGE_READ_ONLY_REASON_SYSTEM_404,
|
|
634
|
+
projection:
|
|
635
|
+
"pull writes the exact stored ProseMirror body and records its SHA-256 in .taproot-site-manifest.json",
|
|
636
|
+
unchangedPush: "A whole-workspace pages push verifies the hash and skips this file without updating the page.",
|
|
637
|
+
approval:
|
|
638
|
+
"An unscoped approve excludes this projection even when the live system page carries a draft; naming it explicitly is refused.",
|
|
639
|
+
modifiedError: "pages.read_only_modified",
|
|
640
|
+
missingError: "pages.read_only_missing",
|
|
641
|
+
replacementError: "pages.system_page_read_only",
|
|
642
|
+
scopedPushError: "pages.page_read_only",
|
|
643
|
+
scopedApprovalError: "approve.page_read_only",
|
|
644
|
+
guidance:
|
|
645
|
+
"Do not delete, replace, or edit this file. Run pull to restore it; author the system 404 through an owner-controlled surface.",
|
|
646
|
+
}),
|
|
647
|
+
]),
|
|
648
|
+
systemHome: "The pulled home page remains an ordinary editable page.",
|
|
649
|
+
}),
|
|
650
|
+
document: Object.freeze({
|
|
651
|
+
root: Object.freeze({ type: "doc", content: "array of supported block nodes" }),
|
|
652
|
+
nodes: NODE_TYPES,
|
|
653
|
+
marks: MARK_TYPES,
|
|
654
|
+
componentNode: Object.freeze({
|
|
655
|
+
type: "componentBlock",
|
|
656
|
+
attrs: Object.freeze({
|
|
657
|
+
componentType: "one of the listed component identifiers",
|
|
658
|
+
componentData: "JSON string containing that component's data object",
|
|
659
|
+
}),
|
|
660
|
+
}),
|
|
661
|
+
sectionNode: Object.freeze({
|
|
662
|
+
type: FREE_FORM_SECTION_REGISTRY.section.nodeType,
|
|
663
|
+
label: FREE_FORM_SECTION_REGISTRY.section.label,
|
|
664
|
+
help: FREE_FORM_SECTION_REGISTRY.section.help,
|
|
665
|
+
minimumChildren: FREE_FORM_SECTION_REGISTRY.section.minimumChildren,
|
|
666
|
+
additionalProperties: false,
|
|
667
|
+
attrs: Object.freeze(sectionAttributeReference()),
|
|
668
|
+
content: "one or more ordinary non-section block nodes; sections may not nest",
|
|
669
|
+
}),
|
|
670
|
+
tableNode: TABLE_REFERENCE,
|
|
671
|
+
inlineFactsNode: INLINE_FACTS_REFERENCE,
|
|
672
|
+
rawHtml: Object.freeze({
|
|
673
|
+
default: "rejected",
|
|
674
|
+
trackedProseMirror: Object.freeze({
|
|
675
|
+
format: ".pm.json",
|
|
676
|
+
optIn: `${CLI_BINARY_NAME} pages push --allow-raw-html`,
|
|
677
|
+
behavior: "The flag permits explicit rawHtml nodes only in a tracked ProseMirror document.",
|
|
678
|
+
}),
|
|
679
|
+
markdown: Object.freeze({
|
|
680
|
+
format: ".md",
|
|
681
|
+
supported: false,
|
|
682
|
+
behavior: "Inline HTML remains unsupported and is rejected even when --allow-raw-html is present.",
|
|
683
|
+
}),
|
|
684
|
+
warning: "rawHtml renders verbatim and unsanitized; use it only for trusted hand-written markup.",
|
|
685
|
+
}),
|
|
686
|
+
}),
|
|
687
|
+
sections: Object.freeze({
|
|
688
|
+
markdownSyntax: ":::section {JSON object}\nordinary structured Markdown\n:::",
|
|
689
|
+
example:
|
|
690
|
+
":::section {\"context\":\"inverted\",\"contentPadding\":\"standard\",\"surface\":\"none\"}\n## A real band\n\nOrdinary structured Markdown remains editable here.\n:::",
|
|
691
|
+
topLevelOnly: true,
|
|
692
|
+
defaults: Object.freeze(Object.fromEntries(
|
|
693
|
+
Object.entries(FREE_FORM_SECTION_REGISTRY.section.attrs).map(([name, definition]) => [name, definition.default]),
|
|
694
|
+
)),
|
|
695
|
+
background: BACKGROUND_REFERENCE,
|
|
696
|
+
decoration: decorationReference(),
|
|
697
|
+
placementMatrix: placementMatrix(),
|
|
698
|
+
}),
|
|
699
|
+
tables: TABLE_REFERENCE,
|
|
700
|
+
inlineFacts: INLINE_FACTS_REFERENCE,
|
|
701
|
+
workflow: Object.freeze([
|
|
702
|
+
Object.freeze({ command: `${CLI_BINARY_NAME} pages push`, result: "Create or update an unapproved draft." }),
|
|
703
|
+
Object.freeze({
|
|
704
|
+
command: `${CLI_BINARY_NAME} approve [page-path...]`,
|
|
705
|
+
result: "Approve drafts and build the staged site.",
|
|
706
|
+
}),
|
|
707
|
+
Object.freeze({
|
|
708
|
+
command: `${CLI_BINARY_NAME} deploy --staging`,
|
|
709
|
+
result: "Deploy the staged build to the staging site.",
|
|
710
|
+
}),
|
|
711
|
+
Object.freeze({
|
|
712
|
+
command: `${CLI_BINARY_NAME} deploy --production`,
|
|
713
|
+
result: "Promote the completed staging deployment to production.",
|
|
714
|
+
}),
|
|
715
|
+
]),
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
export function listPageTypeReferences() {
|
|
719
|
+
return PAGE_TYPES.map((type) => ({
|
|
720
|
+
type,
|
|
721
|
+
displayName: FREE_FORM_REFERENCE.displayName,
|
|
722
|
+
summary: FREE_FORM_REFERENCE.summary,
|
|
723
|
+
helpCommand: `${CLI_BINARY_NAME} help page ${type}`,
|
|
724
|
+
}));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export function getPageTypeReference(type) {
|
|
728
|
+
if (type !== "free-form") return undefined;
|
|
729
|
+
return { ...FREE_FORM_REFERENCE, components: listComponentTypeReferences() };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function schemaLabel(schema) {
|
|
733
|
+
if (schema.enum) return schema.enum.map((value) => JSON.stringify(value)).join(" | ");
|
|
734
|
+
if (schema.type === "array") return `array<${schemaLabel(schema.items)}>`;
|
|
735
|
+
if (Array.isArray(schema.type)) return schema.type.join(" | ");
|
|
736
|
+
if (schema.format) return `${schema.type} (${schema.format})`;
|
|
737
|
+
return schema.type;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function schemaConstraints(schema) {
|
|
741
|
+
const constraints = [];
|
|
742
|
+
if (schema.minimum !== undefined) constraints.push(`minimum ${schema.minimum}`);
|
|
743
|
+
if (schema.maximum !== undefined) constraints.push(`maximum ${schema.maximum}`);
|
|
744
|
+
if (schema.maxItems !== undefined) constraints.push(`maximum ${schema.maxItems} items`);
|
|
745
|
+
if (schema.minLength !== undefined) constraints.push(`minimum length ${schema.minLength}`);
|
|
746
|
+
if (schema.maxLength !== undefined) constraints.push(`maximum length ${schema.maxLength}`);
|
|
747
|
+
if (schema.pattern !== undefined) constraints.push(`pattern ${JSON.stringify(schema.pattern)}`);
|
|
748
|
+
if (schema.allowedUnits !== undefined) constraints.push(`allowed units ${schema.allowedUnits.join(", ")}`);
|
|
749
|
+
if (schema.builtInValues !== undefined) constraints.push(`built-ins ${schema.builtInValues.join(", ")}`);
|
|
750
|
+
if (schema.applicationRegistered?.accepted === true) {
|
|
751
|
+
constraints.push(`application-registered names accepted; ${schema.applicationRegistered.requirement}`);
|
|
752
|
+
}
|
|
753
|
+
if (schema.description) constraints.push(schema.description);
|
|
754
|
+
return constraints;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function formatSchemaLine(name, schema, required, indent) {
|
|
758
|
+
const constraints = schemaConstraints(schema);
|
|
759
|
+
return `${indent}${name.padEnd(Math.max(1, 28 - indent.length))} ${schemaLabel(schema)}; ${
|
|
760
|
+
required ? "required" : "optional"
|
|
761
|
+
}${constraints.length > 0 ? `; ${constraints.join("; ")}` : ""}`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function nestedSchemaLines(schema, path, indent = " ") {
|
|
765
|
+
const valueSchema = schema.type === "array" ? schema.items : schema;
|
|
766
|
+
const valuePath = schema.type === "array" ? `${path}[]` : path;
|
|
767
|
+
if (!valueSchema?.properties) return [];
|
|
768
|
+
|
|
769
|
+
const lines = [];
|
|
770
|
+
for (const property of valueSchema.properties) {
|
|
771
|
+
const propertyPath = `${valuePath}.${property.name}`;
|
|
772
|
+
lines.push(formatSchemaLine(propertyPath, property.schema, property.required, indent));
|
|
773
|
+
lines.push(...nestedSchemaLines(property.schema, propertyPath, `${indent} `));
|
|
774
|
+
}
|
|
775
|
+
return lines;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function formatComponentProperty(property) {
|
|
779
|
+
const firstLine = `${formatSchemaLine(property.name, property.schema, property.required, " ")}; ${
|
|
780
|
+
formatOmission(property)
|
|
781
|
+
}`;
|
|
782
|
+
return [firstLine, ...nestedSchemaLines(property.schema, property.name)].join("\n");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function formatOmission(property) {
|
|
786
|
+
const initial = property.editorInitial.kind === "value"
|
|
787
|
+
? `editor initial ${JSON.stringify(property.editorInitial.value)}`
|
|
788
|
+
: "editor initial omitted";
|
|
789
|
+
const omitted = property.whenOmitted.kind === "value"
|
|
790
|
+
? `when omitted ${JSON.stringify(property.whenOmitted.value)}`
|
|
791
|
+
: property.whenOmitted.kind === "invalid"
|
|
792
|
+
? "when omitted invalid"
|
|
793
|
+
: property.whenOmitted.kind === "conditional"
|
|
794
|
+
? `when omitted ${property.whenOmitted.then} if ${property.whenOmitted.condition}, otherwise ${property.whenOmitted.otherwise}`
|
|
795
|
+
: "when omitted unspecified";
|
|
796
|
+
return `${initial}; ${omitted}`;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function formatMetadata(field) {
|
|
800
|
+
const requirement = field.requiredForNew
|
|
801
|
+
? "required for a new page; inherited for a tracked page"
|
|
802
|
+
: `optional; default for a new page ${JSON.stringify(field.defaultForNew)}; inherited for a tracked page`;
|
|
803
|
+
return ` ${field.name.padEnd(20)} ${field.type}; ${requirement}${field.description ? `; ${field.description}` : ""}`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function formatSectionAttr(attr) {
|
|
807
|
+
const type = attr.values ? attr.values.map((value) => JSON.stringify(value)).join(" | ") : attr.type;
|
|
808
|
+
return ` ${attr.name.padEnd(20)} ${type}; default ${JSON.stringify(attr.default)}; ${attr.help}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function formatRegistryField(field, indent = " ") {
|
|
812
|
+
if (field.type === "processed-image") {
|
|
813
|
+
return `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))} processed-image${field.nullable ? " | null" : ""}; ${
|
|
814
|
+
field.required ? "required" : `default ${JSON.stringify(field.default)}`
|
|
815
|
+
}; required keys ${field.requiredKeys.join(", ")}; optional keys ${
|
|
816
|
+
field.optionalKeys.join(", ")
|
|
817
|
+
}; responsive candidates ${field.responsiveUrls.minItems} through ${field.responsiveUrls.maxItems}; minWidth positive integer; optional type ${
|
|
818
|
+
field.responsiveUrls.fields.find((item) => item.name === "type").values.map((value) => JSON.stringify(value)).join(" | ")
|
|
819
|
+
}; src must match one candidate; unlisted keys rejected`;
|
|
820
|
+
}
|
|
821
|
+
const type = field.values ? field.values.map((value) => JSON.stringify(value)).join(" | ") : field.type;
|
|
822
|
+
const bounds = field.minimum === undefined ? "" : `; bounds ${field.minimum} through ${field.maximum}`;
|
|
823
|
+
const itemBounds = field.minItems === undefined ? "" : `; items ${field.minItems} through ${field.maxItems}`;
|
|
824
|
+
const scalarBounds = field.maxScalars === undefined
|
|
825
|
+
? ""
|
|
826
|
+
: `; ${field.minScalars} through ${field.maxScalars} Unicode scalar values${field.nonWhitespace ? "; non-whitespace" : ""}`;
|
|
827
|
+
const line = `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))} ${type}${field.nullable ? " | null" : ""}; ${
|
|
828
|
+
field.required ? "required" : `default ${JSON.stringify(field.default)}`
|
|
829
|
+
}${bounds}${itemBounds}${scalarBounds}`;
|
|
830
|
+
return [line, ...(field.fields ?? []).map((child) => formatRegistryField(child, `${indent} `))].join("\n");
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function formatPlacement(row) {
|
|
834
|
+
const name = `${row.kind}:${row.type}`;
|
|
835
|
+
return ` ${name.padEnd(29)} ${row.rootPlacement.padEnd(13)} full bleed ${
|
|
836
|
+
row.fullBleed ? "yes" : "no"
|
|
837
|
+
}; ${row.contentPadding}; ${row.well}; measure ${row.measure}`;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function formatReferenceResult(result) {
|
|
841
|
+
switch (result.topic) {
|
|
842
|
+
case "topics":
|
|
843
|
+
return `Usage: ${CLI_BINARY_NAME} help <topic> [name] [--json]\n\nReference topics:\n${
|
|
844
|
+
result.topics.map((topic) => ` ${topic.usage.padEnd(48)} ${topic.summary}`).join("\n")
|
|
845
|
+
}\n\nAdd --json for stable machine-readable output.\n`;
|
|
846
|
+
case "page-types":
|
|
847
|
+
return `Authorable page types:\n${
|
|
848
|
+
result.pageTypes.map((page) =>
|
|
849
|
+
` ${page.type.padEnd(16)} ${page.summary}\n${" ".repeat(19)}Help: ${page.helpCommand}`
|
|
850
|
+
).join("\n")
|
|
851
|
+
}\n`;
|
|
852
|
+
case "component-types":
|
|
853
|
+
return `Free-form components:\n${
|
|
854
|
+
result.components.map((component) =>
|
|
855
|
+
` ${component.type.padEnd(16)} ${component.summary}\n${" ".repeat(19)}Help: ${component.helpCommand}`
|
|
856
|
+
).join("\n")
|
|
857
|
+
}\n`;
|
|
858
|
+
case "page": {
|
|
859
|
+
const page = result.page;
|
|
860
|
+
const markdownFormat = page.workspace.formats.find((format) => format.extension === ".md");
|
|
861
|
+
const sourceRule = page.workspace.sourceRule;
|
|
862
|
+
return `${page.displayName} (${page.type})\n${page.summary}\n\nWorkspace:\n${
|
|
863
|
+
page.workspace.formats.map((format) => ` ${format.extension.padEnd(9)} ${format.purpose}`).join("\n")
|
|
864
|
+
}\n\nOne source per page:\n rule ${sourceRule.rule}\n manifest fields ${
|
|
865
|
+
sourceRule.manifestFields.join(", ")
|
|
866
|
+
}\n pull ${sourceRule.pull}\n internal state ${
|
|
867
|
+
sourceRule.internalState
|
|
868
|
+
}\n format change ${sourceRule.formatChange} (${sourceRule.formatChangeError})\n renamed source ${
|
|
869
|
+
sourceRule.renamedSource
|
|
870
|
+
} (${sourceRule.renamedSourceError})\n conflicts ${
|
|
871
|
+
sourceRule.conflict
|
|
872
|
+
} (${sourceRule.conflictError})\n recovery ${
|
|
873
|
+
sourceRule.conflictRecovery
|
|
874
|
+
}\n push selection ${sourceRule.pushSelection}\n\nSystem page projections:\n${
|
|
875
|
+
page.workspace.systemPages.map((systemPage) =>
|
|
876
|
+
` ${systemPage.path.padEnd(9)} ${systemPage.mode}; ${systemPage.projection}.\n${" ".repeat(12)}${systemPage.unchangedPush} ${systemPage.approval}\n${" ".repeat(12)}${systemPage.guidance}`
|
|
877
|
+
).join("\n")
|
|
878
|
+
}\n home editable; ${page.workspace.systemHome}\n\nMarkdown front matter:\n${
|
|
879
|
+
markdownFormat.metadata.map(formatMetadata).join("\n")
|
|
880
|
+
}\n\nDocument root: { "type": "doc", "content": [...] }\nSupported nodes: ${
|
|
881
|
+
page.document.nodes.join(", ")
|
|
882
|
+
}\nSupported marks: ${
|
|
883
|
+
page.document.marks.join(", ")
|
|
884
|
+
}\n\nTop-level section container:\n${page.sections.example}\n\nSection attributes:\n${
|
|
885
|
+
page.document.sectionNode.attrs.map(formatSectionAttr).join("\n")
|
|
886
|
+
}\n\nSection photo background (closed object):\n${
|
|
887
|
+
page.sections.background.fields.map((field) => formatRegistryField(field)).join("\n")
|
|
888
|
+
}\n delivery URLs ${page.sections.background.deliveryUrlPolicy}\n compact behavior ${page.sections.background.compactBehavior}\n\nScrim opacity mapping:\n${
|
|
889
|
+
Object.entries(page.sections.background.scrimOpacityByValue).map(([name, opacity]) => ` ${name.padEnd(20)} ${opacity}`).join("\n")
|
|
890
|
+
}\n\nResponsive background example:\n${page.sections.background.markdownExample}\n\nSection decoration (closed object):\n${
|
|
891
|
+
page.sections.decoration.fields.map((field) => formatRegistryField(field)).join("\n")
|
|
892
|
+
}\n delivery URLs ${page.sections.decoration.deliveryUrlPolicy}\n\nTint mapping:\n${
|
|
893
|
+
Object.entries(page.sections.decoration.tintTokens).map(([name, token]) => ` ${name.padEnd(20)} ${token}`).join("\n")
|
|
894
|
+
}\n\nTransparent-mask example:\n${page.sections.decoration.markdownExample}\n\nMask guidance: ${
|
|
895
|
+
page.sections.decoration.maskGuidance
|
|
896
|
+
}\nWarning: ${page.sections.decoration.opaqueWarning}\n\nRoot defaults and placement:\n${
|
|
897
|
+
page.sections.placementMatrix.map(formatPlacement).join("\n")
|
|
898
|
+
}\n\nSemantic tables:\n placement document root or directly inside a top-level section; wide/full-well measure\n rows exactly ${page.tables.structure.headerRows} header row, then ${page.tables.limits.dataRows.min} through ${page.tables.limits.dataRows.max} data rows\n columns ${page.tables.limits.columns.min} through ${page.tables.limits.columns.max}; delimiter and every row must match the header width\n caption optional non-empty plain text; maximum ${page.tables.limits.captionScalars.max} Unicode scalar values; syntax ${page.tables.markdown.captionSyntax}\n caption guidance ${page.tables.captionGuidance}\n cells exactly ${page.tables.structure.paragraphChildrenPerCell} paragraph; headers need non-whitespace text; data cells may be empty; maximum ${page.tables.limits.cellTextScalars.max} text scalars\n inline ${page.tables.structure.inlineNodes.join(", ")}; marks ${page.tables.structure.marks.join(", ")}\n pipes outer pipes ${page.tables.markdown.outerPipes}; write a literal pipe as ${page.tables.markdown.literalPipeEscape}\n spans unsupported: ${page.tables.structure.unsupportedSpanAttrs.join(", ")}\n alignment ${page.tables.markdown.alignment}; delimiter cells contain hyphens only\n termination a blank line ends the table before another block\n\nRate table example:\n${page.tables.examples.rateTable}\n\nComparison table example:\n${page.tables.examples.comparisonTable}\n\nRejected alignment example (${CONTENT_ERROR_CODES.markdownTableAlignment}):\n${page.tables.examples.rejectedAlignment}\nRemove the ':' characters from the delimiter row.\n\nDirect ProseMirror table example:\n${JSON.stringify(page.tables.examples.proseMirror, null, 2)}\n\nTable error corrections:\n${
|
|
899
|
+
page.tables.corrections.map((entry) => ` ${entry.code.padEnd(34)} ${entry.correction}`).join("\n")
|
|
900
|
+
}\n\nInline facts:\n placement document root or directly inside a top-level section; ${page.inlineFacts.placement.measure} measure\n items ${page.inlineFacts.attrs.items.minItems} through ${page.inlineFacts.attrs.items.maxItems}; closed objects in value, label, url order\n value and label required non-whitespace plain text; maximum ${page.inlineFacts.attrs.items.fields[0].maxScalars} Unicode scalar values each\n url ${page.inlineFacts.urlPolicy}\n Markdown fenced ${page.inlineFacts.markdown.fence} block whose body is a JSON array\n\nInline-facts Markdown example:\n${page.inlineFacts.markdown.example}\n\nDirect ProseMirror inlineFacts example:\n${JSON.stringify(page.inlineFacts.examples.proseMirror, null, 2)}\n\nRaw HTML: ${page.document.rawHtml.default} by default. For a tracked ${page.document.rawHtml.trackedProseMirror.format} document only: ${page.document.rawHtml.trackedProseMirror.behavior} Opt in with ${page.document.rawHtml.trackedProseMirror.optIn}. For ${page.document.rawHtml.markdown.format}, ${page.document.rawHtml.markdown.behavior} ${page.document.rawHtml.warning}\n\nComponents:\n${
|
|
901
|
+
page.components.map((component) => ` ${component.type.padEnd(16)} ${component.summary}`).join("\n")
|
|
902
|
+
}\n\nWorkflow:\n${
|
|
903
|
+
page.workflow.map((step) => ` ${step.command}: ${step.result}`).join("\n")
|
|
904
|
+
}\n\nUse --json for exact metadata and component-node shapes.\n`;
|
|
905
|
+
}
|
|
906
|
+
case "component": {
|
|
907
|
+
const component = result.component;
|
|
908
|
+
return `${component.displayName} (${component.type})\n${component.summary}\n\nProperties:\n${
|
|
909
|
+
component.properties.map(formatComponentProperty).join("\n")
|
|
910
|
+
}\n\nUnlisted properties are rejected at every object level.\ncomponentData maximum: ${component.componentDataMaxUtf8Bytes} UTF-8 bytes.\n\nAccessibility:\n${
|
|
911
|
+
component.accessibility.map((note) => ` - ${note}`).join("\n")
|
|
912
|
+
}\n\nValid componentData object:\n${
|
|
913
|
+
JSON.stringify(component.example, null, 2)
|
|
914
|
+
}\n\nMarkdown component fence:\n${component.markdownExample}\n\nRemember: componentData stores JSON.stringify(the object), not the object itself. Use --json for the exact nested schema and a complete componentBlock example.\n`;
|
|
915
|
+
}
|
|
916
|
+
case "workflow": {
|
|
917
|
+
const reference = result.reference;
|
|
918
|
+
return `${reference.title}\n${reference.summary}\n\nUsage: ${reference.usage}\n\n${
|
|
919
|
+
reference.details.map((detail) => ` - ${detail}`).join("\n")
|
|
920
|
+
}\n\nExample:\n${JSON.stringify(reference.example, null, 2)}\n`;
|
|
921
|
+
}
|
|
922
|
+
case "presentation":
|
|
923
|
+
return formatPresentationReference(result.reference);
|
|
924
|
+
default:
|
|
925
|
+
throw new TypeError(`Unknown reference topic '${result.topic}'.`);
|
|
926
|
+
}
|
|
927
|
+
}
|