@elytracms/studio 0.0.104 → 0.0.106
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/dist/components/app-views/module.d.ts +4 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +19 -3
- package/dist/components/builder/builder-store.js +24 -13
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +6 -5
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +4 -2
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.js +43 -7
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/duplicate-document.js +5 -5
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +7 -1
- package/dist/components/cms/rich-text/editor.js +9 -4
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-identity.d.ts +36 -0
- package/dist/components/cms/rich-text/embed-identity.js +114 -0
- package/dist/components/cms/rich-text/embed-identity.js.map +1 -0
- package/dist/components/cms/rich-text/extensions.d.ts +7 -1
- package/dist/components/cms/rich-text/extensions.js +7 -1
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +10 -5
- package/dist/components/cms/rich-text/insert.js +10 -12
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +0 -2
- package/dist/components/components/component-preview.js +0 -4
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +3 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +12 -8
- package/dist/components/components/transfer.js +16 -15
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/redirects/redirects-schema.d.ts +31 -0
- package/dist/components/redirects/redirects-schema.js +12 -6
- package/dist/components/redirects/redirects-schema.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +4 -1
- package/dist/lib/ai/translate-document.js +87 -47
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +48 -5
- package/dist/lib/ai/writer-form.js +119 -64
- package/dist/lib/ai/writer-form.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 +20 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +20 -1
- package/dist/lib/i18n/messages.en.js +21 -2
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persona/labels.js +2 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/registry/preview.d.ts +2 -1
- package/dist/lib/registry/preview.js +8 -3
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +9 -1
- package/dist/lib/workspace/index.js +13 -2
- package/dist/lib/workspace/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -56,6 +56,34 @@ export interface FromWriterFormResult {
|
|
|
56
56
|
export declare function fromWriterForm(position: ValuePosition, current: unknown, ctx: WriterFormContext): FromWriterFormResult;
|
|
57
57
|
/** {@link fromWriterForm} for a top-level field's value. */
|
|
58
58
|
export declare function fromWriterFieldValue(field: FieldDef, raw: unknown, current: unknown, ctx: WriterFormContext, locale?: string): FromWriterFormResult;
|
|
59
|
+
/**
|
|
60
|
+
* **THE position key** (EC-774, Codex r11 F56) — what names ONE position inside one field
|
|
61
|
+
* value (one `(field, locale)` bucket), so that two positions never share a name: the JSON of
|
|
62
|
+
* its typed {@link KeySegment}s, built parent → child over core's ONE traversal
|
|
63
|
+
* (`childPositions`).
|
|
64
|
+
*
|
|
65
|
+
* - A component PROP: its enclosing SEQUENCE's key + `['node', id]` + the prop name. Anchored
|
|
66
|
+
* by the node — an inline embed a translation moves keeps its key — and scoped to the
|
|
67
|
+
* sequence that holds it: one node id in two rich-text values of an object, or in two
|
|
68
|
+
* repeater items, is two keys.
|
|
69
|
+
* - Any other child: its parent's key + its location below the parent (a sub-field by name,
|
|
70
|
+
* a repeater item by index, an asset-list entry by asset id and occurrence).
|
|
71
|
+
*
|
|
72
|
+
* Injective: a node step cannot alias a name or an index, and no character inside a name can
|
|
73
|
+
* split it. (The translator used to join locations with `/` and `|` and find a prop by
|
|
74
|
+
* `lastIndexOf('props')`; a sub-field named `a/b` or a holder named `props` then shared a
|
|
75
|
+
* name with another position, and both got the second one's translation.)
|
|
76
|
+
*
|
|
77
|
+
* Every fold that matches positions across two walks or two values reads it: the translator's
|
|
78
|
+
* plan and rebuild ({@link textPositionsOf}, {@link rebuildTextPositions}), the locked carry
|
|
79
|
+
* ({@link withLockedFrom}, {@link lockedChanges}) and a write's own child matching.
|
|
80
|
+
*
|
|
81
|
+
* A position under a component node whose id its sequence holds MORE THAN ONCE has no key
|
|
82
|
+
* (`undefined`): core's writing fold addresses a prop by its node id within one sequence
|
|
83
|
+
* (`rebuildFromChildren`), so it can never be written apart from its twin's. The translator
|
|
84
|
+
* refuses it (`unreadable`); the locked carry leaves it as it is.
|
|
85
|
+
*/
|
|
86
|
+
export type PositionKey = string;
|
|
59
87
|
/**
|
|
60
88
|
* One writer-locked position under a value — the TOPMOST locked one on its branch (it stands
|
|
61
89
|
* for everything below it, which is locked too) — with its value (possibly absent: the
|
|
@@ -144,6 +172,18 @@ export type FromWriterFormAtNodeResult = {
|
|
|
144
172
|
export declare function fromWriterFormAtNode(field: FieldDef, displayed: unknown, nodeId: string, prop: string, raw: unknown, ctx: WriterFormContext, locale?: string): FromWriterFormAtNodeResult;
|
|
145
173
|
/** Whether a field-def holds TEXT a translation turns into units: plain text, or a sequence. */
|
|
146
174
|
export declare function isTextPosition(position: ValuePosition): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* A text position with its {@link PositionKey} below the walk's root (EC-774) — `undefined`
|
|
177
|
+
* when it has none: it sits under a component node whose id its sequence holds twice, and so
|
|
178
|
+
* can never be written apart from its twin's.
|
|
179
|
+
*/
|
|
180
|
+
export interface TextPosition extends ValuePosition {
|
|
181
|
+
readonly key: PositionKey | undefined;
|
|
182
|
+
}
|
|
183
|
+
/** A {@link TextPosition} that has a key — the only kind the writing fold hands back. */
|
|
184
|
+
export interface KeyedTextPosition extends TextPosition {
|
|
185
|
+
readonly key: PositionKey;
|
|
186
|
+
}
|
|
147
187
|
/**
|
|
148
188
|
* Every TEXT position at or below `position` (EC-593, ratified 4e — "translate uses the
|
|
149
189
|
* same traversal"): a `text` value, and a `richText` / `blocks` sequence (its prose —
|
|
@@ -151,17 +191,20 @@ export declare function isTextPosition(position: ValuePosition): boolean;
|
|
|
151
191
|
* below it: component text props in a composition and in rich-text embeds, object
|
|
152
192
|
* sub-fields, repeater items, asset placement sub-fields. A writer-locked position
|
|
153
193
|
* (EC-716: `ai.exclude` or `form.readOnly`) and everything below it is not text a writer
|
|
154
|
-
* may touch — not a unit, and so never a skip either. Depth-first, document order.
|
|
194
|
+
* may touch — not a unit, and so never a skip either. Depth-first, document order. Each
|
|
195
|
+
* carries its {@link PositionKey} (EC-774) — the one name the plan and the rebuild share.
|
|
155
196
|
*/
|
|
156
|
-
export declare function textPositionsOf(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>):
|
|
197
|
+
export declare function textPositionsOf(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>): TextPosition[];
|
|
157
198
|
/**
|
|
158
199
|
* The writing fold over {@link textPositionsOf}: the value at `position` with each text
|
|
159
200
|
* position's value replaced by `replace(textPosition)` — for a sequence, its own value
|
|
160
201
|
* first (its prose), then the text positions below it, read off the REPLACED value (a
|
|
161
|
-
* component keeps its node id, so its props are found again
|
|
162
|
-
* image, a select, a component's structure — comes back as it was
|
|
202
|
+
* component keeps its node id, so its props are found again, under the same key). Every
|
|
203
|
+
* other value — an image, a select, a component's structure — comes back as it was
|
|
204
|
+
* (identity-preserving), and so does a text position with no key (EC-774: `replace` never
|
|
205
|
+
* sees one — it cannot be written apart from its twin).
|
|
163
206
|
*/
|
|
164
|
-
export declare function rebuildTextPositions(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>, replace: (textPosition:
|
|
207
|
+
export declare function rebuildTextPositions(position: ValuePosition, ctx: Pick<WriterFormContext, 'lookupManifest'>, replace: (textPosition: KeyedTextPosition) => unknown): unknown;
|
|
165
208
|
/**
|
|
166
209
|
* The form a model READS a stored value in — the mirror of {@link fromWriterForm}, so
|
|
167
210
|
* what `getDocument` shows is what `updateFields` takes back: rich text as a markdown
|
|
@@ -150,19 +150,64 @@ function canonicalJson(value) {
|
|
|
150
150
|
.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`)
|
|
151
151
|
.join(',')}}`;
|
|
152
152
|
}
|
|
153
|
-
|
|
153
|
+
function keyString(path) {
|
|
154
|
+
return JSON.stringify(path);
|
|
155
|
+
}
|
|
156
|
+
/** The node a child is a prop of — a readable prop or one stored as no static member. */
|
|
157
|
+
function propNodeOf(child) {
|
|
158
|
+
if (isOpaquePosition(child))
|
|
159
|
+
return child.opaque === 'not-a-static-prop' ? child.component?.nodeId : undefined;
|
|
160
|
+
return child.kind === 'prop' ? child.component?.nodeId : undefined;
|
|
161
|
+
}
|
|
162
|
+
/** A child's steps below its parent — the relative part of its {@link PositionKey}. */
|
|
163
|
+
function childSegments(parent, child) {
|
|
164
|
+
const node = propNodeOf(child);
|
|
165
|
+
if (isOpaquePosition(child)) {
|
|
166
|
+
return node !== undefined
|
|
167
|
+
? [['opaque'], ['node', node], child.declaration.name]
|
|
168
|
+
: [['opaque'], ...child.at.slice(parent.at.length)];
|
|
169
|
+
}
|
|
170
|
+
if (node !== undefined)
|
|
171
|
+
return [['node', node], child.field.name];
|
|
172
|
+
const below = child.at.slice(parent.at.length);
|
|
173
|
+
if (parent.field.type === 'asset' && parent.field.multiple !== false && Array.isArray(parent.value)) {
|
|
174
|
+
const index = below[0];
|
|
175
|
+
const entries = parent.value;
|
|
176
|
+
const id = readAssetId(entries[index]);
|
|
177
|
+
const occurrence = entries.slice(0, index).filter((entry) => readAssetId(entry) === id).length;
|
|
178
|
+
return [['asset', id ?? null, occurrence], ...below.slice(1)];
|
|
179
|
+
}
|
|
180
|
+
return below;
|
|
181
|
+
}
|
|
182
|
+
/** The node ids a `blocks` / `richText` value holds more than once (none for any other value). */
|
|
183
|
+
function twinNodeIds(position) {
|
|
184
|
+
const twins = new Set();
|
|
185
|
+
if (position.field.type !== 'blocks' && position.field.type !== 'richText')
|
|
186
|
+
return twins;
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
for (const { node } of sequenceComponentNodes(position.value))
|
|
189
|
+
(seen.has(node.id) ? twins : seen).add(node.id);
|
|
190
|
+
return twins;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The key of each child of `parent`, whose own key is `parentKey`: `undefined` below a keyless
|
|
194
|
+
* parent, and for a prop of a node its sequence holds twice (see {@link PositionKey}).
|
|
195
|
+
*/
|
|
196
|
+
function childKeysUnder(parent, parentKey) {
|
|
197
|
+
if (parentKey === undefined)
|
|
198
|
+
return () => undefined;
|
|
199
|
+
const twins = twinNodeIds(parent);
|
|
200
|
+
return (child) => {
|
|
201
|
+
const node = propNodeOf(child);
|
|
202
|
+
return node !== undefined && twins.has(node) ? undefined : [...parentKey, ...childSegments(parent, child)];
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The key matching a child position of the written value to the current value's, one level
|
|
207
|
+
* down — the child's {@link PositionKey} steps below its parent.
|
|
208
|
+
*/
|
|
154
209
|
function childKey(parent, child) {
|
|
155
|
-
|
|
156
|
-
// everything else by its location relative to the parent.
|
|
157
|
-
return child.kind === 'prop' && child.component
|
|
158
|
-
? `node:${child.component.nodeId}:${child.field.name}`
|
|
159
|
-
: `at:${JSON.stringify(child.at.slice(parent.at.length))}`;
|
|
160
|
-
}
|
|
161
|
-
/** {@link childKey} for a value the traversal cannot read, in its own key space (EC-748). */
|
|
162
|
-
function opaqueChildKey(parent, child) {
|
|
163
|
-
return child.opaque === 'not-a-static-prop' && child.component
|
|
164
|
-
? `opaque:node:${child.component.nodeId}:${child.declaration.name}`
|
|
165
|
-
: `opaque:at:${JSON.stringify(child.at.slice(parent.at.length))}`;
|
|
210
|
+
return keyString(childSegments(parent, child));
|
|
166
211
|
}
|
|
167
212
|
/** The model-facing name of an unreadable value: its declaration's name, located when nested. */
|
|
168
213
|
function opaqueSubject(position, root) {
|
|
@@ -198,12 +243,9 @@ function convertChildren(pos, current, ctx, problems, root) {
|
|
|
198
243
|
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
199
244
|
const currentChildren = new Map();
|
|
200
245
|
if (current !== undefined) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
else
|
|
205
|
-
currentChildren.set(childKey(pos, child), child.value);
|
|
206
|
-
}
|
|
246
|
+
const held = { ...pos, value: current };
|
|
247
|
+
for (const child of childPositions(held, positionCtx))
|
|
248
|
+
currentChildren.set(childKey(held, child), child.value);
|
|
207
249
|
}
|
|
208
250
|
return rebuildFromChildren(pos, positionCtx, (child) => convert(child, currentChildren.get(childKey(pos, child)), ctx, problems, root, false),
|
|
209
251
|
// EC-748: a value the model wrote in a shape its declaration does not read — nothing
|
|
@@ -211,7 +253,7 @@ function convertChildren(pos, current, ctx, problems, root) {
|
|
|
211
253
|
// object would reach a locked sub-field through the canonicaliser's unwrap). The stored
|
|
212
254
|
// value echoed back, unchanged, is no write; anything else is a problem, and not written.
|
|
213
255
|
(unreadable) => {
|
|
214
|
-
const stored = currentChildren.get(
|
|
256
|
+
const stored = currentChildren.get(childKey(pos, unreadable));
|
|
215
257
|
if (stored !== undefined && canonicalJson(stored) === canonicalJson(unreadable.value))
|
|
216
258
|
return unreadable.value;
|
|
217
259
|
problems.push({ at: unreadable.at, code: 'invalid-field-value', message: unreadableWriteMessage(unreadable, root) });
|
|
@@ -307,54 +349,40 @@ export function declaredWriterLock(declaration) {
|
|
|
307
349
|
return 'read-only';
|
|
308
350
|
return undefined;
|
|
309
351
|
}
|
|
310
|
-
/** {@link lockedChildKey} for a value the traversal cannot read — its own key space (EC-748). */
|
|
311
|
-
function lockedOpaqueKey(parent, parentKey, child) {
|
|
312
|
-
if (child.opaque === 'not-a-static-prop' && child.component) {
|
|
313
|
-
return `opaque:node:${child.component.nodeId}:${child.declaration.name}`;
|
|
314
|
-
}
|
|
315
|
-
return `${parentKey}/opaque:${JSON.stringify(child.at.slice(parent.at.length))}`;
|
|
316
|
-
}
|
|
317
352
|
/**
|
|
318
|
-
* The
|
|
319
|
-
* its
|
|
320
|
-
* entry
|
|
321
|
-
*
|
|
322
|
-
*
|
|
353
|
+
* The locked judge's name for a position: its {@link PositionKey} — a component prop by its
|
|
354
|
+
* NODE within its sequence (ids survive a prose edit that shifts indices), an asset-list
|
|
355
|
+
* entry by its asset id and occurrence, anything else by its location below the parent.
|
|
356
|
+
* EC-774: a position with no key (under a node its sequence holds twice) is named by its
|
|
357
|
+
* LOCATION, in its own key space: {@link withLockedFrom} never takes it from the other value
|
|
358
|
+
* (core's write fold would write it into its twin too), and it counts as kept only where the
|
|
359
|
+
* other value holds the same locked value at the same location — fail closed.
|
|
323
360
|
*/
|
|
324
|
-
function
|
|
325
|
-
|
|
326
|
-
return `node:${child.component.nodeId}:${child.field.name}`;
|
|
327
|
-
const below = child.at.slice(parent.at.length);
|
|
328
|
-
if (parent.field.type === 'asset' && parent.field.multiple !== false && Array.isArray(parent.value)) {
|
|
329
|
-
const index = below[0];
|
|
330
|
-
const entries = parent.value;
|
|
331
|
-
const id = readAssetId(entries[index]);
|
|
332
|
-
const occurrence = entries.slice(0, index).filter((entry) => readAssetId(entry) === id).length;
|
|
333
|
-
return `${parentKey}/asset:${JSON.stringify(id)}#${occurrence}/${JSON.stringify(below.slice(1))}`;
|
|
334
|
-
}
|
|
335
|
-
return `${parentKey}/${JSON.stringify(below)}`;
|
|
361
|
+
function lockedKey(key, at) {
|
|
362
|
+
return key !== undefined ? keyString(key) : JSON.stringify([['twin'], ...at]);
|
|
336
363
|
}
|
|
337
|
-
/** Every topmost writer-locked position at or below `position`, by {@link
|
|
364
|
+
/** Every topmost writer-locked position at or below `position`, by {@link lockedKey}. */
|
|
338
365
|
function lockedPositionsUnder(position, positionCtx) {
|
|
339
366
|
const out = new Map();
|
|
340
367
|
const visit = (pos, key) => {
|
|
341
368
|
const lock = writerLockOf(pos.field);
|
|
342
369
|
if (lock !== undefined) {
|
|
343
|
-
out.set(key, { value: pos.value, lock, at: pos.at });
|
|
370
|
+
out.set(lockedKey(key, pos.at), { value: pos.value, lock, at: pos.at });
|
|
344
371
|
return;
|
|
345
372
|
}
|
|
373
|
+
const keyOf = childKeysUnder(pos, key);
|
|
346
374
|
for (const child of childPositions(pos, positionCtx)) {
|
|
347
375
|
if (!isOpaquePosition(child)) {
|
|
348
|
-
visit(child,
|
|
376
|
+
visit(child, keyOf(child));
|
|
349
377
|
continue;
|
|
350
378
|
}
|
|
351
379
|
// EC-748: what cannot be read is judged by its declaration — never "nothing locked here".
|
|
352
380
|
const lock = declaredWriterLock(child.declaration);
|
|
353
381
|
if (lock !== undefined)
|
|
354
|
-
out.set(
|
|
382
|
+
out.set(lockedKey(keyOf(child), child.at), { value: child.value, lock, at: child.at, opaque: child.opaque });
|
|
355
383
|
}
|
|
356
384
|
};
|
|
357
|
-
visit(position,
|
|
385
|
+
visit(position, []);
|
|
358
386
|
return out;
|
|
359
387
|
}
|
|
360
388
|
/** Whether `entry` sits in `other` unchanged at `key` (a defined value, the same JSON). */
|
|
@@ -411,13 +439,20 @@ export function withLockedFrom(position, current, ctx) {
|
|
|
411
439
|
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
412
440
|
const held = lockedPositionsUnder({ ...position, value: current }, positionCtx);
|
|
413
441
|
const rebuild = (pos, key) => {
|
|
414
|
-
if (writerLockOf(pos.field) !== undefined)
|
|
415
|
-
|
|
442
|
+
if (writerLockOf(pos.field) !== undefined) {
|
|
443
|
+
// EC-774: a position with no key (under a node its sequence holds twice) keeps its own
|
|
444
|
+
// value — taken from `current`, core's write fold would write it into its twin too.
|
|
445
|
+
if (key === undefined)
|
|
446
|
+
return pos.value;
|
|
447
|
+
const name = keyString(key);
|
|
448
|
+
return held.has(name) ? held.get(name).value : pos.value;
|
|
449
|
+
}
|
|
416
450
|
// EC-748: a value `position` holds unreadably is kept as it is — nothing below it can be
|
|
417
451
|
// matched by key — and {@link lockedChanges} counts it lost unless `current` holds it too.
|
|
418
|
-
|
|
452
|
+
const keyOf = childKeysUnder(pos, key);
|
|
453
|
+
return rebuildFromChildren(pos, positionCtx, (child) => rebuild(child, keyOf(child)), (unreadable) => unreadable.value);
|
|
419
454
|
};
|
|
420
|
-
return rebuild(position,
|
|
455
|
+
return rebuild(position, []);
|
|
421
456
|
}
|
|
422
457
|
/**
|
|
423
458
|
* The locked values a write from `position.value` (the CURRENT value) to `next` would lose or
|
|
@@ -625,6 +660,11 @@ export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, l
|
|
|
625
660
|
const root = fieldValuePosition(field, displayed, locale !== undefined ? { locale } : {});
|
|
626
661
|
const isTarget = (position) => position.kind === 'prop' && position.component?.nodeId === nodeId && position.field.name === prop;
|
|
627
662
|
let target;
|
|
663
|
+
// Codex r12 F58: every position the address names. A node id is unique within the document
|
|
664
|
+
// (EC-776: `duplicate-node-id` refuses the publish), but STORED drift may still repeat one, and the
|
|
665
|
+
// rebuild below replaces EVERY match — so an address naming two positions is refused, never
|
|
666
|
+
// written: the defence for stored drift.
|
|
667
|
+
let targets = 0;
|
|
628
668
|
// EC-716: the lock the target sits under — its own, or the nearest locked ancestor's
|
|
629
669
|
// (a component embedded in a read-only rich-text prop, a prop of a locked object…).
|
|
630
670
|
let targetLock;
|
|
@@ -637,6 +677,8 @@ export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, l
|
|
|
637
677
|
if (position.kind === 'prop' && position.component?.nodeId === nodeId) {
|
|
638
678
|
componentId = position.component.componentId;
|
|
639
679
|
declared.push(position.field.name);
|
|
680
|
+
if (position.field.name === prop)
|
|
681
|
+
targets++;
|
|
640
682
|
if (target === undefined && position.field.name === prop) {
|
|
641
683
|
target = position;
|
|
642
684
|
targetLock = lock;
|
|
@@ -659,6 +701,12 @@ export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, l
|
|
|
659
701
|
}
|
|
660
702
|
};
|
|
661
703
|
visit(root, undefined);
|
|
704
|
+
if (targets > 1) {
|
|
705
|
+
return {
|
|
706
|
+
ok: false,
|
|
707
|
+
message: `Component "${nodeId}" (${componentId}) occurs in ${targets} positions of "${field.name}" (a duplicate id), so this address does not name one of them; the assistant cannot change prop "${prop}" here. Change it in the editor.`,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
662
710
|
if (!target && unreadableTarget) {
|
|
663
711
|
return {
|
|
664
712
|
ok: false,
|
|
@@ -710,46 +758,53 @@ function isLocked(position) {
|
|
|
710
758
|
* below it: component text props in a composition and in rich-text embeds, object
|
|
711
759
|
* sub-fields, repeater items, asset placement sub-fields. A writer-locked position
|
|
712
760
|
* (EC-716: `ai.exclude` or `form.readOnly`) and everything below it is not text a writer
|
|
713
|
-
* may touch — not a unit, and so never a skip either. Depth-first, document order.
|
|
761
|
+
* may touch — not a unit, and so never a skip either. Depth-first, document order. Each
|
|
762
|
+
* carries its {@link PositionKey} (EC-774) — the one name the plan and the rebuild share.
|
|
714
763
|
*/
|
|
715
764
|
export function textPositionsOf(position, ctx) {
|
|
716
765
|
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
717
766
|
const out = [];
|
|
718
|
-
const visit = (current) => {
|
|
767
|
+
const visit = (current, key) => {
|
|
719
768
|
if (isLocked(current))
|
|
720
769
|
return;
|
|
721
770
|
if (isTextPosition(current))
|
|
722
|
-
out.push(current);
|
|
771
|
+
out.push({ ...current, key: key && keyString(key) });
|
|
723
772
|
if (current.field.type === 'text')
|
|
724
773
|
return;
|
|
725
774
|
// EC-748: a value the traversal cannot read yields no unit — its text cannot be found
|
|
726
775
|
// (and translate skips a bucket where one may hold locked content: `lockedChanges`).
|
|
776
|
+
const keyOf = childKeysUnder(current, key);
|
|
727
777
|
for (const child of childPositions(current, positionCtx))
|
|
728
778
|
if (!isOpaquePosition(child))
|
|
729
|
-
visit(child);
|
|
779
|
+
visit(child, keyOf(child));
|
|
730
780
|
};
|
|
731
|
-
visit(position);
|
|
781
|
+
visit(position, []);
|
|
732
782
|
return out;
|
|
733
783
|
}
|
|
734
784
|
/**
|
|
735
785
|
* The writing fold over {@link textPositionsOf}: the value at `position` with each text
|
|
736
786
|
* position's value replaced by `replace(textPosition)` — for a sequence, its own value
|
|
737
787
|
* first (its prose), then the text positions below it, read off the REPLACED value (a
|
|
738
|
-
* component keeps its node id, so its props are found again
|
|
739
|
-
* image, a select, a component's structure — comes back as it was
|
|
788
|
+
* component keeps its node id, so its props are found again, under the same key). Every
|
|
789
|
+
* other value — an image, a select, a component's structure — comes back as it was
|
|
790
|
+
* (identity-preserving), and so does a text position with no key (EC-774: `replace` never
|
|
791
|
+
* sees one — it cannot be written apart from its twin).
|
|
740
792
|
*/
|
|
741
793
|
export function rebuildTextPositions(position, ctx, replace) {
|
|
742
794
|
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
743
|
-
const rebuild = (current) => {
|
|
795
|
+
const rebuild = (current, key) => {
|
|
744
796
|
if (isLocked(current))
|
|
745
797
|
return current.value;
|
|
798
|
+
const keyed = key && { ...current, key: keyString(key) };
|
|
746
799
|
if (current.field.type === 'text')
|
|
747
|
-
return replace(current
|
|
748
|
-
const value = isTextPosition(current) ? replace(
|
|
800
|
+
return keyed ? replace(keyed) : current.value;
|
|
801
|
+
const value = keyed && isTextPosition(current) ? replace(keyed) : current.value;
|
|
802
|
+
const replaced = { ...current, value };
|
|
803
|
+
const keyOf = childKeysUnder(replaced, key);
|
|
749
804
|
// EC-748: an unreadable value comes back as it was (it held no unit).
|
|
750
|
-
return rebuildFromChildren(
|
|
805
|
+
return rebuildFromChildren(replaced, positionCtx, (child) => rebuild(child, keyOf(child)), (unreadable) => unreadable.value);
|
|
751
806
|
};
|
|
752
|
-
return rebuild(position);
|
|
807
|
+
return rebuild(position, []);
|
|
753
808
|
}
|
|
754
809
|
// --- Stored form → the read (writer) form --------------------------------------------
|
|
755
810
|
/**
|