@taprootio/site-authoring 0.30.0 → 0.31.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 +68 -12
- package/package.json +2 -2
- package/src/api.js +110 -3
- package/src/changelog.json +19 -0
- package/src/cli.js +102 -6
- package/src/constants.js +3 -0
- package/src/content/components.js +16 -9
- package/src/content/free-form-section-registry.json +24 -4
- package/src/content/free-form-sections.js +11 -0
- package/src/content/image-description-document.js +108 -0
- package/src/content/image-description-validation.js +70 -0
- package/src/content/tiptap-prosemirror.ts +10 -0
- package/src/content/validate-document.js +2 -0
- package/src/image-description-contract.js +84 -0
- package/src/navigation-readiness.js +47 -0
- package/src/output.js +8 -0
- package/src/page-metadata.js +10 -10
- package/src/page-structure-checks.js +50 -45
- package/src/reference-help.js +8 -4
- package/src/site-check-findings.js +18 -12
- package/src/site-check-rules.json +10 -6
- package/src/typed-pages.js +25 -9
- package/src/verbs/deploy.js +11 -0
- package/src/verbs/index.js +10 -0
- package/src/verbs/media-images.js +67 -0
- package/src/verbs/pages-push.js +2 -1
- package/src/verbs/status.js +19 -5
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 15,
|
|
3
|
+
|
|
4
|
+
"imageDescription": {
|
|
5
|
+
"fields": {
|
|
6
|
+
"descriptionMode": { "kind": "mode", "storage": "persisted" },
|
|
7
|
+
"defaultDescription": { "kind": "text", "storage": "hydratedReadOnly" },
|
|
8
|
+
"descriptionRevision": { "kind": "revision", "storage": "hydratedReadOnly" }
|
|
9
|
+
},
|
|
10
|
+
"modes": ["inherit", "custom", "decorative"],
|
|
11
|
+
"maxDefaultDescriptionScalars": 1000,
|
|
12
|
+
"maxRevision": "9223372036854775807"
|
|
13
|
+
},
|
|
3
14
|
"section": {
|
|
4
15
|
"nodeType": "section",
|
|
5
16
|
"label": "Section",
|
|
@@ -84,7 +95,10 @@
|
|
|
84
95
|
"optionalKeys": [
|
|
85
96
|
"width",
|
|
86
97
|
"height",
|
|
87
|
-
"alt"
|
|
98
|
+
"alt",
|
|
99
|
+
"descriptionMode",
|
|
100
|
+
"defaultDescription",
|
|
101
|
+
"descriptionRevision"
|
|
88
102
|
],
|
|
89
103
|
"urls": {
|
|
90
104
|
"minItems": 1,
|
|
@@ -124,7 +138,10 @@
|
|
|
124
138
|
"optionalKeys": [
|
|
125
139
|
"width",
|
|
126
140
|
"height",
|
|
127
|
-
"alt"
|
|
141
|
+
"alt",
|
|
142
|
+
"descriptionMode",
|
|
143
|
+
"defaultDescription",
|
|
144
|
+
"descriptionRevision"
|
|
128
145
|
],
|
|
129
146
|
"urls": {
|
|
130
147
|
"minItems": 1,
|
|
@@ -237,7 +254,10 @@
|
|
|
237
254
|
"optionalKeys": [
|
|
238
255
|
"width",
|
|
239
256
|
"height",
|
|
240
|
-
"alt"
|
|
257
|
+
"alt",
|
|
258
|
+
"descriptionMode",
|
|
259
|
+
"defaultDescription",
|
|
260
|
+
"descriptionRevision"
|
|
241
261
|
],
|
|
242
262
|
"urls": {
|
|
243
263
|
"minItems": 1,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import registry from "./free-form-section-registry.json" with { type: "json" };
|
|
2
|
+
import { imageDescriptionIssues, pickImageDescriptionFields } from "./image-description-validation.js";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
CONTENT_ERROR_CODES as CODES,
|
|
@@ -45,6 +46,12 @@ function decorationDeliveryUrlIsSafe(value) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function validateImageDescription(image, path, label, report) {
|
|
50
|
+
for (const issue of imageDescriptionIssues(image)) {
|
|
51
|
+
report(`${path}/${issue.field}`, CODES.attrInvalid, `${label} ${issue.field} must ${issue.expected}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
function normalizeProcessedSectionImage(value, definition, path, label, report) {
|
|
49
56
|
let valid = true;
|
|
50
57
|
const reject = (errorPath, code, message) => {
|
|
@@ -170,6 +177,7 @@ function normalizeProcessedSectionImage(value, definition, path, label, report)
|
|
|
170
177
|
reject(`${path}/alt`, CODES.attrInvalid, `${label} alt must be a string.`);
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
validateImageDescription(value, path, label, reject);
|
|
173
181
|
if (!valid) return undefined;
|
|
174
182
|
return Object.freeze({
|
|
175
183
|
imageId: value.imageId,
|
|
@@ -182,6 +190,7 @@ function normalizeProcessedSectionImage(value, definition, path, label, report)
|
|
|
182
190
|
...(Object.hasOwn(value, "width") ? { width: value.width } : {}),
|
|
183
191
|
...(Object.hasOwn(value, "height") ? { height: value.height } : {}),
|
|
184
192
|
...(Object.hasOwn(value, "alt") ? { alt: value.alt } : {}),
|
|
193
|
+
...pickImageDescriptionFields(value),
|
|
185
194
|
});
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -457,6 +466,7 @@ export function normalizeFreeFormSectionDecoration(
|
|
|
457
466
|
if (Object.hasOwn(image, "alt") && typeof image.alt !== "string") {
|
|
458
467
|
report(`${imagePath}/alt`, CODES.attrInvalid, "Section decoration image alt must be a string.");
|
|
459
468
|
}
|
|
469
|
+
validateImageDescription(image, imagePath, "Section decoration image", report);
|
|
460
470
|
}
|
|
461
471
|
|
|
462
472
|
for (const [name, definition] of Object.entries(fields)) {
|
|
@@ -500,6 +510,7 @@ export function normalizeFreeFormSectionDecoration(
|
|
|
500
510
|
...(Object.hasOwn(image, "width") ? { width: image.width } : {}),
|
|
501
511
|
...(Object.hasOwn(image, "height") ? { height: image.height } : {}),
|
|
502
512
|
...(Object.hasOwn(image, "alt") ? { alt: image.alt } : {}),
|
|
513
|
+
...pickImageDescriptionFields(image),
|
|
503
514
|
};
|
|
504
515
|
const normalized = { image: Object.freeze(normalizedImage) };
|
|
505
516
|
for (const [name, definition] of Object.entries(fields)) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Generated from shared/image-description-document.ts by scripts/sync-field-validation.mjs. Do not edit.
|
|
2
|
+
import { IMAGE_DESCRIPTION_HYDRATED_KEYS, resolveImageDescription } from "./image-description-validation.js";
|
|
3
|
+
export function imageDescriptionImageId(image) {
|
|
4
|
+
return typeof image.imageId === "string"
|
|
5
|
+
? image.imageId
|
|
6
|
+
: typeof image.id === "string" && "caption" in image
|
|
7
|
+
? image.id
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Component data may be serialized inside a structured document. Keep unchanged bytes intact. */
|
|
11
|
+
export function visitImageDescriptionObjects(value, transform, seen = new WeakSet()) {
|
|
12
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
13
|
+
return;
|
|
14
|
+
seen.add(value);
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const child of value)
|
|
17
|
+
visitImageDescriptionObjects(child, transform, seen);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const record = value;
|
|
21
|
+
transform(record);
|
|
22
|
+
for (const [key, child] of Object.entries(record)) {
|
|
23
|
+
if (key === "componentData" && typeof child === "string") {
|
|
24
|
+
try {
|
|
25
|
+
const data = JSON.parse(child);
|
|
26
|
+
const before = JSON.stringify(data);
|
|
27
|
+
visitImageDescriptionObjects(data, transform, seen);
|
|
28
|
+
const after = JSON.stringify(data);
|
|
29
|
+
if (before !== after)
|
|
30
|
+
record[key] = after;
|
|
31
|
+
}
|
|
32
|
+
catch { /* Existing content validation owns malformed component data. */ }
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
visitImageDescriptionObjects(child, transform, seen);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function referencedImageDescriptionIds(value) {
|
|
39
|
+
const ids = new Set();
|
|
40
|
+
visitImageDescriptionObjects(value, image => {
|
|
41
|
+
const id = imageDescriptionImageId(image);
|
|
42
|
+
if (id)
|
|
43
|
+
ids.add(id);
|
|
44
|
+
});
|
|
45
|
+
return ids;
|
|
46
|
+
}
|
|
47
|
+
/** API delivery state does not belong in authored documents or workspace hashes. */
|
|
48
|
+
export function stripImageDescriptionHydration(value) {
|
|
49
|
+
const copy = structuredClone(value);
|
|
50
|
+
visitImageDescriptionObjects(copy, image => {
|
|
51
|
+
for (const key of IMAGE_DESCRIPTION_HYDRATED_KEYS)
|
|
52
|
+
delete image[key];
|
|
53
|
+
if (image.descriptionMode === "inherit") {
|
|
54
|
+
delete image.alt;
|
|
55
|
+
delete image.altText;
|
|
56
|
+
}
|
|
57
|
+
const nested = image.image;
|
|
58
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
59
|
+
const bannerImage = nested;
|
|
60
|
+
if ((image.descriptionMode ?? bannerImage.descriptionMode) === "inherit") {
|
|
61
|
+
delete image.altText;
|
|
62
|
+
delete bannerImage.alt;
|
|
63
|
+
delete bannerImage.altText;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return copy;
|
|
68
|
+
}
|
|
69
|
+
const text = (value) => typeof value === "string" ? value : undefined;
|
|
70
|
+
const placementMode = (mode, contextual) => mode === undefined || mode === null || mode === "" ? contextual.trim() ? "custom" : "inherit" : mode;
|
|
71
|
+
export function hydrateImageDescriptions(value, source) {
|
|
72
|
+
const copy = structuredClone(value);
|
|
73
|
+
const metadata = (image, delivery) => {
|
|
74
|
+
image.defaultDescription = delivery.defaultDescription ?? null;
|
|
75
|
+
image.descriptionRevision = delivery.descriptionRevision;
|
|
76
|
+
};
|
|
77
|
+
visitImageDescriptionObjects(copy, image => {
|
|
78
|
+
const id = imageDescriptionImageId(image);
|
|
79
|
+
const delivery = id ? source(id) : undefined;
|
|
80
|
+
if (delivery) {
|
|
81
|
+
const album = "caption" in image && "urls" in image;
|
|
82
|
+
const field = "altText" in image || album ? "altText" : "alt";
|
|
83
|
+
const contextual = text(image[field])
|
|
84
|
+
?? (album && image.descriptionMode == null ? text(image.caption) : undefined) ?? "";
|
|
85
|
+
const mode = placementMode(text(image.descriptionMode), contextual);
|
|
86
|
+
image.descriptionMode = mode;
|
|
87
|
+
image[field] = resolveImageDescription(mode, contextual, delivery.defaultDescription ?? "");
|
|
88
|
+
metadata(image, delivery);
|
|
89
|
+
}
|
|
90
|
+
const nested = image.image;
|
|
91
|
+
if (!("altText" in image || "descriptionMode" in image || "overlayText" in image)
|
|
92
|
+
|| !nested || typeof nested !== "object" || Array.isArray(nested))
|
|
93
|
+
return;
|
|
94
|
+
const bannerImage = nested;
|
|
95
|
+
const bannerId = imageDescriptionImageId(bannerImage);
|
|
96
|
+
const bannerSource = bannerId ? source(bannerId) : undefined;
|
|
97
|
+
if (!bannerSource)
|
|
98
|
+
return;
|
|
99
|
+
const contextual = text(image.altText) ?? "";
|
|
100
|
+
const mode = placementMode(text(image.descriptionMode) ?? text(bannerImage.descriptionMode), contextual);
|
|
101
|
+
image.descriptionMode = mode;
|
|
102
|
+
image.altText = resolveImageDescription(mode, contextual, bannerSource.defaultDescription ?? "");
|
|
103
|
+
metadata(image, bannerSource);
|
|
104
|
+
bannerImage.descriptionMode = mode;
|
|
105
|
+
bannerImage.alt = image.altText;
|
|
106
|
+
});
|
|
107
|
+
return copy;
|
|
108
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Generated from shared/image-description-validation.ts by scripts/sync-field-validation.mjs. Do not edit.
|
|
2
|
+
import registry from "./free-form-section-registry.json" with { type: "json" };
|
|
3
|
+
const definition = registry.imageDescription;
|
|
4
|
+
export const IMAGE_DESCRIPTION_MODES = Object.freeze([...definition.modes]);
|
|
5
|
+
export const IMAGE_DESCRIPTION_KEYS = Object.freeze(Object.keys(definition.fields));
|
|
6
|
+
const fields = Object.entries(definition.fields);
|
|
7
|
+
export const IMAGE_DESCRIPTION_PERSISTED_KEYS = Object.freeze(fields.filter(([, field]) => field.storage === "persisted").map(([key]) => key));
|
|
8
|
+
export const IMAGE_DESCRIPTION_HYDRATED_KEYS = Object.freeze(fields.filter(([, field]) => field.storage === "hydratedReadOnly").map(([key]) => key));
|
|
9
|
+
export function isImageDescriptionMode(value) {
|
|
10
|
+
return value === undefined || value === null || IMAGE_DESCRIPTION_MODES.includes(value);
|
|
11
|
+
}
|
|
12
|
+
export function isImageDefaultDescription(value) {
|
|
13
|
+
return value === undefined || value === null || typeof value === "string"
|
|
14
|
+
&& !/[\uD800-\uDFFF]/u.test(value) && [...value].length <= definition.maxDefaultDescriptionScalars;
|
|
15
|
+
}
|
|
16
|
+
/** Decimal strings retain int64 precision across JSON and JavaScript. */
|
|
17
|
+
export function normalizeImageDescriptionRevision(value) {
|
|
18
|
+
if (typeof value === "number")
|
|
19
|
+
return Number.isSafeInteger(value) && value >= 0 ? String(value) : undefined;
|
|
20
|
+
if (typeof value !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(value))
|
|
21
|
+
return undefined;
|
|
22
|
+
const maximum = definition.maxRevision;
|
|
23
|
+
return value.length < maximum.length || value.length === maximum.length && value <= maximum ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
export function isImageDescriptionRevision(value) {
|
|
26
|
+
return value === undefined || value === null || normalizeImageDescriptionRevision(value) !== undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Editor controls repair unknown choices; delivery validation reports them instead. */
|
|
29
|
+
export function imageDescriptionMode(mode, contextual) {
|
|
30
|
+
return typeof mode === "string" && IMAGE_DESCRIPTION_MODES.includes(mode)
|
|
31
|
+
? mode
|
|
32
|
+
: contextual?.trim()
|
|
33
|
+
? "custom"
|
|
34
|
+
: "inherit";
|
|
35
|
+
}
|
|
36
|
+
export function resolveImageDescription(mode, contextual, fallback) {
|
|
37
|
+
switch (mode) {
|
|
38
|
+
case "decorative":
|
|
39
|
+
return "";
|
|
40
|
+
case "inherit":
|
|
41
|
+
return fallback;
|
|
42
|
+
case "custom":
|
|
43
|
+
return contextual;
|
|
44
|
+
default:
|
|
45
|
+
return contextual.trim() ? contextual : fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const validators = {
|
|
49
|
+
mode: { accepts: isImageDescriptionMode, expected: `be ${IMAGE_DESCRIPTION_MODES.join(", ")}, or null` },
|
|
50
|
+
text: {
|
|
51
|
+
accepts: isImageDefaultDescription,
|
|
52
|
+
expected: "be valid Unicode text within the registry's scalar limit, or null",
|
|
53
|
+
},
|
|
54
|
+
revision: {
|
|
55
|
+
accepts: isImageDescriptionRevision,
|
|
56
|
+
expected: "be a nonnegative safe integer or canonical int64 decimal string, or null",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
export function imageDescriptionIssues(value) {
|
|
60
|
+
return fields.flatMap(([field, contract]) => {
|
|
61
|
+
const validator = validators[contract.kind];
|
|
62
|
+
return validator.accepts(value[field])
|
|
63
|
+
? []
|
|
64
|
+
: [{ field, storage: contract.storage, expected: validator.expected }];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function pickImageDescriptionFields(value, storage) {
|
|
68
|
+
return Object.fromEntries(fields.filter(([key, field]) => (!storage || field.storage === storage)
|
|
69
|
+
&& Object.prototype.hasOwnProperty.call(value, key)).map(([key]) => [key, value[key]]));
|
|
70
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import freeFormSectionRegistry from "./free-form-section-registry.json" with { type: "json" };
|
|
11
|
+
import { imageDescriptionIssues } from "./image-description-validation.js";
|
|
11
12
|
import {
|
|
12
13
|
explicitImageMaxHeight,
|
|
13
14
|
mediaOverrideStyleParts,
|
|
@@ -820,11 +821,15 @@ function renderSection(
|
|
|
820
821
|
}
|
|
821
822
|
|
|
822
823
|
type NormalizedSectionImage = {
|
|
824
|
+
imageId: string;
|
|
823
825
|
src: string;
|
|
824
826
|
urls: Array<{ minWidth: number; url: string }>;
|
|
825
827
|
width?: number;
|
|
826
828
|
height?: number;
|
|
827
829
|
alt?: string;
|
|
830
|
+
descriptionMode?: string | null;
|
|
831
|
+
defaultDescription?: string | null;
|
|
832
|
+
descriptionRevision?: number | string | null;
|
|
828
833
|
};
|
|
829
834
|
|
|
830
835
|
type NormalizedSectionBackground = {
|
|
@@ -912,12 +917,17 @@ function normalizeSectionImage(
|
|
|
912
917
|
|| (hasOwn(value, "height") && !isPositiveInteger(value.height))
|
|
913
918
|
|| (hasOwn(value, "alt") && typeof value.alt !== "string")
|
|
914
919
|
) return undefined;
|
|
920
|
+
if (imageDescriptionIssues(value).length > 0) return undefined;
|
|
915
921
|
return {
|
|
922
|
+
imageId: value.imageId as string,
|
|
916
923
|
src,
|
|
917
924
|
urls: candidates,
|
|
918
925
|
width: isPositiveInteger(value.width) ? value.width : undefined,
|
|
919
926
|
height: isPositiveInteger(value.height) ? value.height : undefined,
|
|
920
927
|
alt: typeof value.alt === "string" ? value.alt : undefined,
|
|
928
|
+
descriptionMode: value.descriptionMode as string | null | undefined,
|
|
929
|
+
defaultDescription: value.defaultDescription as string | null | undefined,
|
|
930
|
+
descriptionRevision: value.descriptionRevision as number | string | null | undefined,
|
|
921
931
|
};
|
|
922
932
|
}
|
|
923
933
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IMAGE_DESCRIPTION_MODES } from "./image-description-validation.js";
|
|
1
2
|
import { validateComponentBlock } from "./components.js";
|
|
2
3
|
import {
|
|
3
4
|
FREE_FORM_SECTION_REGISTRY,
|
|
@@ -180,6 +181,7 @@ const IMAGE_ATTRS = Object.freeze({
|
|
|
180
181
|
),
|
|
181
182
|
urls: URLS_SPEC,
|
|
182
183
|
alt: optionalString(),
|
|
184
|
+
descriptionMode: optionalEnum(IMAGE_DESCRIPTION_MODES),
|
|
183
185
|
width: optionalPositiveInteger(),
|
|
184
186
|
height: optionalPositiveInteger(),
|
|
185
187
|
crop: CROP_SPEC,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { normalizeImageDescriptionRevision } from "./content/image-description-validation.js";
|
|
2
|
+
import { LIMITS } from "./constants.js";
|
|
3
|
+
import { hasControlCharacter, SiteAuthoringError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
export const IMAGE_LIST_DEFAULT_PAGE_SIZE = 25;
|
|
6
|
+
export const IMAGE_LIST_MAXIMUM_PAGE_SIZE = 100;
|
|
7
|
+
const IMAGE_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u;
|
|
8
|
+
|
|
9
|
+
export function isImageId(value) {
|
|
10
|
+
return typeof value === "string" && IMAGE_ID.test(value) && value !== "00000000-0000-0000-0000-000000000000";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function requireImageId(value) {
|
|
14
|
+
if (!isImageId(value)) {
|
|
15
|
+
throw new SiteAuthoringError("media.image_id_invalid", "Name exactly one canonical lowercase image UUID.", {
|
|
16
|
+
field: "imageId", exitCode: 2,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { normalizeImageDescriptionRevision as imageDescriptionRevision } from "./content/image-description-validation.js";
|
|
23
|
+
|
|
24
|
+
export function imageListOptions(invocation = {}) {
|
|
25
|
+
const pageSize = invocation.pageSize === undefined ? IMAGE_LIST_DEFAULT_PAGE_SIZE : Number(invocation.pageSize);
|
|
26
|
+
if ((invocation.pageSize !== undefined && !["number", "string"].includes(typeof invocation.pageSize))
|
|
27
|
+
|| !Number.isInteger(pageSize) || pageSize < 1 || pageSize > IMAGE_LIST_MAXIMUM_PAGE_SIZE
|
|
28
|
+
|| (typeof invocation.pageSize === "string" && !/^[1-9][0-9]*$/u.test(invocation.pageSize))) {
|
|
29
|
+
throw new SiteAuthoringError("media.image_page_size_invalid", "--page-size must be a whole number from 1 to 100.", {
|
|
30
|
+
field: "pageSize", exitCode: 2,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const pageToken = invocation.pageToken ?? "";
|
|
34
|
+
if (typeof pageToken !== "string" || !pageToken.isWellFormed()
|
|
35
|
+
|| Buffer.byteLength(pageToken, "utf8") > LIMITS.configPathBytes) {
|
|
36
|
+
throw new SiteAuthoringError("media.image_page_token_invalid", "--page-token must be the cursor from the previous result.", {
|
|
37
|
+
field: "pageToken", exitCode: 2,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return { pageSize, pageToken, missingDescription: invocation.missingDescription === true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function imageDescriptionOptions(invocation = {}) {
|
|
44
|
+
const hasDescription = invocation.imageDescription !== undefined;
|
|
45
|
+
const clear = invocation.clearDescription === true;
|
|
46
|
+
if (hasDescription === clear) {
|
|
47
|
+
throw new SiteAuthoringError("media.image_description_choice", "Supply exactly one of --description or --clear.", {
|
|
48
|
+
field: "description", exitCode: 2,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const description = clear ? "" : invocation.imageDescription;
|
|
52
|
+
if (typeof description !== "string" || !description.isWellFormed()
|
|
53
|
+
|| Buffer.byteLength(description, "utf8") > LIMITS.configBytes) {
|
|
54
|
+
throw new SiteAuthoringError("media.image_description_invalid", "--description needs bounded, valid Unicode text.", {
|
|
55
|
+
field: "description", exitCode: 2,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const expectedRevision = normalizeImageDescriptionRevision(invocation.expectedRevision);
|
|
59
|
+
if (expectedRevision === undefined) {
|
|
60
|
+
throw new SiteAuthoringError("media.image_revision_required",
|
|
61
|
+
"Supply --expected-revision from the image's list/get result, as a nonnegative int64 decimal integer.", {
|
|
62
|
+
field: "expectedRevision", exitCode: 2,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const onlyIfMissing = invocation.onlyIfMissing === true;
|
|
66
|
+
if (onlyIfMissing && description.trim() === "") {
|
|
67
|
+
throw new SiteAuthoringError("media.image_condition_clear", "--if-missing cannot clear a description.", {
|
|
68
|
+
field: "onlyIfMissing", exitCode: 2,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { defaultDescription: description, expectedRevision, onlyIfMissing };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function safeImagePreviewUrl(value) {
|
|
75
|
+
if (typeof value !== "string" || !value.isWellFormed() || value.length > LIMITS.configPathBytes
|
|
76
|
+
|| hasControlCharacter(value) || /[\s\\]/u.test(value)) return "";
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(value);
|
|
79
|
+
return url.protocol === "https:" && url.username === "" && url.password === ""
|
|
80
|
+
&& url.search === "" && url.hash === "" ? url.href : "";
|
|
81
|
+
} catch {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const READY = "NAVIGATION_PUBLISHING_READINESS_STATE_READY";
|
|
2
|
+
const BLOCKED = "NAVIGATION_PUBLISHING_READINESS_STATE_BLOCKED";
|
|
3
|
+
const UNKNOWN = "NAVIGATION_PUBLISHING_READINESS_STATE_UNKNOWN";
|
|
4
|
+
const REASON_PREFIX = "NAVIGATION_PUBLISHING_READINESS_BLOCKER_REASON_";
|
|
5
|
+
const MESSAGES = {
|
|
6
|
+
PAGE_NOT_INCLUDED: "Include this approved page in the release.",
|
|
7
|
+
PAGE_NOT_APPROVED: "Approve this page and include it in the release.",
|
|
8
|
+
FOLDER_NEEDS_RELEASED_CONTENT: "Publish a qualifying page under this path, or approve and include one in this release.",
|
|
9
|
+
TARGET_UNAVAILABLE: "Remove or replace this unavailable navigation target.",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function normalizeNavigationReadiness(value) {
|
|
13
|
+
return {
|
|
14
|
+
state: [READY, BLOCKED].includes(value?.state) ? value.state : UNKNOWN,
|
|
15
|
+
blockers: (Array.isArray(value?.blockers) ? value.blockers : []).map((blocker) => ({
|
|
16
|
+
resourceId: typeof blocker?.resourceId === "string" ? blocker.resourceId : "",
|
|
17
|
+
pageId: typeof blocker?.pageId === "string" ? blocker.pageId : "",
|
|
18
|
+
title: typeof blocker?.title === "string" ? blocker.title : "",
|
|
19
|
+
path: typeof blocker?.path === "string" ? blocker.path : "",
|
|
20
|
+
reason: typeof blocker?.reason === "string" ? blocker.reason : "",
|
|
21
|
+
})),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isNavigationReady(navigation) {
|
|
26
|
+
return navigation.state === READY;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function describeNavigationReadiness(navigation) {
|
|
30
|
+
if (isNavigationReady(navigation)) return "Navigation is ready.";
|
|
31
|
+
if (navigation.state !== BLOCKED) return "Navigation could not be checked. Try the readiness check again.";
|
|
32
|
+
const details = navigation.blockers.slice(0, 20).map((blocker) => {
|
|
33
|
+
const target = blocker.title || "Unavailable page";
|
|
34
|
+
const path = blocker.pageId ? ` (/${blocker.path.replace(/^\/+/, "")})` : "";
|
|
35
|
+
const message = MESSAGES[blocker.reason.replace(REASON_PREFIX, "")] || MESSAGES.TARGET_UNAVAILABLE;
|
|
36
|
+
return `${target}${path}: ${message}`;
|
|
37
|
+
});
|
|
38
|
+
return details.length ? `Navigation is blocked. ${details.join(" ")}` : "Navigation cannot be published with this set.";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function reportNavigationReadiness(navigation) {
|
|
42
|
+
return {
|
|
43
|
+
state: navigation.state,
|
|
44
|
+
blockers: navigation.blockers.slice(0, 20),
|
|
45
|
+
...(navigation.blockers.length > 20 ? { blockersTruncated: true } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/output.js
CHANGED
|
@@ -159,6 +159,14 @@ export async function writeGithubActionsOutput(outputPath, result) {
|
|
|
159
159
|
const safeResult = structuredClone(result);
|
|
160
160
|
if (safeResult.verb === "preview page") delete safeResult.url;
|
|
161
161
|
if (safeResult.stagingPreview) delete safeResult.stagingPreview.url;
|
|
162
|
+
if (["media images list", "media images get", "media images describe"].includes(safeResult.verb)) {
|
|
163
|
+
const images = safeResult.image ? [safeResult.image] : safeResult.images?.items ?? [];
|
|
164
|
+
for (const image of images) {
|
|
165
|
+
delete image.previewUrl;
|
|
166
|
+
delete image.responsivePreviewUrls;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const image of safeResult.siteHealth?.imageFindings ?? []) delete image.previewUrl;
|
|
162
170
|
const json = requireBoundedJson(safeResult);
|
|
163
171
|
const delimiter = githubDelimiter(json);
|
|
164
172
|
const lines = [
|
package/src/page-metadata.js
CHANGED
|
@@ -4,9 +4,9 @@ import { boundedByBytes } from "./session.js";
|
|
|
4
4
|
import { displayPagePath } from "./workspace.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* is
|
|
7
|
+
* Description length advice is shared with the document rules. The separate
|
|
8
|
+
* descriptionWarnings output stays compatible; the 1000-character storage
|
|
9
|
+
* maximum is still enforced here and by the API (TR01194).
|
|
10
10
|
*/
|
|
11
11
|
export { DESCRIPTION_WARN_LENGTH };
|
|
12
12
|
export const DESCRIPTION_MAXIMUM_LENGTH = 1000;
|
|
@@ -20,27 +20,27 @@ export function requireDescriptionLength(description, field = "description") {
|
|
|
20
20
|
throw new SiteAuthoringError(
|
|
21
21
|
"pages.description_too_long",
|
|
22
22
|
`The description is ${length(description)} characters; it may be at most ${DESCRIPTION_MAXIMUM_LENGTH} `
|
|
23
|
-
+ `(
|
|
23
|
+
+ `(the writing guidance is at most ${DESCRIPTION_WARN_LENGTH}).`,
|
|
24
24
|
{ field },
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
29
|
+
/** The compatible length-warning output for known author-entered descriptions. */
|
|
30
30
|
export function descriptionWarnings(pages, onProgress) {
|
|
31
31
|
const items = [];
|
|
32
32
|
for (const page of pages ?? []) {
|
|
33
|
-
const description = page.declaredDescription ?? page.description;
|
|
34
|
-
if (typeof description !== "string" || length(description) <= DESCRIPTION_WARN_LENGTH) continue;
|
|
33
|
+
const description = page.metadataDescriptionKnown === false ? undefined : page.declaredDescription ?? page.description;
|
|
34
|
+
if (typeof description !== "string" || length(description.trim()) <= DESCRIPTION_WARN_LENGTH) continue;
|
|
35
35
|
const item = {
|
|
36
36
|
code: PAGE_DESCRIPTION_LONG,
|
|
37
37
|
file: page.file,
|
|
38
38
|
path: displayPagePath(page.pagePath ?? page.path),
|
|
39
|
-
length: length(description),
|
|
39
|
+
length: length(description.trim()),
|
|
40
40
|
};
|
|
41
41
|
onProgress(
|
|
42
|
-
`Warning: ${item.file}'s description is ${item.length} characters;
|
|
43
|
-
+
|
|
42
|
+
`Warning: ${item.file}'s description is ${item.length} characters; consider shortening it `
|
|
43
|
+
+ `(guidance: at most ${DESCRIPTION_WARN_LENGTH}).`,
|
|
44
44
|
);
|
|
45
45
|
items.push(item);
|
|
46
46
|
}
|