@elytracms/studio 0.0.92 → 0.0.93
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/convex/asset-alt-migration.test.ts +127 -0
- package/convex/asset-replace.test.ts +168 -0
- package/convex/asset-revalidate.test.ts +6 -6
- package/convex/assets.ts +41 -5
- package/convex/cms.ts +25 -11
- package/convex/contract-parity.test.ts +18 -3
- package/convex/document-revalidate.test.ts +10 -12
- package/convex/orphan-hydration.test.ts +116 -0
- package/convex/rail-write-parity.test.ts +11 -8
- package/convex/schema.ts +6 -0
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.js +4 -2
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -0
- package/dist/components/ai/translate-skipped.js +46 -0
- package/dist/components/ai/translate-skipped.js.map +1 -0
- package/dist/components/assets/asset-detail.js +18 -8
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +15 -4
- package/dist/components/assets/asset-metadata.js +15 -2
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +4 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +0 -8
- package/dist/components/assets/asset-selection.js +4 -23
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +3 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
- package/dist/components/assets/assets-collection-view-source.js +9 -5
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +14 -0
- package/dist/components/assets/assets-store.js +23 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/replace-file.d.ts +22 -0
- package/dist/components/assets/replace-file.js +59 -0
- package/dist/components/assets/replace-file.js.map +1 -0
- package/dist/components/builder/blocks-model.d.ts +14 -26
- package/dist/components/builder/blocks-model.js +35 -41
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +7 -3
- package/dist/components/builder/blocks-panel.js +6 -5
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +104 -15
- package/dist/components/builder/builder-store.js +156 -31
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-body.d.ts +30 -0
- package/dist/components/builder/canvas-body.js +38 -0
- package/dist/components/builder/canvas-body.js.map +1 -0
- package/dist/components/builder/canvas-chrome-css.js +7 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -7
- package/dist/components/builder/canvas-dnd.js +5 -5
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +27 -19
- package/dist/components/builder/canvas-render-context.js +42 -43
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-runs.d.ts +45 -0
- package/dist/components/builder/canvas-runs.js +51 -0
- package/dist/components/builder/canvas-runs.js.map +1 -0
- package/dist/components/builder/canvas.js +32 -5
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +15 -0
- package/dist/components/builder/clipboard-actions.js +44 -7
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +3 -15
- package/dist/components/builder/composition-field-editor.js +29 -40
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/delete-key.d.ts +5 -0
- package/dist/components/builder/delete-key.js +4 -0
- package/dist/components/builder/delete-key.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +17 -25
- package/dist/components/builder/insert-context.js +45 -92
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -3
- package/dist/components/builder/page-tree.js +3 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.d.ts +18 -0
- package/dist/components/builder/placement-refusal.js +46 -0
- package/dist/components/builder/placement-refusal.js.map +1 -0
- package/dist/components/builder/property-inspector.js +4 -0
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +65 -27
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +79 -1
- package/dist/components/builder/reuse.js +69 -2
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/run-inspector.d.ts +11 -0
- package/dist/components/builder/run-inspector.js +33 -0
- package/dist/components/builder/run-inspector.js.map +1 -0
- package/dist/components/cms/asset-field.d.ts +38 -1
- package/dist/components/cms/asset-field.js +182 -58
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +16 -0
- package/dist/components/cms/asset-picker-core.js +20 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +12 -18
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +3 -19
- package/dist/components/cms/bulk-actions.js +3 -24
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +5 -4
- package/dist/components/cms/cms-store.js +16 -36
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +0 -1
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +3 -1
- package/dist/components/cms/document-diff-view.js +12 -7
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/field-anchors.d.ts +7 -4
- package/dist/components/cms/field-anchors.js +9 -7
- package/dist/components/cms/field-anchors.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +15 -1
- package/dist/components/cms/field-control-lib.js +22 -3
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +21 -30
- package/dist/components/cms/gate-validation.js +11 -32
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/object-array-field.js +4 -4
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +7 -2
- package/dist/components/cms/prop-fields.js +13 -6
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +20 -9
- package/dist/components/cms/rich-text/insert.js +25 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +1 -2
- package/dist/components/cms/use-publish-controller.js +7 -19
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -1
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +3 -1
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +2 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +10 -10
- package/dist/components/components/transfer.js +13 -12
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/lib/ai/apply-content.js +56 -116
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +8 -0
- package/dist/lib/ai/chat-context.js +37 -6
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +6 -0
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +20 -5
- package/dist/lib/ai/constraints.js +125 -33
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +16 -3
- package/dist/lib/ai/fake-provider.js +22 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/leash.d.ts +34 -0
- package/dist/lib/ai/leash.js +28 -0
- package/dist/lib/ai/leash.js.map +1 -0
- package/dist/lib/ai/rich-text-input.d.ts +8 -8
- package/dist/lib/ai/rich-text-input.js +2 -1
- package/dist/lib/ai/rich-text-input.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.d.ts +27 -4
- package/dist/lib/ai/rich-text-translate.js +28 -10
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +13 -13
- package/dist/lib/ai/shape.js +69 -93
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +36 -41
- package/dist/lib/ai/tools.js +83 -87
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +32 -4
- package/dist/lib/ai/translate-document.js +144 -46
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +34 -23
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +15 -12
- package/dist/lib/ai/update-fields.js +112 -188
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +116 -0
- package/dist/lib/ai/writer-form.js +429 -0
- package/dist/lib/ai/writer-form.js.map +1 -0
- package/dist/lib/assets/upload.d.ts +31 -2
- package/dist/lib/assets/upload.js +61 -6
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
- package/dist/lib/clipboard/os-clipboard.js +8 -4
- package/dist/lib/clipboard/os-clipboard.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +18 -9
- package/dist/lib/clipboard/paste-target.js +8 -7
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/field-patch.d.ts +0 -10
- package/dist/lib/cms/field-patch.js +4 -30
- package/dist/lib/cms/field-patch.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/collection-view/collection-view-source.d.ts +7 -3
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.js +3 -2
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +25 -31
- package/dist/lib/dnd/drop-rules.js +53 -59
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +40 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +34 -2
- package/dist/lib/i18n/messages.en.js +43 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/project-runtime-config.d.ts +25 -6
- package/dist/lib/operations/project-runtime-config.js +37 -10
- package/dist/lib/operations/project-runtime-config.js.map +1 -1
- package/dist/lib/operations/publish-with-revision.d.ts +33 -1
- package/dist/lib/operations/publish-with-revision.js +32 -0
- package/dist/lib/operations/publish-with-revision.js.map +1 -1
- package/dist/lib/operations/runtime-validator.d.ts +19 -68
- package/dist/lib/operations/runtime-validator.js +12 -138
- package/dist/lib/operations/runtime-validator.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +9 -2
- package/dist/lib/operations/studio-operations.js +13 -11
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +6 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.js +3 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +8 -11
- package/dist/lib/workspace/index.js +12 -24
- package/dist/lib/workspace/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/components/cms/cms-composition-validators.d.ts +0 -16
- package/dist/components/cms/cms-composition-validators.js +0 -133
- package/dist/components/cms/cms-composition-validators.js.map +0 -1
- package/dist/components/cms/publish-clean.d.ts +0 -28
- package/dist/components/cms/publish-clean.js +0 -38
- package/dist/components/cms/publish-clean.js.map +0 -1
- package/dist/lib/assets/asset-placements.d.ts +0 -28
- package/dist/lib/assets/asset-placements.js +0 -83
- package/dist/lib/assets/asset-placements.js.map +0 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { CmsDocument, CollectionDef, FieldDef, PropFieldLookup, ValuePath, ValuePosition } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { WriterLeash } from './types.js';
|
|
3
|
+
/** The ids a document already references — reusable anywhere in it without a search. */
|
|
4
|
+
export interface HeldReferences {
|
|
5
|
+
/** Referenced document ids, by collection. */
|
|
6
|
+
readonly documents: ReadonlyMap<string, ReadonlySet<string>>;
|
|
7
|
+
readonly assets: ReadonlySet<string>;
|
|
8
|
+
}
|
|
9
|
+
export declare const NO_HELD_REFERENCES: HeldReferences;
|
|
10
|
+
/**
|
|
11
|
+
* Every document and asset id a document references, read off THE reference walk
|
|
12
|
+
* (EC-589) — at any depth. An id the document already holds is real (the leash exists
|
|
13
|
+
* so a model cannot INVENT one), so it stays usable at any position without a search.
|
|
14
|
+
*/
|
|
15
|
+
export declare function heldReferencesOf(document: CmsDocument | undefined): HeldReferences;
|
|
16
|
+
export interface WriterFormContext {
|
|
17
|
+
/**
|
|
18
|
+
* Component id → prop field-defs — the validator's own lookup
|
|
19
|
+
* (`validateOptions.lookupManifest`, the canonical bag's), so the converter reaches
|
|
20
|
+
* exactly the props the validator judges. Without it a composition has no prop
|
|
21
|
+
* positions (and {@link fromWriterFormAtNode} refuses, loudly).
|
|
22
|
+
*/
|
|
23
|
+
readonly lookupManifest?: PropFieldLookup;
|
|
24
|
+
/** What the model searched (absent → no searched ids). */
|
|
25
|
+
readonly leash?: WriterLeash;
|
|
26
|
+
/** What the document already references ({@link heldReferencesOf}). */
|
|
27
|
+
readonly held?: HeldReferences;
|
|
28
|
+
}
|
|
29
|
+
/** One problem the conversion found, located at its position below the field value. */
|
|
30
|
+
export interface WriterFormProblem {
|
|
31
|
+
/** Index-bearing location below the field value (`ValuePosition.at`). */
|
|
32
|
+
readonly at: readonly (string | number)[];
|
|
33
|
+
readonly code: 'invalid-field-value' | 'unknown-asset' | 'unknown-relation-target';
|
|
34
|
+
readonly message: string;
|
|
35
|
+
/** An id outside the leash: it was REMOVED from the converted value. */
|
|
36
|
+
readonly leash?: true;
|
|
37
|
+
}
|
|
38
|
+
export interface FromWriterFormResult {
|
|
39
|
+
/** The stored form (`undefined` — clear the position). */
|
|
40
|
+
readonly value: unknown;
|
|
41
|
+
readonly problems: readonly WriterFormProblem[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert a value a model authored at `position` (its `value` is the WRITER form) into
|
|
45
|
+
* the stored form, at that position and every position below it. `current` is the value
|
|
46
|
+
* the write replaces — the DISPLAYED value of the bucket being written (ratified 2c) —
|
|
47
|
+
* which the placement carry and the composition leash read. `null` / `undefined` clears.
|
|
48
|
+
*/
|
|
49
|
+
export declare function fromWriterForm(position: ValuePosition, current: unknown, ctx: WriterFormContext): FromWriterFormResult;
|
|
50
|
+
/** {@link fromWriterForm} for a top-level field's value. */
|
|
51
|
+
export declare function fromWriterFieldValue(field: FieldDef, raw: unknown, current: unknown, ctx: WriterFormContext, locale?: string): FromWriterFormResult;
|
|
52
|
+
export type FromWriterFormAtNodeResult = {
|
|
53
|
+
readonly ok: true;
|
|
54
|
+
/** The field's next value (the displayed value with the one prop converted). */
|
|
55
|
+
readonly value: unknown;
|
|
56
|
+
/**
|
|
57
|
+
* The position the change wrote, in the value-position grammar: the prop by its
|
|
58
|
+
* node path where the grammar reaches it, else the coarsest addressable ancestor
|
|
59
|
+
* — the field (bucket) itself (EC-595: a writer may always emit a coarser path).
|
|
60
|
+
*/
|
|
61
|
+
readonly path: ValuePath;
|
|
62
|
+
/** The converted prop value when {@link path} is the prop itself (`undefined` — unset). */
|
|
63
|
+
readonly propValue: unknown;
|
|
64
|
+
readonly exact: boolean;
|
|
65
|
+
readonly problems: readonly WriterFormProblem[];
|
|
66
|
+
} | {
|
|
67
|
+
readonly ok: false;
|
|
68
|
+
readonly message: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Change ONE component prop, addressed by its node id and prop name, inside a field's
|
|
72
|
+
* value — at ANY depth the traversal reaches: a top-level block, a child in a section's
|
|
73
|
+
* slot, a component embedded in rich text, a component inside an object's rich text or
|
|
74
|
+
* inside another component's prop. The prop's writer form goes through
|
|
75
|
+
* {@link fromWriterForm} with the prop's current value (carry + leash at that position),
|
|
76
|
+
* and the field value is rebuilt by the traversal's writing fold. Nothing else in the
|
|
77
|
+
* value changes — no other prop, no node, no prose.
|
|
78
|
+
*/
|
|
79
|
+
export declare function fromWriterFormAtNode(field: FieldDef, displayed: unknown, nodeId: string, prop: string, raw: unknown, ctx: WriterFormContext, locale?: string): FromWriterFormAtNodeResult;
|
|
80
|
+
/** Whether a field-def holds TEXT a translation turns into units: plain text, or a sequence. */
|
|
81
|
+
export declare function isTextPosition(position: ValuePosition): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Every TEXT position at or below `position` (EC-593, ratified 4e — "translate uses the
|
|
84
|
+
* same traversal"): a `text` value, and a `richText` / `blocks` sequence (its prose —
|
|
85
|
+
* the sequence's own text — is the caller's to split) together with every text position
|
|
86
|
+
* below it: component text props in a composition and in rich-text embeds, object
|
|
87
|
+
* sub-fields, repeater items, asset placement sub-fields. An `ai.exclude` position (and
|
|
88
|
+
* everything below it) is not text a writer may touch. Depth-first, document order.
|
|
89
|
+
*/
|
|
90
|
+
export declare function textPositionsOf(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>): ValuePosition[];
|
|
91
|
+
/**
|
|
92
|
+
* The writing fold over {@link textPositionsOf}: the value at `position` with each text
|
|
93
|
+
* position's value replaced by `replace(textPosition)` — for a sequence, its own value
|
|
94
|
+
* first (its prose), then the text positions below it, read off the REPLACED value (a
|
|
95
|
+
* component keeps its node id, so its props are found again). Every other value — an
|
|
96
|
+
* image, a select, a component's structure — comes back as it was (identity-preserving).
|
|
97
|
+
*/
|
|
98
|
+
export declare function rebuildTextPositions(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>, replace: (textPosition: ValuePosition) => unknown): unknown;
|
|
99
|
+
/**
|
|
100
|
+
* The form a model READS a stored value in — the mirror of {@link fromWriterForm}, so
|
|
101
|
+
* what `getDocument` shows is what `updateFields` takes back: rich text as a markdown
|
|
102
|
+
* string wherever the engine's loss-guarded inverse can express it faithfully (no
|
|
103
|
+
* embedded components, subset-only marks — else the stored `{ version: 2, content }`
|
|
104
|
+
* stays, so nothing echoed back can silently lose structure), at EVERY position: a
|
|
105
|
+
* top-level field, an object sub-field, a repeater item, a component's rich-text prop.
|
|
106
|
+
* Everything else reads as stored (envelopes are writer forms too).
|
|
107
|
+
*/
|
|
108
|
+
export declare function toWriterForm(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>): unknown;
|
|
109
|
+
/**
|
|
110
|
+
* The components a document holds whose props a writer may change — every component
|
|
111
|
+
* the traversal reaches a DECLARED prop of, in every field of every bucket, at any depth
|
|
112
|
+
* — in first-seen order. What the `getDocument` output's prop catalog lists.
|
|
113
|
+
*/
|
|
114
|
+
export declare function componentIdsOf(collection: CollectionDef, document: CmsDocument, lookupManifest: PropFieldLookup): string[];
|
|
115
|
+
/** {@link toWriterForm} over every field of every bucket of a document. */
|
|
116
|
+
export declare function toWriterDocument(collection: CollectionDef, document: CmsDocument, ctx: Pick<WriterFormContext, 'lookupManifest'>): CmsDocument;
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **THE AI writer-form converter pair** (EC-593, owner-ratified 2026-10-24 — EC-565
|
|
3
|
+
* Round 4, 4c / 4e / 2c; closes EC-571, EC-562 and EC-570 #3).
|
|
4
|
+
*
|
|
5
|
+
* What a model WRITES is not what is stored: rich text is a markdown string, an asset a
|
|
6
|
+
* bare library id (or its envelope when the position declares placement sub-fields), a
|
|
7
|
+
* relation `{ collection, id }`. Every AI writer — create (`applyContentWithShape`), edit
|
|
8
|
+
* (`buildUpdateProposal`), translate (`translateDocument`) — and the one AI reader
|
|
9
|
+
* (`getDocument`'s output) cross that line here, and only here:
|
|
10
|
+
*
|
|
11
|
+
* - {@link fromWriterForm} — writer form → stored form at a position and EVERY position
|
|
12
|
+
* below it;
|
|
13
|
+
* - {@link toWriterForm} — stored form → the form the model reads (rich text as markdown
|
|
14
|
+
* wherever it is faithfully expressible), at every position.
|
|
15
|
+
*
|
|
16
|
+
* Both are folds over core's ONE value-position traversal (`childPositions` /
|
|
17
|
+
* `rebuildFromChildren`, EC-582): an object's sub-fields, each repeater item, an asset
|
|
18
|
+
* placement's sub-fields, and every DECLARED prop of every component node inside a
|
|
19
|
+
* `blocks` / `richText` value (top level, typography, slots — and below a prop). So a
|
|
20
|
+
* nested position is converted by the same rule as a top-level one; there is no second
|
|
21
|
+
* walk to forget a depth (the bug class EC-571 measured: create converted at every depth,
|
|
22
|
+
* edit at the top level only, the read form and translate at the top level only).
|
|
23
|
+
*
|
|
24
|
+
* At every position the converter applies:
|
|
25
|
+
* - **the id leash** (EC-381) — a relation or asset id must come from what the model
|
|
26
|
+
* searched ({@link WriterLeash}, asked per position: a relation by its target, an asset
|
|
27
|
+
* by its declaration's `accept`) or be one the document already references. Anything
|
|
28
|
+
* else is REMOVED from the value and reported as a `leash` problem (create drops it,
|
|
29
|
+
* edit refuses — the callers' ratified policies);
|
|
30
|
+
* - **the placement carry** (EC-558, ratified 2c) — an asset swap keeps the placement's
|
|
31
|
+
* per-use sub-fields through core's one rule (`carryAssetPlacements`), read from the
|
|
32
|
+
* `current` value the caller passes: the DISPLAYED value of the bucket being written
|
|
33
|
+
* (a new locale bucket carries from its fallback, as the pickers do);
|
|
34
|
+
* - **the composition leash** (ratified 4c) — the AI writes content at every depth and
|
|
35
|
+
* never composition STRUCTURE: a `blocks` value is never written whole
|
|
36
|
+
* (`FIELD_TYPES.blocks.writer === 'composition'`), and a rich-text value must keep the
|
|
37
|
+
* component nodes it holds (same ids, same components, same places). A component's
|
|
38
|
+
* PROP is content — {@link fromWriterFormAtNode} changes one, by its node, at any depth
|
|
39
|
+
* the traversal reaches.
|
|
40
|
+
*
|
|
41
|
+
* Pure and deterministic; no provider, no I/O.
|
|
42
|
+
*/
|
|
43
|
+
import { carryAssetPlacements, childPositions, fieldPath, fieldTypeOf, fieldValuePosition, isSequenceValue, rebuildFromChildren, referenceNode, sequenceComponentNodes, walkReferences, walkValuePositions, } from '@elytracms/core/cms-core';
|
|
44
|
+
import { parseRichTextValue, richTextValueToMarkdownProse } from '@elytracms/core/rich-text';
|
|
45
|
+
import { richTextValueFromModelInput } from './rich-text-input.js';
|
|
46
|
+
export const NO_HELD_REFERENCES = { documents: new Map(), assets: new Set() };
|
|
47
|
+
/**
|
|
48
|
+
* Every document and asset id a document references, read off THE reference walk
|
|
49
|
+
* (EC-589) — at any depth. An id the document already holds is real (the leash exists
|
|
50
|
+
* so a model cannot INVENT one), so it stays usable at any position without a search.
|
|
51
|
+
*/
|
|
52
|
+
export function heldReferencesOf(document) {
|
|
53
|
+
if (!document)
|
|
54
|
+
return NO_HELD_REFERENCES;
|
|
55
|
+
const documents = new Map();
|
|
56
|
+
const assets = new Set();
|
|
57
|
+
walkReferences(document, (hit) => {
|
|
58
|
+
if (hit.target.type === 'asset')
|
|
59
|
+
assets.add(hit.target.id);
|
|
60
|
+
else if (hit.target.type === 'document') {
|
|
61
|
+
const ids = documents.get(hit.target.ref.collection) ?? new Set();
|
|
62
|
+
ids.add(hit.target.ref.id);
|
|
63
|
+
documents.set(hit.target.ref.collection, ids);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return { documents, assets };
|
|
67
|
+
}
|
|
68
|
+
// --- Writer form → stored form -------------------------------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Convert a value a model authored at `position` (its `value` is the WRITER form) into
|
|
71
|
+
* the stored form, at that position and every position below it. `current` is the value
|
|
72
|
+
* the write replaces — the DISPLAYED value of the bucket being written (ratified 2c) —
|
|
73
|
+
* which the placement carry and the composition leash read. `null` / `undefined` clears.
|
|
74
|
+
*/
|
|
75
|
+
export function fromWriterForm(position, current, ctx) {
|
|
76
|
+
const problems = [];
|
|
77
|
+
const value = convert(position, current, ctx, problems, position.field.name);
|
|
78
|
+
return { value, problems };
|
|
79
|
+
}
|
|
80
|
+
/** {@link fromWriterForm} for a top-level field's value. */
|
|
81
|
+
export function fromWriterFieldValue(field, raw, current, ctx, locale) {
|
|
82
|
+
return fromWriterForm(fieldValuePosition(field, raw, locale !== undefined ? { locale } : {}), current, ctx);
|
|
83
|
+
}
|
|
84
|
+
function isPlainRecord(value) {
|
|
85
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
/** `title`, `card.image`, `menu[2].label`, `body.content[0].props.headline` — for messages. */
|
|
88
|
+
function label(root, at) {
|
|
89
|
+
return at.reduce((out, key) => (typeof key === 'number' ? `${out}[${key}]` : `${out}.${key}`), root);
|
|
90
|
+
}
|
|
91
|
+
/** The model-facing name of a position: the field's own name, located when nested. */
|
|
92
|
+
function subject(pos, root) {
|
|
93
|
+
return pos.at.length === 0 ? `"${pos.field.name}"` : `"${pos.field.name}" (at ${label(root, pos.at)})`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The structural fingerprint of a composition: which component nodes there are, nested
|
|
97
|
+
* where (their component-ancestor chain), in order relative to each other. Prose between
|
|
98
|
+
* them is content (a new paragraph before an embed shifts its index, not its place among
|
|
99
|
+
* the components); adding, removing, re-nesting or reordering a component is structure.
|
|
100
|
+
*/
|
|
101
|
+
function compositionOf(value) {
|
|
102
|
+
return sequenceComponentNodes(value)
|
|
103
|
+
.map(({ node, chain }) => `${[...chain, node.id].join('/')}=${node.componentId}`)
|
|
104
|
+
.join('|');
|
|
105
|
+
}
|
|
106
|
+
/** Key-order-insensitive JSON — "is this the same value" for a composition the writer may not change. */
|
|
107
|
+
function canonicalJson(value) {
|
|
108
|
+
if (value === null || typeof value !== 'object')
|
|
109
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
110
|
+
if (Array.isArray(value))
|
|
111
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
112
|
+
return `{${Object.entries(value)
|
|
113
|
+
.filter(([, v]) => v !== undefined)
|
|
114
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
115
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`)
|
|
116
|
+
.join(',')}}`;
|
|
117
|
+
}
|
|
118
|
+
/** The stable key matching a child position of the written value to the current value's. */
|
|
119
|
+
function childKey(parent, child) {
|
|
120
|
+
// A prop by its node (ids are stable across a prose edit that shifts indices);
|
|
121
|
+
// everything else by its location relative to the parent.
|
|
122
|
+
return child.kind === 'prop' && child.component
|
|
123
|
+
? `node:${child.component.nodeId}:${child.field.name}`
|
|
124
|
+
: `at:${JSON.stringify(child.at.slice(parent.at.length))}`;
|
|
125
|
+
}
|
|
126
|
+
/** Convert every child position of `pos` (its value already in a readable shape). */
|
|
127
|
+
function convertChildren(pos, current, ctx, problems, root) {
|
|
128
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
129
|
+
const currentChildren = new Map();
|
|
130
|
+
if (current !== undefined) {
|
|
131
|
+
for (const child of childPositions({ ...pos, value: current }, positionCtx)) {
|
|
132
|
+
currentChildren.set(childKey(pos, child), child.value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return rebuildFromChildren(pos, positionCtx, (child) => convert(child, currentChildren.get(childKey(pos, child)), ctx, problems, root));
|
|
136
|
+
}
|
|
137
|
+
function convert(pos, current, ctx, problems, root) {
|
|
138
|
+
const { field } = pos;
|
|
139
|
+
const raw = pos.value === null ? undefined : pos.value;
|
|
140
|
+
const report = (code, message, leash) => {
|
|
141
|
+
problems.push({ at: pos.at, code, message, ...(leash ? { leash } : {}) });
|
|
142
|
+
return undefined;
|
|
143
|
+
};
|
|
144
|
+
switch (fieldTypeOf(field).writer) {
|
|
145
|
+
case 'composition': {
|
|
146
|
+
// 4c: a composition is never written whole — the value stays exactly what it is
|
|
147
|
+
// (re-sending it unchanged is no change); its props change one at a time.
|
|
148
|
+
if (canonicalJson(raw) === canonicalJson(current))
|
|
149
|
+
return current;
|
|
150
|
+
return report('invalid-field-value', `Field ${subject(pos, root)} is a page-builder composition: adding, reordering or removing its components is done on the canvas, not by the assistant. Change one component's prop with blockId + prop (the component's id from getDocument).`);
|
|
151
|
+
}
|
|
152
|
+
case 'markdown': {
|
|
153
|
+
let next = undefined;
|
|
154
|
+
if (raw !== undefined) {
|
|
155
|
+
const parsed = richTextValueFromModelInput(raw);
|
|
156
|
+
if (!parsed.ok)
|
|
157
|
+
return report('invalid-field-value', `Field ${subject(pos, root)}: ${parsed.problems.join(' ')}`);
|
|
158
|
+
// An echoed stored value may carry components: their props are positions too.
|
|
159
|
+
next = convertChildren({ ...pos, value: parsed.value }, current, ctx, problems, root);
|
|
160
|
+
}
|
|
161
|
+
// 4c: prose is content, the components embedded in it are structure.
|
|
162
|
+
if (compositionOf(next) !== compositionOf(current)) {
|
|
163
|
+
return report('invalid-field-value', `Field ${subject(pos, root)} holds embedded components; the assistant cannot add, remove or move them (that is done in the editor). Send the value back as the stored { version: 2, content } object with every component node kept in place, or change a component's prop with blockId + prop.`);
|
|
164
|
+
}
|
|
165
|
+
return next;
|
|
166
|
+
}
|
|
167
|
+
case 'placement': {
|
|
168
|
+
if (raw === undefined)
|
|
169
|
+
return undefined;
|
|
170
|
+
const asset = field;
|
|
171
|
+
const items = Array.isArray(raw) ? raw : [raw];
|
|
172
|
+
const entries = [];
|
|
173
|
+
for (const item of items) {
|
|
174
|
+
if (typeof item === 'string' && item.length > 0)
|
|
175
|
+
entries.push({ id: item, rest: {} });
|
|
176
|
+
else if (isPlainRecord(item) &&
|
|
177
|
+
typeof item.id === 'string' &&
|
|
178
|
+
item.id.length > 0 &&
|
|
179
|
+
(item.type === undefined || item.type === 'asset')) {
|
|
180
|
+
const { type: _type, id, ...rest } = item;
|
|
181
|
+
entries.push({ id: id, rest });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
return report('invalid-field-value', `Asset field ${subject(pos, root)} takes asset id string(s) from listAssets results.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (asset.multiple !== true && entries.length > 1) {
|
|
188
|
+
return report('invalid-field-value', `Field ${subject(pos, root)} holds a single asset — pass one id.`);
|
|
189
|
+
}
|
|
190
|
+
// EC-381: the asset id leash — searched (fitting this position's `accept`) or
|
|
191
|
+
// already on the document; the assistant never invents or uploads an asset.
|
|
192
|
+
const allowed = new Set([
|
|
193
|
+
...(ctx.leash?.assetCandidates(asset) ?? []).map((candidate) => candidate.id),
|
|
194
|
+
...(ctx.held?.assets ?? []),
|
|
195
|
+
]);
|
|
196
|
+
const unknown = entries.filter((entry) => !allowed.has(entry.id));
|
|
197
|
+
if (unknown.length > 0) {
|
|
198
|
+
report('unknown-asset', `Asset field ${subject(pos, root)} may only use ids from listAssets results — search the media library first (got: ${unknown.map((e) => e.id).join(', ')}).`, true);
|
|
199
|
+
}
|
|
200
|
+
const kept = entries.filter((entry) => allowed.has(entry.id));
|
|
201
|
+
if (kept.length === 0)
|
|
202
|
+
return undefined;
|
|
203
|
+
// EC-558 / 2c: the ONE carry rule — a held placement keeps its per-use sub-fields,
|
|
204
|
+
// a swap carries them to the new image, a new id is an envelope.
|
|
205
|
+
const carried = carryAssetPlacements(current, asset.multiple === true ? { multiple: true, ids: kept.map((e) => e.id) } : { multiple: false, id: kept[0].id });
|
|
206
|
+
// The writer's own sub-field values win over the carried ones.
|
|
207
|
+
const withWriter = (entry, index) => {
|
|
208
|
+
const rest = kept[index]?.rest ?? {};
|
|
209
|
+
if (Object.keys(rest).length === 0)
|
|
210
|
+
return entry;
|
|
211
|
+
const base = isPlainRecord(entry) ? entry : { type: 'asset', id: kept[index].id };
|
|
212
|
+
return { ...base, ...rest };
|
|
213
|
+
};
|
|
214
|
+
const merged = Array.isArray(carried) ? carried.map(withWriter) : withWriter(carried, 0);
|
|
215
|
+
return convertChildren({ ...pos, value: merged }, carried, ctx, problems, root);
|
|
216
|
+
}
|
|
217
|
+
case 'reference': {
|
|
218
|
+
if (raw === undefined)
|
|
219
|
+
return undefined;
|
|
220
|
+
const relation = field;
|
|
221
|
+
const items = Array.isArray(raw) ? raw : [raw];
|
|
222
|
+
const ids = [];
|
|
223
|
+
for (const item of items) {
|
|
224
|
+
const id = typeof item === 'string' ? item : isPlainRecord(item) && typeof item.id === 'string' ? item.id : undefined;
|
|
225
|
+
const collection = isPlainRecord(item) ? item.collection : undefined;
|
|
226
|
+
if (!id || (collection !== undefined && collection !== relation.target)) {
|
|
227
|
+
return report('invalid-field-value', `Relation ${subject(pos, root)} takes document id string(s) from searchDocuments results.`);
|
|
228
|
+
}
|
|
229
|
+
ids.push(id);
|
|
230
|
+
}
|
|
231
|
+
if (relation.multiple !== true && ids.length > 1) {
|
|
232
|
+
return report('invalid-field-value', `Field ${subject(pos, root)} holds a single reference — pass one id.`);
|
|
233
|
+
}
|
|
234
|
+
// EC-378/EC-381: the relation id leash — searched in the target collection, or
|
|
235
|
+
// already referenced by the document.
|
|
236
|
+
const allowed = new Set([
|
|
237
|
+
...(ctx.leash?.relationCandidates(relation.target) ?? []).map((candidate) => candidate.id),
|
|
238
|
+
...(ctx.held?.documents.get(relation.target) ?? []),
|
|
239
|
+
]);
|
|
240
|
+
const unknown = ids.filter((id) => !allowed.has(id));
|
|
241
|
+
if (unknown.length > 0) {
|
|
242
|
+
report('unknown-relation-target', `Relation ${subject(pos, root)} may only use ids from searchDocuments results — search "${relation.target}" first (got: ${unknown.join(', ')}).`, true);
|
|
243
|
+
}
|
|
244
|
+
const refs = ids.filter((id) => allowed.has(id)).map((id) => referenceNode({ collection: relation.target, id }));
|
|
245
|
+
if (refs.length === 0)
|
|
246
|
+
return undefined;
|
|
247
|
+
return relation.multiple === true ? refs : refs[0];
|
|
248
|
+
}
|
|
249
|
+
case 'object':
|
|
250
|
+
// A value of the wrong shape has no children; the validator names it.
|
|
251
|
+
return raw === undefined ? undefined : convertChildren({ ...pos, value: raw }, current, ctx, problems, root);
|
|
252
|
+
case 'value':
|
|
253
|
+
return raw;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Change ONE component prop, addressed by its node id and prop name, inside a field's
|
|
258
|
+
* value — at ANY depth the traversal reaches: a top-level block, a child in a section's
|
|
259
|
+
* slot, a component embedded in rich text, a component inside an object's rich text or
|
|
260
|
+
* inside another component's prop. The prop's writer form goes through
|
|
261
|
+
* {@link fromWriterForm} with the prop's current value (carry + leash at that position),
|
|
262
|
+
* and the field value is rebuilt by the traversal's writing fold. Nothing else in the
|
|
263
|
+
* value changes — no other prop, no node, no prose.
|
|
264
|
+
*/
|
|
265
|
+
export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, locale) {
|
|
266
|
+
if (!ctx.lookupManifest) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
message: `Component props cannot be changed here: the project's components are not available to judge "${prop}".`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const positionCtx = { lookupManifest: ctx.lookupManifest };
|
|
273
|
+
const root = fieldValuePosition(field, displayed, locale !== undefined ? { locale } : {});
|
|
274
|
+
const isTarget = (position) => position.kind === 'prop' && position.component?.nodeId === nodeId && position.field.name === prop;
|
|
275
|
+
let target;
|
|
276
|
+
const declared = [];
|
|
277
|
+
let componentId;
|
|
278
|
+
walkValuePositions(root, positionCtx, (position) => {
|
|
279
|
+
if (position.kind !== 'prop' || position.component?.nodeId !== nodeId)
|
|
280
|
+
return;
|
|
281
|
+
componentId = position.component.componentId;
|
|
282
|
+
declared.push(position.field.name);
|
|
283
|
+
if (target === undefined && position.field.name === prop)
|
|
284
|
+
target = position;
|
|
285
|
+
});
|
|
286
|
+
if (!target) {
|
|
287
|
+
if (componentId === undefined) {
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
message: `Component "${nodeId}" was not found in "${field.name}" (or its component declares no props) — read the document first; component ids come from getDocument.`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
ok: false,
|
|
295
|
+
message: `Component "${nodeId}" (${componentId}) has no prop "${prop}" — its props are: ${declared.join(', ')}.`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const found = target;
|
|
299
|
+
const { value: converted, problems } = fromWriterForm({ ...found, value: raw }, found.value, ctx);
|
|
300
|
+
const rebuild = (position) => rebuildFromChildren(position, positionCtx, (child) => (isTarget(child) ? converted : rebuild(child)));
|
|
301
|
+
const value = rebuild(root);
|
|
302
|
+
return found.exact
|
|
303
|
+
? { ok: true, value, path: found.path, propValue: converted, exact: true, problems }
|
|
304
|
+
: { ok: true, value, path: fieldPath(field.name, locale), propValue: undefined, exact: false, problems };
|
|
305
|
+
}
|
|
306
|
+
// --- The text positions (translate) --------------------------------------------------
|
|
307
|
+
/** Whether a field-def holds TEXT a translation turns into units: plain text, or a sequence. */
|
|
308
|
+
export function isTextPosition(position) {
|
|
309
|
+
const { type } = position.field;
|
|
310
|
+
return type === 'text' || type === 'richText' || type === 'blocks';
|
|
311
|
+
}
|
|
312
|
+
function isExcludedBelow(position) {
|
|
313
|
+
return position.at.length > 0 && position.field.ai?.exclude === true;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Every TEXT position at or below `position` (EC-593, ratified 4e — "translate uses the
|
|
317
|
+
* same traversal"): a `text` value, and a `richText` / `blocks` sequence (its prose —
|
|
318
|
+
* the sequence's own text — is the caller's to split) together with every text position
|
|
319
|
+
* below it: component text props in a composition and in rich-text embeds, object
|
|
320
|
+
* sub-fields, repeater items, asset placement sub-fields. An `ai.exclude` position (and
|
|
321
|
+
* everything below it) is not text a writer may touch. Depth-first, document order.
|
|
322
|
+
*/
|
|
323
|
+
export function textPositionsOf(position, ctx) {
|
|
324
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
325
|
+
const out = [];
|
|
326
|
+
const visit = (current) => {
|
|
327
|
+
if (isExcludedBelow(current))
|
|
328
|
+
return;
|
|
329
|
+
if (isTextPosition(current))
|
|
330
|
+
out.push(current);
|
|
331
|
+
if (current.field.type === 'text')
|
|
332
|
+
return;
|
|
333
|
+
for (const child of childPositions(current, positionCtx))
|
|
334
|
+
visit(child);
|
|
335
|
+
};
|
|
336
|
+
visit(position);
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The writing fold over {@link textPositionsOf}: the value at `position` with each text
|
|
341
|
+
* position's value replaced by `replace(textPosition)` — for a sequence, its own value
|
|
342
|
+
* first (its prose), then the text positions below it, read off the REPLACED value (a
|
|
343
|
+
* component keeps its node id, so its props are found again). Every other value — an
|
|
344
|
+
* image, a select, a component's structure — comes back as it was (identity-preserving).
|
|
345
|
+
*/
|
|
346
|
+
export function rebuildTextPositions(position, ctx, replace) {
|
|
347
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
348
|
+
const rebuild = (current) => {
|
|
349
|
+
if (isExcludedBelow(current))
|
|
350
|
+
return current.value;
|
|
351
|
+
if (current.field.type === 'text')
|
|
352
|
+
return replace(current);
|
|
353
|
+
const value = isTextPosition(current) ? replace(current) : current.value;
|
|
354
|
+
return rebuildFromChildren({ ...current, value }, positionCtx, rebuild);
|
|
355
|
+
};
|
|
356
|
+
return rebuild(position);
|
|
357
|
+
}
|
|
358
|
+
// --- Stored form → the read (writer) form --------------------------------------------
|
|
359
|
+
/**
|
|
360
|
+
* The form a model READS a stored value in — the mirror of {@link fromWriterForm}, so
|
|
361
|
+
* what `getDocument` shows is what `updateFields` takes back: rich text as a markdown
|
|
362
|
+
* string wherever the engine's loss-guarded inverse can express it faithfully (no
|
|
363
|
+
* embedded components, subset-only marks — else the stored `{ version: 2, content }`
|
|
364
|
+
* stays, so nothing echoed back can silently lose structure), at EVERY position: a
|
|
365
|
+
* top-level field, an object sub-field, a repeater item, a component's rich-text prop.
|
|
366
|
+
* Everything else reads as stored (envelopes are writer forms too).
|
|
367
|
+
*/
|
|
368
|
+
export function toWriterForm(position, ctx) {
|
|
369
|
+
const { value } = position;
|
|
370
|
+
if (value === undefined || value === null)
|
|
371
|
+
return value;
|
|
372
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
373
|
+
if (fieldTypeOf(position.field).writer === 'markdown' && isSequenceValue(value)) {
|
|
374
|
+
const parsed = parseRichTextValue(value);
|
|
375
|
+
const markdown = parsed.ok ? richTextValueToMarkdownProse(parsed.value) : undefined;
|
|
376
|
+
if (markdown !== undefined)
|
|
377
|
+
return markdown;
|
|
378
|
+
}
|
|
379
|
+
return rebuildFromChildren(position, positionCtx, (child) => toWriterForm(child, ctx));
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* The components a document holds whose props a writer may change — every component
|
|
383
|
+
* the traversal reaches a DECLARED prop of, in every field of every bucket, at any depth
|
|
384
|
+
* — in first-seen order. What the `getDocument` output's prop catalog lists.
|
|
385
|
+
*/
|
|
386
|
+
export function componentIdsOf(collection, document, lookupManifest) {
|
|
387
|
+
const ids = new Set();
|
|
388
|
+
const byName = new Map(collection.fields.map((field) => [field.name, field]));
|
|
389
|
+
const visitBucket = (bucket) => {
|
|
390
|
+
for (const [name, value] of Object.entries(bucket)) {
|
|
391
|
+
const field = byName.get(name);
|
|
392
|
+
if (!field)
|
|
393
|
+
continue;
|
|
394
|
+
walkValuePositions(fieldValuePosition(field, value), { lookupManifest }, (position) => {
|
|
395
|
+
if (position.kind === 'prop' && position.component)
|
|
396
|
+
ids.add(position.component.componentId);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
visitBucket(document.values);
|
|
401
|
+
for (const bucket of Object.values(document.localized ?? {}))
|
|
402
|
+
visitBucket(bucket);
|
|
403
|
+
return [...ids];
|
|
404
|
+
}
|
|
405
|
+
/** {@link toWriterForm} over every field of every bucket of a document. */
|
|
406
|
+
export function toWriterDocument(collection, document, ctx) {
|
|
407
|
+
const byName = new Map(collection.fields.map((field) => [field.name, field]));
|
|
408
|
+
const mapBucket = (bucket, locale) => {
|
|
409
|
+
let next = bucket;
|
|
410
|
+
for (const [name, value] of Object.entries(bucket)) {
|
|
411
|
+
const field = byName.get(name);
|
|
412
|
+
if (!field)
|
|
413
|
+
continue;
|
|
414
|
+
const read = toWriterForm(fieldValuePosition(field, value, locale !== undefined ? { locale } : {}), ctx);
|
|
415
|
+
if (read === value)
|
|
416
|
+
continue;
|
|
417
|
+
if (next === bucket)
|
|
418
|
+
next = { ...bucket };
|
|
419
|
+
next[name] = read;
|
|
420
|
+
}
|
|
421
|
+
return next;
|
|
422
|
+
};
|
|
423
|
+
const values = mapBucket(document.values);
|
|
424
|
+
const localized = document.localized
|
|
425
|
+
? Object.fromEntries(Object.entries(document.localized).map(([locale, bucket]) => [locale, mapBucket(bucket, locale)]))
|
|
426
|
+
: undefined;
|
|
427
|
+
return { ...document, values, ...(localized ? { localized } : {}) };
|
|
428
|
+
}
|
|
429
|
+
//# sourceMappingURL=writer-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer-form.js","sourceRoot":"","sources":["../../../src/lib/ai/writer-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * **THE AI writer-form converter pair** (EC-593, owner-ratified 2026-10-24 — EC-565\n * Round 4, 4c / 4e / 2c; closes EC-571, EC-562 and EC-570 #3).\n *\n * What a model WRITES is not what is stored: rich text is a markdown string, an asset a\n * bare library id (or its envelope when the position declares placement sub-fields), a\n * relation `{ collection, id }`. Every AI writer — create (`applyContentWithShape`), edit\n * (`buildUpdateProposal`), translate (`translateDocument`) — and the one AI reader\n * (`getDocument`'s output) cross that line here, and only here:\n *\n * - {@link fromWriterForm} — writer form → stored form at a position and EVERY position\n * below it;\n * - {@link toWriterForm} — stored form → the form the model reads (rich text as markdown\n * wherever it is faithfully expressible), at every position.\n *\n * Both are folds over core's ONE value-position traversal (`childPositions` /\n * `rebuildFromChildren`, EC-582): an object's sub-fields, each repeater item, an asset\n * placement's sub-fields, and every DECLARED prop of every component node inside a\n * `blocks` / `richText` value (top level, typography, slots — and below a prop). So a\n * nested position is converted by the same rule as a top-level one; there is no second\n * walk to forget a depth (the bug class EC-571 measured: create converted at every depth,\n * edit at the top level only, the read form and translate at the top level only).\n *\n * At every position the converter applies:\n * - **the id leash** (EC-381) — a relation or asset id must come from what the model\n * searched ({@link WriterLeash}, asked per position: a relation by its target, an asset\n * by its declaration's `accept`) or be one the document already references. Anything\n * else is REMOVED from the value and reported as a `leash` problem (create drops it,\n * edit refuses — the callers' ratified policies);\n * - **the placement carry** (EC-558, ratified 2c) — an asset swap keeps the placement's\n * per-use sub-fields through core's one rule (`carryAssetPlacements`), read from the\n * `current` value the caller passes: the DISPLAYED value of the bucket being written\n * (a new locale bucket carries from its fallback, as the pickers do);\n * - **the composition leash** (ratified 4c) — the AI writes content at every depth and\n * never composition STRUCTURE: a `blocks` value is never written whole\n * (`FIELD_TYPES.blocks.writer === 'composition'`), and a rich-text value must keep the\n * component nodes it holds (same ids, same components, same places). A component's\n * PROP is content — {@link fromWriterFormAtNode} changes one, by its node, at any depth\n * the traversal reaches.\n *\n * Pure and deterministic; no provider, no I/O.\n */\nimport {\n carryAssetPlacements,\n childPositions,\n fieldPath,\n fieldTypeOf,\n fieldValuePosition,\n isSequenceValue,\n rebuildFromChildren,\n referenceNode,\n sequenceComponentNodes,\n walkReferences,\n walkValuePositions,\n} from '@elytracms/core/cms-core'\nimport type {\n AssetFieldDef,\n CmsDocument,\n CollectionDef,\n FieldDef,\n FieldValues,\n PropFieldLookup,\n RelationFieldDef,\n ValuePath,\n ValuePosition,\n} from '@elytracms/core/cms-core'\nimport { parseRichTextValue, richTextValueToMarkdownProse } from '@elytracms/core/rich-text'\nimport { richTextValueFromModelInput } from './rich-text-input'\nimport type { WriterLeash } from './types'\n\n// --- The context ---------------------------------------------------------------------\n\n/** The ids a document already references — reusable anywhere in it without a search. */\nexport interface HeldReferences {\n /** Referenced document ids, by collection. */\n readonly documents: ReadonlyMap<string, ReadonlySet<string>>\n readonly assets: ReadonlySet<string>\n}\n\nexport const NO_HELD_REFERENCES: HeldReferences = { documents: new Map(), assets: new Set() }\n\n/**\n * Every document and asset id a document references, read off THE reference walk\n * (EC-589) — at any depth. An id the document already holds is real (the leash exists\n * so a model cannot INVENT one), so it stays usable at any position without a search.\n */\nexport function heldReferencesOf(document: CmsDocument | undefined): HeldReferences {\n if (!document) return NO_HELD_REFERENCES\n const documents = new Map<string, Set<string>>()\n const assets = new Set<string>()\n walkReferences(document, (hit) => {\n if (hit.target.type === 'asset') assets.add(hit.target.id)\n else if (hit.target.type === 'document') {\n const ids = documents.get(hit.target.ref.collection) ?? new Set<string>()\n ids.add(hit.target.ref.id)\n documents.set(hit.target.ref.collection, ids)\n }\n })\n return { documents, assets }\n}\n\nexport interface WriterFormContext {\n /**\n * Component id → prop field-defs — the validator's own lookup\n * (`validateOptions.lookupManifest`, the canonical bag's), so the converter reaches\n * exactly the props the validator judges. Without it a composition has no prop\n * positions (and {@link fromWriterFormAtNode} refuses, loudly).\n */\n readonly lookupManifest?: PropFieldLookup\n /** What the model searched (absent → no searched ids). */\n readonly leash?: WriterLeash\n /** What the document already references ({@link heldReferencesOf}). */\n readonly held?: HeldReferences\n}\n\n/** One problem the conversion found, located at its position below the field value. */\nexport interface WriterFormProblem {\n /** Index-bearing location below the field value (`ValuePosition.at`). */\n readonly at: readonly (string | number)[]\n readonly code: 'invalid-field-value' | 'unknown-asset' | 'unknown-relation-target'\n readonly message: string\n /** An id outside the leash: it was REMOVED from the converted value. */\n readonly leash?: true\n}\n\nexport interface FromWriterFormResult {\n /** The stored form (`undefined` — clear the position). */\n readonly value: unknown\n readonly problems: readonly WriterFormProblem[]\n}\n\n// --- Writer form → stored form -------------------------------------------------------\n\n/**\n * Convert a value a model authored at `position` (its `value` is the WRITER form) into\n * the stored form, at that position and every position below it. `current` is the value\n * the write replaces — the DISPLAYED value of the bucket being written (ratified 2c) —\n * which the placement carry and the composition leash read. `null` / `undefined` clears.\n */\nexport function fromWriterForm(\n position: ValuePosition,\n current: unknown,\n ctx: WriterFormContext,\n): FromWriterFormResult {\n const problems: WriterFormProblem[] = []\n const value = convert(position, current, ctx, problems, position.field.name)\n return { value, problems }\n}\n\n/** {@link fromWriterForm} for a top-level field's value. */\nexport function fromWriterFieldValue(\n field: FieldDef,\n raw: unknown,\n current: unknown,\n ctx: WriterFormContext,\n locale?: string,\n): FromWriterFormResult {\n return fromWriterForm(fieldValuePosition(field, raw, locale !== undefined ? { locale } : {}), current, ctx)\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** `title`, `card.image`, `menu[2].label`, `body.content[0].props.headline` — for messages. */\nfunction label(root: string, at: readonly (string | number)[]): string {\n return at.reduce<string>((out, key) => (typeof key === 'number' ? `${out}[${key}]` : `${out}.${key}`), root)\n}\n\n/** The model-facing name of a position: the field's own name, located when nested. */\nfunction subject(pos: ValuePosition, root: string): string {\n return pos.at.length === 0 ? `\"${pos.field.name}\"` : `\"${pos.field.name}\" (at ${label(root, pos.at)})`\n}\n\n/**\n * The structural fingerprint of a composition: which component nodes there are, nested\n * where (their component-ancestor chain), in order relative to each other. Prose between\n * them is content (a new paragraph before an embed shifts its index, not its place among\n * the components); adding, removing, re-nesting or reordering a component is structure.\n */\nfunction compositionOf(value: unknown): string {\n return sequenceComponentNodes(value)\n .map(({ node, chain }) => `${[...chain, node.id].join('/')}=${node.componentId}`)\n .join('|')\n}\n\n/** Key-order-insensitive JSON — \"is this the same value\" for a composition the writer may not change. */\nfunction canonicalJson(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'undefined'\n if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`\n return `{${Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`)\n .join(',')}}`\n}\n\n/** The stable key matching a child position of the written value to the current value's. */\nfunction childKey(parent: ValuePosition, child: ValuePosition): string {\n // A prop by its node (ids are stable across a prose edit that shifts indices);\n // everything else by its location relative to the parent.\n return child.kind === 'prop' && child.component\n ? `node:${child.component.nodeId}:${child.field.name}`\n : `at:${JSON.stringify(child.at.slice(parent.at.length))}`\n}\n\n/** Convert every child position of `pos` (its value already in a readable shape). */\nfunction convertChildren(\n pos: ValuePosition,\n current: unknown,\n ctx: WriterFormContext,\n problems: WriterFormProblem[],\n root: string,\n): unknown {\n const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {}\n const currentChildren = new Map<string, unknown>()\n if (current !== undefined) {\n for (const child of childPositions({ ...pos, value: current }, positionCtx)) {\n currentChildren.set(childKey(pos, child), child.value)\n }\n }\n return rebuildFromChildren(pos, positionCtx, (child) =>\n convert(child, currentChildren.get(childKey(pos, child)), ctx, problems, root),\n )\n}\n\nfunction convert(\n pos: ValuePosition,\n current: unknown,\n ctx: WriterFormContext,\n problems: WriterFormProblem[],\n root: string,\n): unknown {\n const { field } = pos\n const raw = pos.value === null ? undefined : pos.value\n const report = (code: WriterFormProblem['code'], message: string, leash?: true): undefined => {\n problems.push({ at: pos.at, code, message, ...(leash ? { leash } : {}) })\n return undefined\n }\n\n switch (fieldTypeOf(field).writer) {\n case 'composition': {\n // 4c: a composition is never written whole — the value stays exactly what it is\n // (re-sending it unchanged is no change); its props change one at a time.\n if (canonicalJson(raw) === canonicalJson(current)) return current\n return report(\n 'invalid-field-value',\n `Field ${subject(pos, root)} is a page-builder composition: adding, reordering or removing its components is done on the canvas, not by the assistant. Change one component's prop with blockId + prop (the component's id from getDocument).`,\n )\n }\n\n case 'markdown': {\n let next: unknown = undefined\n if (raw !== undefined) {\n const parsed = richTextValueFromModelInput(raw)\n if (!parsed.ok) return report('invalid-field-value', `Field ${subject(pos, root)}: ${parsed.problems.join(' ')}`)\n // An echoed stored value may carry components: their props are positions too.\n next = convertChildren({ ...pos, value: parsed.value }, current, ctx, problems, root)\n }\n // 4c: prose is content, the components embedded in it are structure.\n if (compositionOf(next) !== compositionOf(current)) {\n return report(\n 'invalid-field-value',\n `Field ${subject(pos, root)} holds embedded components; the assistant cannot add, remove or move them (that is done in the editor). Send the value back as the stored { version: 2, content } object with every component node kept in place, or change a component's prop with blockId + prop.`,\n )\n }\n return next\n }\n\n case 'placement': {\n if (raw === undefined) return undefined\n const asset = field as AssetFieldDef\n const items = Array.isArray(raw) ? raw : [raw]\n const entries: { id: string; rest: Record<string, unknown> }[] = []\n for (const item of items) {\n if (typeof item === 'string' && item.length > 0) entries.push({ id: item, rest: {} })\n else if (\n isPlainRecord(item) &&\n typeof item.id === 'string' &&\n item.id.length > 0 &&\n (item.type === undefined || item.type === 'asset')\n ) {\n const { type: _type, id, ...rest } = item\n entries.push({ id: id as string, rest })\n } else {\n return report(\n 'invalid-field-value',\n `Asset field ${subject(pos, root)} takes asset id string(s) from listAssets results.`,\n )\n }\n }\n if (asset.multiple !== true && entries.length > 1) {\n return report('invalid-field-value', `Field ${subject(pos, root)} holds a single asset — pass one id.`)\n }\n // EC-381: the asset id leash — searched (fitting this position's `accept`) or\n // already on the document; the assistant never invents or uploads an asset.\n const allowed = new Set([\n ...(ctx.leash?.assetCandidates(asset) ?? []).map((candidate) => candidate.id),\n ...(ctx.held?.assets ?? []),\n ])\n const unknown = entries.filter((entry) => !allowed.has(entry.id))\n if (unknown.length > 0) {\n report(\n 'unknown-asset',\n `Asset field ${subject(pos, root)} may only use ids from listAssets results — search the media library first (got: ${unknown.map((e) => e.id).join(', ')}).`,\n true,\n )\n }\n const kept = entries.filter((entry) => allowed.has(entry.id))\n if (kept.length === 0) return undefined\n // EC-558 / 2c: the ONE carry rule — a held placement keeps its per-use sub-fields,\n // a swap carries them to the new image, a new id is an envelope.\n const carried = carryAssetPlacements(\n current,\n asset.multiple === true ? { multiple: true, ids: kept.map((e) => e.id) } : { multiple: false, id: kept[0]!.id },\n )\n // The writer's own sub-field values win over the carried ones.\n const withWriter = (entry: unknown, index: number): unknown => {\n const rest = kept[index]?.rest ?? {}\n if (Object.keys(rest).length === 0) return entry\n const base = isPlainRecord(entry) ? entry : { type: 'asset', id: kept[index]!.id }\n return { ...base, ...rest }\n }\n const merged = Array.isArray(carried) ? carried.map(withWriter) : withWriter(carried, 0)\n return convertChildren({ ...pos, value: merged }, carried, ctx, problems, root)\n }\n\n case 'reference': {\n if (raw === undefined) return undefined\n const relation = field as RelationFieldDef\n const items = Array.isArray(raw) ? raw : [raw]\n const ids: string[] = []\n for (const item of items) {\n const id = typeof item === 'string' ? item : isPlainRecord(item) && typeof item.id === 'string' ? item.id : undefined\n const collection = isPlainRecord(item) ? item.collection : undefined\n if (!id || (collection !== undefined && collection !== relation.target)) {\n return report(\n 'invalid-field-value',\n `Relation ${subject(pos, root)} takes document id string(s) from searchDocuments results.`,\n )\n }\n ids.push(id)\n }\n if (relation.multiple !== true && ids.length > 1) {\n return report('invalid-field-value', `Field ${subject(pos, root)} holds a single reference — pass one id.`)\n }\n // EC-378/EC-381: the relation id leash — searched in the target collection, or\n // already referenced by the document.\n const allowed = new Set([\n ...(ctx.leash?.relationCandidates(relation.target) ?? []).map((candidate) => candidate.id),\n ...(ctx.held?.documents.get(relation.target) ?? []),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n report(\n 'unknown-relation-target',\n `Relation ${subject(pos, root)} may only use ids from searchDocuments results — search \"${relation.target}\" first (got: ${unknown.join(', ')}).`,\n true,\n )\n }\n const refs = ids.filter((id) => allowed.has(id)).map((id) => referenceNode({ collection: relation.target, id }))\n if (refs.length === 0) return undefined\n return relation.multiple === true ? refs : refs[0]\n }\n\n case 'object':\n // A value of the wrong shape has no children; the validator names it.\n return raw === undefined ? undefined : convertChildren({ ...pos, value: raw }, current, ctx, problems, root)\n\n case 'value':\n return raw\n }\n}\n\n// --- A component prop, by its node ---------------------------------------------------\n\nexport type FromWriterFormAtNodeResult =\n | {\n readonly ok: true\n /** The field's next value (the displayed value with the one prop converted). */\n readonly value: unknown\n /**\n * The position the change wrote, in the value-position grammar: the prop by its\n * node path where the grammar reaches it, else the coarsest addressable ancestor\n * — the field (bucket) itself (EC-595: a writer may always emit a coarser path).\n */\n readonly path: ValuePath\n /** The converted prop value when {@link path} is the prop itself (`undefined` — unset). */\n readonly propValue: unknown\n readonly exact: boolean\n readonly problems: readonly WriterFormProblem[]\n }\n | { readonly ok: false; readonly message: string }\n\n/**\n * Change ONE component prop, addressed by its node id and prop name, inside a field's\n * value — at ANY depth the traversal reaches: a top-level block, a child in a section's\n * slot, a component embedded in rich text, a component inside an object's rich text or\n * inside another component's prop. The prop's writer form goes through\n * {@link fromWriterForm} with the prop's current value (carry + leash at that position),\n * and the field value is rebuilt by the traversal's writing fold. Nothing else in the\n * value changes — no other prop, no node, no prose.\n */\nexport function fromWriterFormAtNode(\n field: FieldDef,\n displayed: unknown,\n nodeId: string,\n prop: string,\n raw: unknown,\n ctx: WriterFormContext,\n locale?: string,\n): FromWriterFormAtNodeResult {\n if (!ctx.lookupManifest) {\n return {\n ok: false,\n message: `Component props cannot be changed here: the project's components are not available to judge \"${prop}\".`,\n }\n }\n const positionCtx = { lookupManifest: ctx.lookupManifest }\n const root = fieldValuePosition(field, displayed, locale !== undefined ? { locale } : {})\n const isTarget = (position: ValuePosition) =>\n position.kind === 'prop' && position.component?.nodeId === nodeId && position.field.name === prop\n let target: ValuePosition | undefined\n const declared: string[] = []\n let componentId: string | undefined\n walkValuePositions(root, positionCtx, (position) => {\n if (position.kind !== 'prop' || position.component?.nodeId !== nodeId) return\n componentId = position.component.componentId\n declared.push(position.field.name)\n if (target === undefined && position.field.name === prop) target = position\n })\n if (!target) {\n if (componentId === undefined) {\n return {\n ok: false,\n message: `Component \"${nodeId}\" was not found in \"${field.name}\" (or its component declares no props) — read the document first; component ids come from getDocument.`,\n }\n }\n return {\n ok: false,\n message: `Component \"${nodeId}\" (${componentId}) has no prop \"${prop}\" — its props are: ${declared.join(', ')}.`,\n }\n }\n const found = target\n const { value: converted, problems } = fromWriterForm({ ...found, value: raw }, found.value, ctx)\n const rebuild = (position: ValuePosition): unknown =>\n rebuildFromChildren(position, positionCtx, (child) => (isTarget(child) ? converted : rebuild(child)))\n const value = rebuild(root)\n return found.exact\n ? { ok: true, value, path: found.path, propValue: converted, exact: true, problems }\n : { ok: true, value, path: fieldPath(field.name, locale), propValue: undefined, exact: false, problems }\n}\n\n// --- The text positions (translate) --------------------------------------------------\n\n/** Whether a field-def holds TEXT a translation turns into units: plain text, or a sequence. */\nexport function isTextPosition(position: ValuePosition): boolean {\n const { type } = position.field\n return type === 'text' || type === 'richText' || type === 'blocks'\n}\n\nfunction isExcludedBelow(position: ValuePosition): boolean {\n return position.at.length > 0 && position.field.ai?.exclude === true\n}\n\n/**\n * Every TEXT position at or below `position` (EC-593, ratified 4e — \"translate uses the\n * same traversal\"): a `text` value, and a `richText` / `blocks` sequence (its prose —\n * the sequence's own text — is the caller's to split) together with every text position\n * below it: component text props in a composition and in rich-text embeds, object\n * sub-fields, repeater items, asset placement sub-fields. An `ai.exclude` position (and\n * everything below it) is not text a writer may touch. Depth-first, document order.\n */\nexport function textPositionsOf(\n position: ValuePosition,\n ctx: Pick<WriterFormContext, 'lookupManifest'>,\n): ValuePosition[] {\n const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {}\n const out: ValuePosition[] = []\n const visit = (current: ValuePosition): void => {\n if (isExcludedBelow(current)) return\n if (isTextPosition(current)) out.push(current)\n if (current.field.type === 'text') return\n for (const child of childPositions(current, positionCtx)) visit(child)\n }\n visit(position)\n return out\n}\n\n/**\n * The writing fold over {@link textPositionsOf}: the value at `position` with each text\n * position's value replaced by `replace(textPosition)` — for a sequence, its own value\n * first (its prose), then the text positions below it, read off the REPLACED value (a\n * component keeps its node id, so its props are found again). Every other value — an\n * image, a select, a component's structure — comes back as it was (identity-preserving).\n */\nexport function rebuildTextPositions(\n position: ValuePosition,\n ctx: Pick<WriterFormContext, 'lookupManifest'>,\n replace: (textPosition: ValuePosition) => unknown,\n): unknown {\n const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {}\n const rebuild = (current: ValuePosition): unknown => {\n if (isExcludedBelow(current)) return current.value\n if (current.field.type === 'text') return replace(current)\n const value = isTextPosition(current) ? replace(current) : current.value\n return rebuildFromChildren({ ...current, value }, positionCtx, rebuild)\n }\n return rebuild(position)\n}\n\n// --- Stored form → the read (writer) form --------------------------------------------\n\n/**\n * The form a model READS a stored value in — the mirror of {@link fromWriterForm}, so\n * what `getDocument` shows is what `updateFields` takes back: rich text as a markdown\n * string wherever the engine's loss-guarded inverse can express it faithfully (no\n * embedded components, subset-only marks — else the stored `{ version: 2, content }`\n * stays, so nothing echoed back can silently lose structure), at EVERY position: a\n * top-level field, an object sub-field, a repeater item, a component's rich-text prop.\n * Everything else reads as stored (envelopes are writer forms too).\n */\nexport function toWriterForm(\n position: ValuePosition,\n ctx: Pick<WriterFormContext, 'lookupManifest'>,\n): unknown {\n const { value } = position\n if (value === undefined || value === null) return value\n const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {}\n if (fieldTypeOf(position.field).writer === 'markdown' && isSequenceValue(value)) {\n const parsed = parseRichTextValue(value)\n const markdown = parsed.ok ? richTextValueToMarkdownProse(parsed.value) : undefined\n if (markdown !== undefined) return markdown\n }\n return rebuildFromChildren(position, positionCtx, (child) => toWriterForm(child, ctx))\n}\n\n/**\n * The components a document holds whose props a writer may change — every component\n * the traversal reaches a DECLARED prop of, in every field of every bucket, at any depth\n * — in first-seen order. What the `getDocument` output's prop catalog lists.\n */\nexport function componentIdsOf(\n collection: CollectionDef,\n document: CmsDocument,\n lookupManifest: PropFieldLookup,\n): string[] {\n const ids = new Set<string>()\n const byName = new Map(collection.fields.map((field) => [field.name, field]))\n const visitBucket = (bucket: FieldValues) => {\n for (const [name, value] of Object.entries(bucket)) {\n const field = byName.get(name)\n if (!field) continue\n walkValuePositions(fieldValuePosition(field, value), { lookupManifest }, (position) => {\n if (position.kind === 'prop' && position.component) ids.add(position.component.componentId)\n })\n }\n }\n visitBucket(document.values)\n for (const bucket of Object.values(document.localized ?? {})) visitBucket(bucket)\n return [...ids]\n}\n\n/** {@link toWriterForm} over every field of every bucket of a document. */\nexport function toWriterDocument(\n collection: CollectionDef,\n document: CmsDocument,\n ctx: Pick<WriterFormContext, 'lookupManifest'>,\n): CmsDocument {\n const byName = new Map(collection.fields.map((field) => [field.name, field]))\n const mapBucket = (bucket: FieldValues, locale?: string): FieldValues => {\n let next = bucket\n for (const [name, value] of Object.entries(bucket)) {\n const field = byName.get(name)\n if (!field) continue\n const read = toWriterForm(fieldValuePosition(field, value, locale !== undefined ? { locale } : {}), ctx)\n if (read === value) continue\n if (next === bucket) next = { ...bucket }\n next[name] = read\n }\n return next\n }\n const values = mapBucket(document.values)\n const localized = document.localized\n ? Object.fromEntries(Object.entries(document.localized).map(([locale, bucket]) => [locale, mapBucket(bucket, locale)]))\n : undefined\n return { ...document, values, ...(localized ? { localized } : {}) }\n}\n"]}
|