@taprootio/site-authoring 0.6.3 → 0.7.1
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 +103 -10
- package/package.json +2 -2
- package/src/api.js +281 -61
- package/src/cli.js +73 -26
- package/src/constants.js +2 -1
- package/src/content/components.js +7 -33
- package/src/content/free-form-section-registry.json +4 -2
- package/src/content/inline-facts.js +6 -0
- package/src/content/tiptap-prosemirror.ts +3 -4
- package/src/content/vocabulary.js +6 -2
- package/src/errors.js +27 -0
- package/src/fixture-contract.js +4 -4
- package/src/fixture-init.js +231 -0
- package/src/output.js +12 -2
- package/src/reference-help.js +139 -89
- package/src/staging-check.js +145 -0
- package/src/theme-validation.js +31 -12
- package/src/transport.js +12 -3
- package/src/verbs/deploy.js +35 -101
- package/src/verbs/index.js +4 -1
- package/src/verbs/pull.js +22 -5
- package/src/verbs/redirects-check.js +18 -0
- package/src/verbs/status.js +34 -17
- package/src/verbs/theme-push.js +44 -9
- package/src/verbs/validate.js +51 -12
package/src/reference-help.js
CHANGED
|
@@ -2,6 +2,14 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import { TEMPLATE_TYPE_FREE_FORM } from "./api.js";
|
|
4
4
|
import { CLI_BINARY_NAME } from "./constants.js";
|
|
5
|
+
import {
|
|
6
|
+
canonicalizeComponentData,
|
|
7
|
+
COMPONENT_TYPES,
|
|
8
|
+
getComponentDefinition,
|
|
9
|
+
getComponentPropertyReference,
|
|
10
|
+
} from "./content/components.js";
|
|
11
|
+
import { FREE_FORM_SECTION_REGISTRY } from "./content/free-form-sections.js";
|
|
12
|
+
import { CONTENT_ERROR_CODES, CONTENT_LIMITS, MARK_TYPES, NODE_TYPES } from "./content/vocabulary.js";
|
|
5
13
|
import {
|
|
6
14
|
FIXTURE_CONTRACT_VERSION,
|
|
7
15
|
FIXTURE_DELIVERY_ORIGIN_DOMAIN,
|
|
@@ -13,6 +21,12 @@ import {
|
|
|
13
21
|
SHIPPED_FIXTURE_NAME,
|
|
14
22
|
shippedFixtureDirectory,
|
|
15
23
|
} from "./fixture-contract.js";
|
|
24
|
+
import {
|
|
25
|
+
formatPresentationReference,
|
|
26
|
+
getAppearanceReference,
|
|
27
|
+
getFooterReference,
|
|
28
|
+
getThemeReference,
|
|
29
|
+
} from "./presentation-reference.js";
|
|
16
30
|
import {
|
|
17
31
|
DEFAULT_REDIRECT_STATUS,
|
|
18
32
|
GONE_STATUS,
|
|
@@ -21,25 +35,6 @@ import {
|
|
|
21
35
|
REDIRECTS_FILE_NAME,
|
|
22
36
|
} from "./redirects-contract.js";
|
|
23
37
|
import { SETTINGS_GROUPS } from "./settings-catalog.js";
|
|
24
|
-
import {
|
|
25
|
-
canonicalizeComponentData,
|
|
26
|
-
COMPONENT_TYPES,
|
|
27
|
-
getComponentDefinition,
|
|
28
|
-
getComponentPropertyReference,
|
|
29
|
-
} from "./content/components.js";
|
|
30
|
-
import { FREE_FORM_SECTION_REGISTRY } from "./content/free-form-sections.js";
|
|
31
|
-
import {
|
|
32
|
-
CONTENT_ERROR_CODES,
|
|
33
|
-
CONTENT_LIMITS,
|
|
34
|
-
MARK_TYPES,
|
|
35
|
-
NODE_TYPES,
|
|
36
|
-
} from "./content/vocabulary.js";
|
|
37
|
-
import {
|
|
38
|
-
formatPresentationReference,
|
|
39
|
-
getAppearanceReference,
|
|
40
|
-
getFooterReference,
|
|
41
|
-
getThemeReference,
|
|
42
|
-
} from "./presentation-reference.js";
|
|
43
38
|
import {
|
|
44
39
|
INTERNAL_PAGE_BASELINE_DIRECTORY,
|
|
45
40
|
MANIFEST_VERSION,
|
|
@@ -48,15 +43,15 @@ import {
|
|
|
48
43
|
PAGE_SOURCE_EXTENSIONS,
|
|
49
44
|
PAGE_WORKSPACE_MODE_EDITABLE,
|
|
50
45
|
PAGE_WORKSPACE_MODE_READ_ONLY,
|
|
51
|
-
pageSourceFormat,
|
|
52
46
|
PAGES_DIRECTORY,
|
|
47
|
+
pageSourceFormat,
|
|
53
48
|
SETTINGS_DIRECTORY,
|
|
54
49
|
SYSTEM_PAGE_NOT_FOUND_PATH,
|
|
55
50
|
} from "./workspace.js";
|
|
56
51
|
|
|
57
52
|
export { getAppearanceReference, getFooterReference, getThemeReference };
|
|
58
53
|
|
|
59
|
-
export const REFERENCE_VERSION =
|
|
54
|
+
export const REFERENCE_VERSION = 20;
|
|
60
55
|
export const PAGE_TYPES = Object.freeze(["free-form"]);
|
|
61
56
|
export const REFERENCE_TOPICS = Object.freeze([
|
|
62
57
|
Object.freeze({ name: "pages", usage: `${CLI_BINARY_NAME} help pages`, summary: "List authorable page types." }),
|
|
@@ -139,12 +134,21 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
139
134
|
}),
|
|
140
135
|
redirects: Object.freeze({
|
|
141
136
|
title: "Redirect map workspace contract",
|
|
142
|
-
summary:
|
|
143
|
-
`${REDIRECTS_FILE_NAME} is the site's whole redirect map. 'redirects push' replaces it, fenced by the `
|
|
137
|
+
summary: `${REDIRECTS_FILE_NAME} is the site's whole redirect map. 'redirects push' replaces it, fenced by the `
|
|
144
138
|
+ "revision 'redirects pull' recorded, so a stale write is refused rather than deleting an entry a page "
|
|
145
139
|
+ "rename added.",
|
|
146
|
-
usage: `${CLI_BINARY_NAME} redirects pull | ${CLI_BINARY_NAME} redirects push`,
|
|
140
|
+
usage: `${CLI_BINARY_NAME} redirects pull | ${CLI_BINARY_NAME} redirects push | ${CLI_BINARY_NAME} redirects check`,
|
|
147
141
|
details: Object.freeze([
|
|
142
|
+
"Run 'redirects check' after staging, or read deploy --staging's automatic check. The Node CLI consumes "
|
|
143
|
+
+ "a separate staging handoff and checks the current map at the real edge, using an in-memory cookie and "
|
|
144
|
+
+ "manual redirects. It reports each path, HTTP status and Location without following targets.",
|
|
145
|
+
"verified is true only if every entry matched and the map revision stayed unchanged during the check. "
|
|
146
|
+
+ "The check proves the current map at staging, not a historical deployment. A mismatch can be propagation "
|
|
147
|
+
+ "delay; re-run redirects check before promoting. Requests are bounded to 90 seconds, eight at a time; "
|
|
148
|
+
+ "large JSON lists are truncated explicitly with failed entries first. Human output reports every entry.",
|
|
149
|
+
"stagingPreview.url is a fresh, single-use two-minute handoff, minted after checks. It is emitted only "
|
|
150
|
+
+ "in final JSON, excluded from progress and GITHUB_OUTPUT; keep it private. The staging cookie lasts "
|
|
151
|
+
+ "five minutes and remains subject to current site/host/credential permission.",
|
|
148
152
|
`${REDIRECTS_FILE_NAME} is { siteId, revision, entries }, not a bare array. 'redirects pull' writes it and `
|
|
149
153
|
+ "records the revision in .taproot-site-manifest.json; 'pull' writes it too.",
|
|
150
154
|
"An entry is { path, kind, target, status }. kind is 'redirect' (the default) or 'gone'; a gone entry "
|
|
@@ -239,6 +243,13 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
239
243
|
summary: "Render one persisted draft behind the normal staging gate.",
|
|
240
244
|
usage: `${CLI_BINARY_NAME} preview page <page-path-or-id>`,
|
|
241
245
|
details: Object.freeze([
|
|
246
|
+
"Preview failures carry safe error class, page ID, and node/attribute labels when known. Raw renderer "
|
|
247
|
+
+ "messages and page content are not returned. A deploy refuses a retained failed preview matching a "
|
|
248
|
+
+ "selected page's content until a newer matching preview is ready, the content changes, or the operator "
|
|
249
|
+
+ "explicitly supplies --allow-failed-preview. Pending retries do not clear a known failure; previews "
|
|
250
|
+
+ "remain optional and old snapshots without content identity do not block deployment.",
|
|
251
|
+
"deploy --staging and redirects check return a separate fresh single-use staging handoff in "
|
|
252
|
+
+ "stagingPreview.url, after authenticating headless redirect checks. Handoffs never go to GITHUB_OUTPUT.",
|
|
242
253
|
"A page path resolves through .taproot-site-manifest.json; a canonical page UUID works directly.",
|
|
243
254
|
"The homepage's manifest path is empty; address it as '/', which resolves to that empty root path.",
|
|
244
255
|
"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.",
|
|
@@ -259,8 +270,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
259
270
|
usage: `${CLI_BINARY_NAME} validate <fixture-directory>`,
|
|
260
271
|
details: Object.freeze([
|
|
261
272
|
`Required root fields: ${FIXTURE_REQUIRED_ROOT_FIELDS.join(", ")}.`,
|
|
262
|
-
`
|
|
263
|
-
+ "
|
|
273
|
+
`Optional root fields: ${FIXTURE_OPTIONAL_ROOT_FIELDS.join(", ")}. Appearance and footer metadata, when present, `
|
|
274
|
+
+ "must match the local settings. Snapshot times and deployments are not live evidence. Other root fields are refused.",
|
|
264
275
|
`manifestVersion must be ${MANIFEST_VERSION}; fixture.contractVersion must be ${FIXTURE_CONTRACT_VERSION}.`,
|
|
265
276
|
"siteId, every pageId and resourceId, every settings entityId, and every fixture.imageIds entry is a canonical "
|
|
266
277
|
+ "lowercase UUID. They are deterministic fixture identities, not identities from a live site.",
|
|
@@ -295,6 +306,16 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
295
306
|
+ "with no duplicates. Fixtures are copied and shipped, so a real delivery host in one would be a live "
|
|
296
307
|
+ "reference in every copy.",
|
|
297
308
|
"validate reads the fixture and writes nothing to it. Copy the directory somewhere writable before editing it.",
|
|
309
|
+
"validate --init <new-directory> exports the current pulled workspace as a validated version-6 fixture with "
|
|
310
|
+
+ "appearance and footer metadata. Run from the workspace or its configured project; --config may select the source.",
|
|
311
|
+
"Initialization keeps editable free-form pages as ProseMirror sources and reports excluded metadata/read-only pages. "
|
|
312
|
+
+ "References to excluded pages must be resolved before the fixture can validate. It never copies credentials, "
|
|
313
|
+
+ "internal reconciliation state, or deployment receipts. UUIDs and HTTP(S) origins are replaced with deterministic "
|
|
314
|
+
+ "fixture identities and example.test origins; URL credentials, queries and fragments are removed. Authored prose is retained.",
|
|
315
|
+
"The destination must be outside the source workspace and must not exist; its parent must already exist without symlinks. Exports are bounded to 64 MiB. "
|
|
316
|
+
+ "The source is unchanged; a failed fixture validation removes the newly created output.",
|
|
317
|
+
"Missing required settings are reported together in error.details with field paths and the pull remedy, "
|
|
318
|
+
+ "separately from present-but-invalid values.",
|
|
298
319
|
]),
|
|
299
320
|
}),
|
|
300
321
|
});
|
|
@@ -416,32 +437,40 @@ function registryFieldReference(name, definition) {
|
|
|
416
437
|
minItems: definition.urls.minItems,
|
|
417
438
|
maxItems: definition.urls.maxItems,
|
|
418
439
|
itemAdditionalProperties: !definition.urls.closedItems,
|
|
419
|
-
fields: Object.freeze(
|
|
420
|
-
|
|
421
|
-
|
|
440
|
+
fields: Object.freeze(
|
|
441
|
+
Object.entries(definition.urls.fields).map(([fieldName, field]) =>
|
|
442
|
+
registryFieldReference(fieldName, field)
|
|
443
|
+
),
|
|
444
|
+
),
|
|
422
445
|
}),
|
|
423
446
|
}
|
|
424
447
|
: {}),
|
|
425
448
|
...(definition.srcMustMatchUrls === true ? { srcMustMatchUrls: true } : {}),
|
|
426
449
|
...(definition.fields
|
|
427
450
|
? {
|
|
428
|
-
fields: Object.freeze(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
451
|
+
fields: Object.freeze(
|
|
452
|
+
Object.entries(definition.fields).map(([fieldName, field]) =>
|
|
453
|
+
Object.freeze({
|
|
454
|
+
...registryFieldReference(fieldName, field),
|
|
455
|
+
...(field.requiredKeys ? { requiredKeys: [...field.requiredKeys] } : {}),
|
|
456
|
+
...(field.optionalKeys ? { optionalKeys: [...field.optionalKeys] } : {}),
|
|
457
|
+
...(field.tokenByValue ? { tokenByValue: { ...field.tokenByValue } } : {}),
|
|
458
|
+
})
|
|
459
|
+
),
|
|
460
|
+
),
|
|
434
461
|
}
|
|
435
462
|
: {}),
|
|
436
463
|
});
|
|
437
464
|
}
|
|
438
465
|
|
|
439
466
|
function sectionAttributeReference() {
|
|
440
|
-
return Object.entries(FREE_FORM_SECTION_REGISTRY.section.attrs).map(([name, definition]) =>
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
467
|
+
return Object.entries(FREE_FORM_SECTION_REGISTRY.section.attrs).map(([name, definition]) =>
|
|
468
|
+
Object.freeze({
|
|
469
|
+
...registryFieldReference(name, definition),
|
|
470
|
+
label: definition.label,
|
|
471
|
+
help: definition.help,
|
|
472
|
+
})
|
|
473
|
+
);
|
|
445
474
|
}
|
|
446
475
|
|
|
447
476
|
const SECTION_IMAGE_EXAMPLE = Object.freeze({
|
|
@@ -490,7 +519,9 @@ function backgroundReference() {
|
|
|
490
519
|
compactBehavior:
|
|
491
520
|
"At the shared compact breakpoint, portraitImage and portraitFocus replace the landscape image and focus when supplied.",
|
|
492
521
|
example: BACKGROUND_EXAMPLE,
|
|
493
|
-
markdownExample: `:::section ${
|
|
522
|
+
markdownExample: `:::section ${
|
|
523
|
+
JSON.stringify({ background: BACKGROUND_EXAMPLE })
|
|
524
|
+
}\n## Practice with us\n\nFind your next class.\n:::`,
|
|
494
525
|
});
|
|
495
526
|
}
|
|
496
527
|
|
|
@@ -522,7 +553,9 @@ function decorationReference() {
|
|
|
522
553
|
"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.",
|
|
523
554
|
tintTokens: Object.freeze({ ...DECORATION_DEFINITION.fields.tint.tokenByValue }),
|
|
524
555
|
example: DECORATION_EXAMPLE,
|
|
525
|
-
markdownExample: `:::section ${
|
|
556
|
+
markdownExample: `:::section ${
|
|
557
|
+
JSON.stringify({ decoration: DECORATION_EXAMPLE })
|
|
558
|
+
}\n## Rooted in warmth\n\nMove with confidence.\n:::`,
|
|
526
559
|
maskGuidance:
|
|
527
560
|
"Use a transparent PNG or WebP whose alpha channel contains only the mark. The active section context supplies the tint in both schemes.",
|
|
528
561
|
opaqueWarning:
|
|
@@ -562,18 +595,20 @@ function inlineFactsReference() {
|
|
|
562
595
|
}),
|
|
563
596
|
}),
|
|
564
597
|
placement: Object.freeze({ ...INLINE_FACTS_DEFINITION.placement }),
|
|
565
|
-
valuePolicy:
|
|
566
|
-
|
|
567
|
-
labelPolicy:
|
|
568
|
-
`Optional non-whitespace plain text; maximum ${items.fields.label.maxScalars} Unicode scalar values. `
|
|
598
|
+
valuePolicy: `Required non-whitespace plain text; maximum ${items.fields.value.maxScalars} Unicode scalar values. `
|
|
599
|
+
+ "Unicode spaces, tabs, and LF/CRLF line breaks are allowed. Line breaks display on separate lines inside the value's link. Other C0/C1 controls, U+2028/U+2029, U+FEFF, and literal <br> tags are rejected.",
|
|
600
|
+
labelPolicy: `Optional non-whitespace plain text; maximum ${items.fields.label.maxScalars} Unicode scalar values. `
|
|
569
601
|
+ "Omit it when the value already names itself, such as a rating, an address, or a phone number; a row may "
|
|
570
|
-
+ "mix labelled and standalone facts. An empty string is rejected, so omitting a label stays explicit."
|
|
602
|
+
+ "mix labelled and standalone facts. An empty string is rejected, so omitting a label stays explicit. "
|
|
603
|
+
+ "Unicode spaces, tabs, and LF/CRLF line breaks are allowed; other C0/C1 controls, U+2028/U+2029, U+FEFF, and literal <br> tags are rejected. The label follows the value in reading order and is outside its link.",
|
|
571
604
|
urlPolicy:
|
|
572
|
-
"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.",
|
|
605
|
+
"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. Contact URLs require a body and refuse tel:// and mailto:// authority forms or control characters, consistently with navigation and footer links.",
|
|
573
606
|
markdown: Object.freeze({
|
|
574
607
|
fence: INLINE_FACTS_DEFINITION.markdown.fence,
|
|
575
608
|
body: INLINE_FACTS_DEFINITION.markdown.body,
|
|
576
|
-
example: `\`\`\`${INLINE_FACTS_DEFINITION.markdown.fence}\n${
|
|
609
|
+
example: `\`\`\`${INLINE_FACTS_DEFINITION.markdown.fence}\n${
|
|
610
|
+
JSON.stringify(INLINE_FACT_ITEMS_EXAMPLE, null, 2)
|
|
611
|
+
}\n\`\`\``,
|
|
577
612
|
}),
|
|
578
613
|
examples: Object.freeze({ items: INLINE_FACT_ITEMS_EXAMPLE, proseMirror }),
|
|
579
614
|
});
|
|
@@ -709,7 +744,8 @@ function tableReference() {
|
|
|
709
744
|
captionMustImmediatelyPrecedeHeader: TABLE_DEFINITION.markdown.captionImmediate,
|
|
710
745
|
outerPipes: TABLE_DEFINITION.markdown.outerPipes,
|
|
711
746
|
literalPipeEscape: TABLE_DEFINITION.markdown.literalPipeEscape,
|
|
712
|
-
delimiter:
|
|
747
|
+
delimiter:
|
|
748
|
+
`one or more hyphens per column (minimum ${TABLE_DEFINITION.markdown.delimiterMinHyphens}); no other text`,
|
|
713
749
|
alignment: TABLE_DEFINITION.markdown.alignment,
|
|
714
750
|
blankLineTerminates: TABLE_DEFINITION.markdown.blankLineTerminates,
|
|
715
751
|
}),
|
|
@@ -722,31 +758,38 @@ function tableReference() {
|
|
|
722
758
|
corrections: Object.freeze([
|
|
723
759
|
Object.freeze({
|
|
724
760
|
code: CONTENT_ERROR_CODES.childNotAllowed,
|
|
725
|
-
correction:
|
|
761
|
+
correction:
|
|
762
|
+
"Move a table to the document root or directly into a top-level section; cells contain a paragraph, never another block.",
|
|
726
763
|
}),
|
|
727
764
|
Object.freeze({
|
|
728
765
|
code: CONTENT_ERROR_CODES.attrUnknown,
|
|
729
|
-
correction:
|
|
766
|
+
correction:
|
|
767
|
+
"Keep only the optional caption attribute on table; tableRow, tableHeader, and tableCell have no attributes or appearance controls.",
|
|
730
768
|
}),
|
|
731
769
|
Object.freeze({
|
|
732
770
|
code: CONTENT_ERROR_CODES.tableHeader,
|
|
733
|
-
correction:
|
|
771
|
+
correction:
|
|
772
|
+
"Put exactly one tableRow first, use only tableHeader cells in it, and give every header non-whitespace text.",
|
|
734
773
|
}),
|
|
735
774
|
Object.freeze({
|
|
736
775
|
code: CONTENT_ERROR_CODES.tableShape,
|
|
737
|
-
correction:
|
|
776
|
+
correction:
|
|
777
|
+
"Use tableRow children, tableHeader only in the first row, tableCell after it, and keep tables at root or directly in a section.",
|
|
738
778
|
}),
|
|
739
779
|
Object.freeze({
|
|
740
780
|
code: CONTENT_ERROR_CODES.tableRagged,
|
|
741
|
-
correction:
|
|
781
|
+
correction:
|
|
782
|
+
"Add or remove cells so the delimiter and every data row have exactly the header row's column count.",
|
|
742
783
|
}),
|
|
743
784
|
Object.freeze({
|
|
744
785
|
code: CONTENT_ERROR_CODES.tableBounds,
|
|
745
|
-
correction:
|
|
786
|
+
correction:
|
|
787
|
+
`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.`,
|
|
746
788
|
}),
|
|
747
789
|
Object.freeze({
|
|
748
790
|
code: CONTENT_ERROR_CODES.tableCellContent,
|
|
749
|
-
correction:
|
|
791
|
+
correction:
|
|
792
|
+
"Give every tableHeader and tableCell exactly one paragraph containing only text, hardBreak, and the supported marks/links.",
|
|
750
793
|
}),
|
|
751
794
|
Object.freeze({
|
|
752
795
|
code: CONTENT_ERROR_CODES.tableSpan,
|
|
@@ -754,7 +797,8 @@ function tableReference() {
|
|
|
754
797
|
}),
|
|
755
798
|
Object.freeze({
|
|
756
799
|
code: CONTENT_ERROR_CODES.markdownTable,
|
|
757
|
-
correction:
|
|
800
|
+
correction:
|
|
801
|
+
"Use a pipe header, a matching hyphen-only delimiter, at least one data row, and a blank line before the next block.",
|
|
758
802
|
}),
|
|
759
803
|
Object.freeze({
|
|
760
804
|
code: CONTENT_ERROR_CODES.markdownTableAlignment,
|
|
@@ -1045,12 +1089,15 @@ function formatSectionAttr(attr) {
|
|
|
1045
1089
|
|
|
1046
1090
|
function formatRegistryField(field, indent = " ") {
|
|
1047
1091
|
if (field.type === "processed-image") {
|
|
1048
|
-
return `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))} processed-image${
|
|
1049
|
-
field.
|
|
1050
|
-
}; required
|
|
1092
|
+
return `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))} processed-image${
|
|
1093
|
+
field.nullable ? " | null" : ""
|
|
1094
|
+
}; ${field.required ? "required" : `default ${JSON.stringify(field.default)}`}; required keys ${
|
|
1095
|
+
field.requiredKeys.join(", ")
|
|
1096
|
+
}; optional keys ${
|
|
1051
1097
|
field.optionalKeys.join(", ")
|
|
1052
1098
|
}; responsive candidates ${field.responsiveUrls.minItems} through ${field.responsiveUrls.maxItems}; minWidth positive integer; optional type ${
|
|
1053
|
-
field.responsiveUrls.fields.find((item) => item.name === "type").values.map((value) => JSON.stringify(value))
|
|
1099
|
+
field.responsiveUrls.fields.find((item) => item.name === "type").values.map((value) => JSON.stringify(value))
|
|
1100
|
+
.join(" | ")
|
|
1054
1101
|
}; src must match one candidate; unlisted keys rejected`;
|
|
1055
1102
|
}
|
|
1056
1103
|
const type = field.values ? field.values.map((value) => JSON.stringify(value)).join(" | ") : field.type;
|
|
@@ -1058,10 +1105,12 @@ function formatRegistryField(field, indent = " ") {
|
|
|
1058
1105
|
const itemBounds = field.minItems === undefined ? "" : `; items ${field.minItems} through ${field.maxItems}`;
|
|
1059
1106
|
const scalarBounds = field.maxScalars === undefined
|
|
1060
1107
|
? ""
|
|
1061
|
-
: `; ${field.minScalars} through ${field.maxScalars} Unicode scalar values${
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
}${
|
|
1108
|
+
: `; ${field.minScalars} through ${field.maxScalars} Unicode scalar values${
|
|
1109
|
+
field.nonWhitespace ? "; non-whitespace" : ""
|
|
1110
|
+
}`;
|
|
1111
|
+
const line = `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))} ${type}${
|
|
1112
|
+
field.nullable ? " | null" : ""
|
|
1113
|
+
}; ${field.required ? "required" : `default ${JSON.stringify(field.default)}`}${bounds}${itemBounds}${scalarBounds}`;
|
|
1065
1114
|
return [line, ...(field.fields ?? []).map((child) => formatRegistryField(child, `${indent} `))].join("\n");
|
|
1066
1115
|
}
|
|
1067
1116
|
|
|
@@ -1098,23 +1147,11 @@ export function formatReferenceResult(result) {
|
|
|
1098
1147
|
page.workspace.formats.map((format) => ` ${format.extension.padEnd(9)} ${format.purpose}`).join("\n")
|
|
1099
1148
|
}\n\nOne source per page:\n rule ${sourceRule.rule}\n manifest fields ${
|
|
1100
1149
|
sourceRule.manifestFields.join(", ")
|
|
1101
|
-
}\n pull ${sourceRule.pull}\n internal state ${
|
|
1102
|
-
sourceRule.internalState
|
|
1103
|
-
}\n format change ${sourceRule.formatChange} (${sourceRule.formatChangeError})\n renamed source ${
|
|
1104
|
-
sourceRule.renamedSource
|
|
1105
|
-
} (${sourceRule.renamedSourceError})\n conflicts ${
|
|
1106
|
-
sourceRule.conflict
|
|
1107
|
-
} (${sourceRule.conflictError})\n conflict detail ${
|
|
1108
|
-
sourceRule.conflictDetail
|
|
1109
|
-
}\n push conflicts ${
|
|
1110
|
-
sourceRule.pushConflict
|
|
1111
|
-
} (${sourceRule.pushConflictError})\n revision ${
|
|
1112
|
-
sourceRule.revisionSource
|
|
1113
|
-
}\n recovery ${
|
|
1114
|
-
sourceRule.conflictRecovery
|
|
1115
|
-
}\n push selection ${sourceRule.pushSelection}\n\nSystem page projections:\n${
|
|
1150
|
+
}\n pull ${sourceRule.pull}\n internal state ${sourceRule.internalState}\n format change ${sourceRule.formatChange} (${sourceRule.formatChangeError})\n renamed source ${sourceRule.renamedSource} (${sourceRule.renamedSourceError})\n conflicts ${sourceRule.conflict} (${sourceRule.conflictError})\n conflict detail ${sourceRule.conflictDetail}\n push conflicts ${sourceRule.pushConflict} (${sourceRule.pushConflictError})\n revision ${sourceRule.revisionSource}\n recovery ${sourceRule.conflictRecovery}\n push selection ${sourceRule.pushSelection}\n\nSystem page projections:\n${
|
|
1116
1151
|
page.workspace.systemPages.map((systemPage) =>
|
|
1117
|
-
` ${systemPage.path.padEnd(9)} ${systemPage.mode}; ${systemPage.projection}.\n${
|
|
1152
|
+
` ${systemPage.path.padEnd(9)} ${systemPage.mode}; ${systemPage.projection}.\n${
|
|
1153
|
+
" ".repeat(12)
|
|
1154
|
+
}${systemPage.unchangedPush} ${systemPage.approval}\n${" ".repeat(12)}${systemPage.guidance}`
|
|
1118
1155
|
).join("\n")
|
|
1119
1156
|
}\n home editable; ${page.workspace.systemHome}\n\nMarkdown front matter:\n${
|
|
1120
1157
|
markdownFormat.metadata.map(formatMetadata).join("\n")
|
|
@@ -1127,18 +1164,31 @@ export function formatReferenceResult(result) {
|
|
|
1127
1164
|
}\n\nSection photo background (closed object):\n${
|
|
1128
1165
|
page.sections.background.fields.map((field) => formatRegistryField(field)).join("\n")
|
|
1129
1166
|
}\n delivery URLs ${page.sections.background.deliveryUrlPolicy}\n compact behavior ${page.sections.background.compactBehavior}\n\nScrim opacity mapping:\n${
|
|
1130
|
-
Object.entries(page.sections.background.scrimOpacityByValue).map(([name, opacity]) =>
|
|
1167
|
+
Object.entries(page.sections.background.scrimOpacityByValue).map(([name, opacity]) =>
|
|
1168
|
+
` ${name.padEnd(20)} ${opacity}`
|
|
1169
|
+
).join("\n")
|
|
1131
1170
|
}\n\nResponsive background example:\n${page.sections.background.markdownExample}\n\nSection decoration (closed object):\n${
|
|
1132
1171
|
page.sections.decoration.fields.map((field) => formatRegistryField(field)).join("\n")
|
|
1133
1172
|
}\n delivery URLs ${page.sections.decoration.deliveryUrlPolicy}\n\nTint mapping:\n${
|
|
1134
|
-
Object.entries(page.sections.decoration.tintTokens).map(([name, token]) => ` ${name.padEnd(20)} ${token}`)
|
|
1135
|
-
|
|
1136
|
-
page.sections.decoration.maskGuidance
|
|
1137
|
-
}\nWarning: ${page.sections.decoration.opaqueWarning}\n\nRoot defaults and placement:\n${
|
|
1173
|
+
Object.entries(page.sections.decoration.tintTokens).map(([name, token]) => ` ${name.padEnd(20)} ${token}`)
|
|
1174
|
+
.join("\n")
|
|
1175
|
+
}\n\nTransparent-mask example:\n${page.sections.decoration.markdownExample}\n\nMask guidance: ${page.sections.decoration.maskGuidance}\nWarning: ${page.sections.decoration.opaqueWarning}\n\nRoot defaults and placement:\n${
|
|
1138
1176
|
page.sections.placementMatrix.map(formatPlacement).join("\n")
|
|
1139
|
-
}\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 ${
|
|
1177
|
+
}\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 ${
|
|
1178
|
+
page.tables.structure.inlineNodes.join(", ")
|
|
1179
|
+
}; marks ${
|
|
1180
|
+
page.tables.structure.marks.join(", ")
|
|
1181
|
+
}\n pipes outer pipes ${page.tables.markdown.outerPipes}; write a literal pipe as ${page.tables.markdown.literalPipeEscape}\n spans unsupported: ${
|
|
1182
|
+
page.tables.structure.unsupportedSpanAttrs.join(", ")
|
|
1183
|
+
}\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${
|
|
1184
|
+
JSON.stringify(page.tables.examples.proseMirror, null, 2)
|
|
1185
|
+
}\n\nTable error corrections:\n${
|
|
1140
1186
|
page.tables.corrections.map((entry) => ` ${entry.code.padEnd(34)} ${entry.correction}`).join("\n")
|
|
1141
|
-
}\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 ${
|
|
1187
|
+
}\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 ${
|
|
1188
|
+
page.inlineFacts.attrs.items.itemFieldOrder.join(", ")
|
|
1189
|
+
} order\n value ${page.inlineFacts.valuePolicy}\n label ${page.inlineFacts.labelPolicy}\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${
|
|
1190
|
+
JSON.stringify(page.inlineFacts.examples.proseMirror, null, 2)
|
|
1191
|
+
}\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${
|
|
1142
1192
|
page.components.map((component) => ` ${component.type.padEnd(16)} ${component.summary}`).join("\n")
|
|
1143
1193
|
}\n\nWorkflow:\n${
|
|
1144
1194
|
page.workflow.map((step) => ` ${step.command}: ${step.result}`).join("\n")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { getSiteRedirectMap, mintStagingPreviewHandoff } from "./api.js";
|
|
2
|
+
import { sanitizeDiagnostic, SiteAuthoringError } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
const COOKIE = "__Host-taproot_staging_preview";
|
|
5
|
+
const CHECK = "__taproot_preview_check";
|
|
6
|
+
const TIMEOUT = 5_000;
|
|
7
|
+
const REPORT_BYTES = 28_000;
|
|
8
|
+
|
|
9
|
+
async function request(client, url, cookie, signal) {
|
|
10
|
+
const response = await client.fetch(url, {
|
|
11
|
+
method: "GET",
|
|
12
|
+
redirect: "manual",
|
|
13
|
+
signal: AbortSignal.any([
|
|
14
|
+
client.timeoutSignal(TIMEOUT),
|
|
15
|
+
...(signal ? [signal] : []),
|
|
16
|
+
...(client.signal ? [client.signal] : []),
|
|
17
|
+
]),
|
|
18
|
+
headers: {
|
|
19
|
+
accept: "text/html",
|
|
20
|
+
"user-agent": "taproot-site/staging-redirect-check",
|
|
21
|
+
...(cookie ? { cookie } : {}),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
await response.body?.cancel().catch(() => {});
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function unavailable() {
|
|
29
|
+
return new SiteAuthoringError("staging.gate_unavailable", "The staging gate could not establish authorized access.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Consume only the checker's handoff; never forward a platform bearer or follow a redirect. */
|
|
33
|
+
async function authorize(client, handoff, signal) {
|
|
34
|
+
const response = await request(client, handoff.url, undefined, signal);
|
|
35
|
+
const origin = new URL(handoff.stagingUrl).origin;
|
|
36
|
+
const checkUrl = `${origin}/?${CHECK}=1`;
|
|
37
|
+
const cookies = response.headers.getSetCookie?.() ?? [response.headers.get("set-cookie") ?? ""];
|
|
38
|
+
const matching = cookies.filter((value) => value.startsWith(`${COOKIE}=`));
|
|
39
|
+
if (response.status !== 302 || response.headers.get("location") !== checkUrl || matching.length !== 1) {
|
|
40
|
+
throw unavailable();
|
|
41
|
+
}
|
|
42
|
+
const match = new RegExp(`^${COOKIE}=([A-Za-z0-9_-]{43});`).exec(matching[0]);
|
|
43
|
+
if (!match || !/;\s*Secure(?:;|$)/i.test(matching[0]) || !/;\s*HttpOnly(?:;|$)/i.test(matching[0])) {
|
|
44
|
+
throw unavailable();
|
|
45
|
+
}
|
|
46
|
+
const cookie = `${COOKIE}=${match[1]}`;
|
|
47
|
+
const checked = await request(client, checkUrl, cookie, signal);
|
|
48
|
+
if (checked.status !== 302 || checked.headers.get("location") !== `${origin}/`) throw unavailable();
|
|
49
|
+
return cookie;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function safeLocation(value) {
|
|
53
|
+
if (value === null) return "";
|
|
54
|
+
// The edge could be misconfigured. Never reflect handoff credentials from a
|
|
55
|
+
// redirect response into JSON, stderr or Actions, even if the check fails.
|
|
56
|
+
if (
|
|
57
|
+
value.length > 4096 || /[\u0000-\u001f\u007f-\u009f]/u.test(value)
|
|
58
|
+
|| /__taproot_preview_|[?&]handoff=/iu.test(value)
|
|
59
|
+
) return "[withheld]";
|
|
60
|
+
return sanitizeDiagnostic(value, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Check the current redirect-map revision at the real edge from the Node CLI. */
|
|
64
|
+
export async function checkStagingRedirects(client, siteId, onProgress, now) {
|
|
65
|
+
const map = await getSiteRedirectMap(client, siteId);
|
|
66
|
+
const handoff = await mintStagingPreviewHandoff(client, siteId, { now });
|
|
67
|
+
const signal = client.timeoutSignal(90_000);
|
|
68
|
+
const cookie = await authorize(client, handoff, signal);
|
|
69
|
+
const origin = new URL(handoff.stagingUrl).origin;
|
|
70
|
+
const rows = new Array(map.entries.length);
|
|
71
|
+
let next = 0;
|
|
72
|
+
await Promise.all(Array.from({ length: Math.min(8, map.entries.length) }, async () => {
|
|
73
|
+
while (next < map.entries.length) {
|
|
74
|
+
const index = next++;
|
|
75
|
+
const entry = map.entries[index];
|
|
76
|
+
if (
|
|
77
|
+
!entry.path.startsWith("/") || entry.path.startsWith("//") || entry.path.includes("\\")
|
|
78
|
+
|| /[?#\u0000-\u001f\u007f]/u.test(entry.path)
|
|
79
|
+
) throw unavailable();
|
|
80
|
+
const url = new URL(entry.path, `${origin}/`);
|
|
81
|
+
if (
|
|
82
|
+
url.origin !== origin || url.pathname !== entry.path || url.username || url.password || url.hash
|
|
83
|
+
|| [...url.searchParams.keys()].some((key) => key.startsWith("__taproot_preview_"))
|
|
84
|
+
) throw unavailable();
|
|
85
|
+
let row;
|
|
86
|
+
try {
|
|
87
|
+
const response = await request(client, url.href, cookie, signal);
|
|
88
|
+
const location = safeLocation(response.headers.get("location"));
|
|
89
|
+
const expectedLocation = entry.kind === "gone" ? "" : entry.target;
|
|
90
|
+
// Compare URL semantics while retaining the literal Location evidence.
|
|
91
|
+
const targetMatches = entry.kind === "gone"
|
|
92
|
+
? location === ""
|
|
93
|
+
: location !== "[withheld]" && location !== ""
|
|
94
|
+
&& new URL(location, url).href === new URL(expectedLocation, url).href;
|
|
95
|
+
row = {
|
|
96
|
+
path: entry.path,
|
|
97
|
+
status: response.status,
|
|
98
|
+
location,
|
|
99
|
+
expectedStatus: entry.status,
|
|
100
|
+
matches: response.status === entry.status && targetMatches,
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
row = {
|
|
104
|
+
path: entry.path,
|
|
105
|
+
status: 0,
|
|
106
|
+
location: "",
|
|
107
|
+
expectedStatus: entry.status,
|
|
108
|
+
matches: false,
|
|
109
|
+
error: "request_failed",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
rows[index] = row;
|
|
113
|
+
onProgress(
|
|
114
|
+
`${sanitizeDiagnostic(row.path)} → ${row.status || "unavailable"} → ${row.location || "(no Location)"}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}));
|
|
118
|
+
const latest = await getSiteRedirectMap(client, siteId);
|
|
119
|
+
const revisionUnchanged = latest.revision === map.revision;
|
|
120
|
+
const failed = rows.filter((row) => !row.matches).length;
|
|
121
|
+
const items = [];
|
|
122
|
+
let bytes = 2;
|
|
123
|
+
// Include failures first when a large map exceeds the JSON output budget.
|
|
124
|
+
for (const row of [...rows.filter((row) => !row.matches), ...rows.filter((row) => row.matches)]) {
|
|
125
|
+
const size = Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
|
|
126
|
+
if (bytes + size > REPORT_BYTES) break;
|
|
127
|
+
items.push(row);
|
|
128
|
+
bytes += size;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
routeCheck: "resolved",
|
|
132
|
+
stagingUrl: handoff.stagingUrl,
|
|
133
|
+
redirects: {
|
|
134
|
+
revision: map.revision,
|
|
135
|
+
revisionUnchanged,
|
|
136
|
+
total: rows.length,
|
|
137
|
+
checked: rows.length,
|
|
138
|
+
matched: rows.length - failed,
|
|
139
|
+
failed,
|
|
140
|
+
verified: revisionUnchanged && failed === 0,
|
|
141
|
+
items,
|
|
142
|
+
...(items.length < rows.length ? { itemsTruncated: true } : {}),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
package/src/theme-validation.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
encodeTheme,
|
|
3
|
-
LIGHTNESS_KEYS,
|
|
4
|
-
SEMANTIC_COLOR_NAMES,
|
|
5
|
-
validateThemePair,
|
|
6
|
-
} from "@taprootio/espalier/shared/theme";
|
|
1
|
+
import { encodeTheme, LIGHTNESS_KEYS, SEMANTIC_COLOR_NAMES, validateThemePair } from "@taprootio/espalier/shared/theme";
|
|
7
2
|
|
|
8
3
|
import { hasControlCharacter, sanitizeDiagnostic, SiteAuthoringError } from "./errors.js";
|
|
9
4
|
|
|
@@ -138,6 +133,30 @@ function hasThemePath(theme, path) {
|
|
|
138
133
|
return true;
|
|
139
134
|
}
|
|
140
135
|
|
|
136
|
+
/** Collect absent contract keys without treating present invalid values as omissions. */
|
|
137
|
+
export function missingThemeFields(theme, scheme) {
|
|
138
|
+
if (!isPlainObject(theme)) return [];
|
|
139
|
+
const fields = REQUIRED_THEME_PROPERTIES
|
|
140
|
+
.filter((property) => !Object.hasOwn(theme, property))
|
|
141
|
+
.map((property) => `${scheme}Theme.${property}`);
|
|
142
|
+
for (const path of REQUIRED_THEME_PATHS) {
|
|
143
|
+
const segments = path.split(".");
|
|
144
|
+
// A missing root property is already listed. A present null, array, or
|
|
145
|
+
// scalar remains a value-validation fault, not a request for a newer key.
|
|
146
|
+
if (!Object.hasOwn(theme, segments[0])) continue;
|
|
147
|
+
let current = theme;
|
|
148
|
+
for (const segment of segments) {
|
|
149
|
+
if (!isPlainObject(current)) break;
|
|
150
|
+
if (!Object.hasOwn(current, segment)) {
|
|
151
|
+
fields.push(`${scheme}Theme.${path}`);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
current = current[segment];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return fields;
|
|
158
|
+
}
|
|
159
|
+
|
|
141
160
|
function requireBoundedOpenMaps(theme, scheme) {
|
|
142
161
|
for (const property of ["anchors", "contexts", "dataRamps"]) {
|
|
143
162
|
const collection = theme[property];
|
|
@@ -155,9 +174,7 @@ function requireBoundedOpenMaps(theme, scheme) {
|
|
|
155
174
|
Object.keys(collection).some((key) => [...key].length > MAXIMUM_THEME_OPEN_MAP_KEY_SCALARS)
|
|
156
175
|
|| Object.values(collection).some((anchor) =>
|
|
157
176
|
isPlainObject(anchor)
|
|
158
|
-
&& Object.keys(anchor).some((key) =>
|
|
159
|
-
key !== "color" && [...key].length > MAXIMUM_THEME_OPEN_MAP_KEY_SCALARS
|
|
160
|
-
)
|
|
177
|
+
&& Object.keys(anchor).some((key) => key !== "color" && [...key].length > MAXIMUM_THEME_OPEN_MAP_KEY_SCALARS)
|
|
161
178
|
)
|
|
162
179
|
)
|
|
163
180
|
) {
|
|
@@ -302,9 +319,11 @@ export function validateAndEncodeThemePair(lightTheme, darkTheme) {
|
|
|
302
319
|
refuseAgentStylesheets(darkTheme, "dark");
|
|
303
320
|
const warnings = result.warnings
|
|
304
321
|
.slice(0, MAXIMUM_THEME_WARNINGS)
|
|
305
|
-
.map((warning) =>
|
|
306
|
-
|
|
307
|
-
|
|
322
|
+
.map((warning) =>
|
|
323
|
+
[...sanitizeDiagnostic(warning, "Theme validation warning.")]
|
|
324
|
+
.slice(0, MAXIMUM_THEME_WARNING_SCALARS)
|
|
325
|
+
.join("")
|
|
326
|
+
);
|
|
308
327
|
return {
|
|
309
328
|
light,
|
|
310
329
|
dark,
|