@volter/editor-react 0.5.210 → 0.5.212
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/contributions/component-contracts.service.ts +1 -1
- package/dist-node/serving.mjs +14770 -1629
- package/package.json +20 -9
- package/serving/component-prop-worker.ts +3 -19
- package/serving/index.ts +20 -8
- package/serving/source-diagnostics.ts +311 -0
- package/serving/ui-oid-plugin.ts +154 -775
- package/src/source/component-prop-types.ts +6 -62
- package/src/source/drei-detailed-names.ts +24 -0
- package/src/source/oid-transform.ts +166 -376
- package/src/source/plan-csf-story.ts +3 -26
- package/src/source/plan-extract-component.ts +15 -60
- package/src/source/plan-fork-component.ts +25 -92
- package/src/source/plan-named-style.ts +3 -17
- package/src/source/plan-source-edit.ts +9 -40
- package/src/source/r3f-contract-resolver.ts +9 -103
- package/src/source/r3f-physics-binding.ts +28 -186
- package/src/source/r3f-project-contracts.ts +13 -103
- package/src/source/relative-import-specifier.ts +3 -18
- package/src/source/reparent-guard.ts +16 -61
- package/src/source/source-edit-request.ts +2 -20
- package/src/source/ts-ast.ts +5 -95
- package/src/source/utility-class-support.ts +4 -32
- package/src/source/writer.ts +74 -326
- package/src/source/r3f-diagnostic-index.ts +0 -165
- package/src/source/r3f-environment-binding.ts +0 -106
- package/src/source/r3f-joint-binding.ts +0 -228
- package/src/source/r3f-lod-binding.ts +0 -81
- package/src/source/r3f-particle-binding.ts +0 -213
- package/src/source/syntactic-prop-specs.ts +0 -296
package/src/source/writer.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* AST rewrite" trick. Given the original source and an element's location (from the
|
|
4
|
-
* OID index), it edits the inline `style={{…}}` object IN PLACE, preserving all
|
|
5
|
-
* other properties (including dynamic ones), formatting, and comments — and refuses
|
|
6
|
-
* to touch expression-bound (dynamic) values (the literal-vs-dynamic guard). Plus a
|
|
7
|
-
* structural delete. Pure string ops (no parser, like visual-edit/writer.ts).
|
|
8
|
-
*/
|
|
1
|
+
/** Surgical source edits by element offset. Preserve unrelated properties,
|
|
2
|
+
* formatting, and comments, and refuse writes to expression-bound values. */
|
|
9
3
|
|
|
10
4
|
import { UTILITY_CLASSES_UNPROVEN, type UtilityClassSupport } from './utility-class-support';
|
|
11
5
|
|
|
@@ -19,15 +13,8 @@ export interface StyleProperty {
|
|
|
19
13
|
literal: boolean;
|
|
20
14
|
valueStart: number;
|
|
21
15
|
valueEnd: number;
|
|
22
|
-
/**
|
|
23
|
-
* The full `key: value` member span — from the first char of the key through the
|
|
24
|
-
* end of the (trimmed) value, EXCLUDING any leading/trailing whitespace or the
|
|
25
|
-
* separating comma (D-A2, `removeInlineStyle`). `memberEnd` always equals
|
|
26
|
-
* `valueEnd` (the member ends exactly where its value ends); kept as a distinct
|
|
27
|
-
* field for readability at removeInlineStyle's call site.
|
|
28
|
-
*/
|
|
16
|
+
/** Start of the key/value span, excluding surrounding whitespace. */
|
|
29
17
|
memberStart: number;
|
|
30
|
-
memberEnd: number;
|
|
31
18
|
}
|
|
32
19
|
|
|
33
20
|
/** Find the `>` that closes the opening tag starting at `start` (a `<`). */
|
|
@@ -40,12 +27,7 @@ export function findTagEnd(code: string, start: number): number {
|
|
|
40
27
|
if (c === str && code[i - 1] !== '\\') str = null;
|
|
41
28
|
continue;
|
|
42
29
|
}
|
|
43
|
-
//
|
|
44
|
-
// `{…}` expression alike, and an apostrophe in its prose ("the tab's drag
|
|
45
|
-
// handle", "the palette's active ink") otherwise opens a string that
|
|
46
|
-
// swallows the tag's own `>` and every element after it — measured on
|
|
47
|
-
// `GameHierarchy.tsx` and its neighbours. A self-closing `/` is
|
|
48
|
-
// followed by `>`, never by `/` or `*`, so it cannot be mistaken for one.
|
|
30
|
+
// Skip comments between attributes or inside expressions before interpreting quotes.
|
|
49
31
|
if (c === '/' && code[i + 1] === '/') {
|
|
50
32
|
const newline = code.indexOf('\n', i);
|
|
51
33
|
if (newline < 0) return -1;
|
|
@@ -130,7 +112,6 @@ export function analyzeStyleProperties(objBody: string, baseOffset: number): Sty
|
|
|
130
112
|
valueStart,
|
|
131
113
|
valueEnd,
|
|
132
114
|
memberStart: baseOffset + keyStart,
|
|
133
|
-
memberEnd: valueEnd,
|
|
134
115
|
});
|
|
135
116
|
}
|
|
136
117
|
}
|
|
@@ -165,28 +146,7 @@ export interface StyleEditResult {
|
|
|
165
146
|
changed: boolean;
|
|
166
147
|
/** True if the edit was refused because the existing value is a JS expression. */
|
|
167
148
|
dynamic: boolean;
|
|
168
|
-
/**
|
|
169
|
-
* D-A1: true ONLY when `setInlineStyle` took the APPEND branch — the
|
|
170
|
-
* element's style object had no literal (and no dynamic expression) for this prop
|
|
171
|
-
* before the write, e.g. it was absent entirely or spread-derived (`style={{
|
|
172
|
-
* ...vars }}`). False/absent on a replace, a dynamic-value refusal, or a no-op.
|
|
173
|
-
* Undo must know this: replaying the write with the prior value would find the
|
|
174
|
-
* NOW-appended literal and REPLACE it — restoring a hardcoded value into a spot
|
|
175
|
-
* the source never had one — instead of removing the prop to truly restore the
|
|
176
|
-
* pre-edit source.
|
|
177
|
-
*/
|
|
178
|
-
appended?: boolean;
|
|
179
|
-
/**
|
|
180
|
-
* Refused because the property's existing value is a CSS CUSTOM-PROPERTY
|
|
181
|
-
* REFERENCE — `backgroundColor: 'var(--brand-color)'`. Carries the reference
|
|
182
|
-
* text so the refusal can name the token the author would otherwise have lost.
|
|
183
|
-
*
|
|
184
|
-
* A `var()` is a literal string, so the dynamic-expression guard above never
|
|
185
|
-
* saw it: an ordinary colour-picker commit rewrote a design-token binding to
|
|
186
|
-
* `#ff0000`, silently, on one click. Guarding it is the same idiom for the
|
|
187
|
-
* same reason — the source value is a REFERENCE, and replacing it with a
|
|
188
|
-
* literal is a loss no widget can compose back.
|
|
189
|
-
*/
|
|
149
|
+
/** CSS custom-property reference guarded against replacement by a literal. */
|
|
190
150
|
tokenRef?: string;
|
|
191
151
|
}
|
|
192
152
|
|
|
@@ -257,7 +217,7 @@ export function setInlineStyle(
|
|
|
257
217
|
const sep = trimmedBody.length === 0 ? '' : trimmedBody.endsWith(',') ? ' ' : ', ';
|
|
258
218
|
const insertAt = styleObj.innerStart + trimmedBody.length;
|
|
259
219
|
const next = `${code.slice(0, insertAt)}${sep}${prop}: ${valueText}${code.slice(insertAt)}`;
|
|
260
|
-
return { code: next, changed: true, dynamic: false
|
|
220
|
+
return { code: next, changed: true, dynamic: false };
|
|
261
221
|
}
|
|
262
222
|
|
|
263
223
|
// A whole-attribute dynamic style (`style={expr}`, single brace) exists but is
|
|
@@ -274,20 +234,9 @@ export function setInlineStyle(
|
|
|
274
234
|
return { code: next, changed: true, dynamic: false };
|
|
275
235
|
}
|
|
276
236
|
|
|
277
|
-
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* that took `setInlineStyle`'s APPEND branch (`appended: true`). Fixes up the
|
|
281
|
-
* neighbouring separator/trailing comma so the remaining object stays
|
|
282
|
-
* syntactically valid; if `prop` was the object's ONLY content, collapses to an
|
|
283
|
-
* empty `style={{}}` rather than stripping the attribute entirely (keeps redo — a
|
|
284
|
-
* plain re-append — trivially reversible, and avoids re-deriving the
|
|
285
|
-
* whole-attribute insertion position `setInlineStyle`'s no-style-attr branch
|
|
286
|
-
* uses). `changed: false` (and no-op) if `prop` isn't present as a literal member
|
|
287
|
-
* at all — including a DYNAMIC member (mirrors `setInlineStyle`'s
|
|
288
|
-
* literal-vs-dynamic guard: never remove an expression-bound value the caller
|
|
289
|
-
* didn't write).
|
|
290
|
-
*/
|
|
237
|
+
/** Remove a literal inline-style property and repair adjacent separators.
|
|
238
|
+
* An absent or dynamic member is unchanged; removing the only member leaves
|
|
239
|
+
* an empty style object for subsequent writes. */
|
|
291
240
|
export function removeInlineStyle(
|
|
292
241
|
code: string,
|
|
293
242
|
elementStart: number,
|
|
@@ -307,7 +256,7 @@ export function removeInlineStyle(
|
|
|
307
256
|
// Prefer consuming a comma that FOLLOWS this member (it precedes others in the
|
|
308
257
|
// list); else one that PRECEDES it (it is last); else this was the ONLY thing in
|
|
309
258
|
// the object — collapse to `{}` instead.
|
|
310
|
-
let after = existing.
|
|
259
|
+
let after = existing.valueEnd;
|
|
311
260
|
while (after < styleObj.innerEnd && /\s/.test(code[after] ?? '')) after++;
|
|
312
261
|
if (code[after] === ',') {
|
|
313
262
|
let end = after + 1;
|
|
@@ -323,7 +272,7 @@ export function removeInlineStyle(
|
|
|
323
272
|
while (before >= styleObj.innerStart && /\s/.test(code[before] ?? '')) before--;
|
|
324
273
|
if (code[before] === ',') {
|
|
325
274
|
return {
|
|
326
|
-
code: code.slice(0, before) + code.slice(existing.
|
|
275
|
+
code: code.slice(0, before) + code.slice(existing.valueEnd),
|
|
327
276
|
changed: true,
|
|
328
277
|
dynamic: false,
|
|
329
278
|
};
|
|
@@ -376,7 +325,7 @@ function skipJsxExpression(code: string, open: number): number {
|
|
|
376
325
|
if (depth === 0) return i;
|
|
377
326
|
continue;
|
|
378
327
|
}
|
|
379
|
-
// NESTED JSX — `{cond ? <div>the shot's takes</div
|
|
328
|
+
// NESTED JSX — `{cond ? <div>the shot's takes</div>: null}`. Inside the
|
|
380
329
|
// element the quotes are TEXT again, so the element is handed back to
|
|
381
330
|
// `findElementEnd`, whose walk knows that; scanning it here would read
|
|
382
331
|
// that apostrophe as a string exactly like the defect this pair fixes.
|
|
@@ -412,30 +361,9 @@ function startsTag(code: string, i: number): boolean {
|
|
|
412
361
|
);
|
|
413
362
|
}
|
|
414
363
|
|
|
415
|
-
/**
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* **A QUOTE BETWEEN TWO TAGS IS TEXT, NOT A STRING**, and reading it as one
|
|
420
|
-
* destroyed source. This walk used to flip into "inside a string" on any `'`,
|
|
421
|
-
* `"` or backtick it met, exactly as {@link findTagEnd} does INSIDE a tag —
|
|
422
|
-
* but between tags those characters are JSX children, and an apostrophe is an
|
|
423
|
-
* ordinary English one. Measured 2026-09-19 on the game template's own
|
|
424
|
-
* `MainScene.tsx`: the JSX comment inside `<group name="Light Rig">` says
|
|
425
|
-
* *"The sun's position… that component's header… than this scene's."* — three
|
|
426
|
-
* apostrophes, so the walk was left mid-"string", never saw `</group>`, and
|
|
427
|
-
* answered with the END OF THE FILE. Every consumer of that answer takes the
|
|
428
|
-
* whole rest of the component: Delete on the Light Rig row would have removed
|
|
429
|
-
* it, copy would have copied it, and `group` wrote a structurally broken file
|
|
430
|
-
* (that is how this was found).
|
|
431
|
-
*
|
|
432
|
-
* So: enter a string only inside a `{…}` expression container, where
|
|
433
|
-
* {@link skipJsxExpression} also handles `//` and block comments — which is
|
|
434
|
-
* what makes a JSX comment's prose invisible to this walk — and step over any
|
|
435
|
-
* OTHER element's opening tag with {@link findTagEnd}, so its attributes'
|
|
436
|
-
* quotes and braces never reach here either. Only our own tag moves `depth`,
|
|
437
|
-
* so another element's children stay in this walk and cost nothing.
|
|
438
|
-
*/
|
|
364
|
+
/** Find a JSX element's exclusive end, balancing same-tag nesting. Quotes
|
|
365
|
+
* between tags are text; expression containers and other opening tags are
|
|
366
|
+
* skipped with their own comment and string rules. */
|
|
439
367
|
export function findElementEnd(code: string, start: number): number {
|
|
440
368
|
const tagEnd = findTagEnd(code, start);
|
|
441
369
|
if (tagEnd < 0) return -1;
|
|
@@ -465,7 +393,7 @@ export function findElementEnd(code: string, start: number): number {
|
|
|
465
393
|
}
|
|
466
394
|
if (code.startsWith(`<${tag}`, i) && /[\s/>]/.test(code[i + 1 + tag.length] ?? '')) {
|
|
467
395
|
// A nested SAME-TAG element: only a non-self-closing one increments depth.
|
|
468
|
-
// A self-closing `<tag
|
|
396
|
+
// A self-closing `<tag.../>` opens+closes in one tag and must NOT bump depth
|
|
469
397
|
// (else the balancer runs past the real closing tag and over-deletes).
|
|
470
398
|
const nestedTagEnd = findTagEnd(code, i);
|
|
471
399
|
if (nestedTagEnd < 0) {
|
|
@@ -486,17 +414,7 @@ export function findElementEnd(code: string, start: number): number {
|
|
|
486
414
|
return i;
|
|
487
415
|
}
|
|
488
416
|
|
|
489
|
-
// ---
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Encode a CSS prop:value as a Tailwind-style utility class. Numeric/color values
|
|
493
|
-
* use ARBITRARY values (`w-[200px]`, `bg-[#059669]`) so the round-trip is LOSSLESS
|
|
494
|
-
* (a named utility like `bg-emerald-600` would NOT recover the exact hex). Enums map
|
|
495
|
-
* to named utilities.
|
|
496
|
-
*/
|
|
497
|
-
// Cap 6 (React visual-edit parity): broadened to the grouped-inspector property set. Enum
|
|
498
|
-
// values map to NAMED utilities; each named utility string is unique across groups so the
|
|
499
|
-
// ENUM_GROUP reverse map (dedupe) stays unambiguous.
|
|
417
|
+
// --- Style routing: inline declarations and className utilities ---
|
|
500
418
|
const ENUM_UTILITIES: Record<string, Record<string, string>> = {
|
|
501
419
|
display: {
|
|
502
420
|
flex: 'flex',
|
|
@@ -588,7 +506,7 @@ const ENUM_GROUP: Record<string, string> = (() => {
|
|
|
588
506
|
const ARB_MAP: Record<string, { prefix: string; hint?: 'color' | 'length' }> = {
|
|
589
507
|
backgroundColor: { prefix: 'bg' },
|
|
590
508
|
// `border` covers both border-color and border-width, so — like `text-` — they use type
|
|
591
|
-
// hints to stay unambiguous for property-based dedupe
|
|
509
|
+
// hints to stay unambiguous for property-based dedupe.
|
|
592
510
|
borderColor: { prefix: 'border', hint: 'color' },
|
|
593
511
|
borderWidth: { prefix: 'border', hint: 'length' },
|
|
594
512
|
color: { prefix: 'text', hint: 'color' },
|
|
@@ -652,18 +570,8 @@ function classGroup(cls: string): string {
|
|
|
652
570
|
return hintMatch ? `${arb}${hintMatch[1]}:` : arb;
|
|
653
571
|
}
|
|
654
572
|
|
|
655
|
-
/**
|
|
656
|
-
*
|
|
657
|
-
* attribute.
|
|
658
|
-
*
|
|
659
|
-
* D-A1 assessment: unlike `setInlineStyle`, this function has NO
|
|
660
|
-
* distinguishable append-vs-replace branch to surface as `appended` — it
|
|
661
|
-
* always does the SAME operation (filter out any class in the target's dedupe
|
|
662
|
-
* GROUP, then push the new class), regardless of whether a same-group class
|
|
663
|
-
* existed before. Scoped OUT of this wave's fix: see the design doc's SHIPPED
|
|
664
|
-
* note for why the className route's undo correctness is a separate, broader
|
|
665
|
-
* pre-existing gap, not this bug.
|
|
666
|
-
*/
|
|
573
|
+
/** Merge a utility class into a literal className, replacing classes from
|
|
574
|
+
* the same deduplication group, or add the attribute when absent. */
|
|
667
575
|
export function setClassName(code: string, elementStart: number, cls: string): StyleEditResult {
|
|
668
576
|
const tagEnd = findTagEnd(code, elementStart);
|
|
669
577
|
if (tagEnd < 0) return { code, changed: false, dynamic: false };
|
|
@@ -695,26 +603,9 @@ export function setClassName(code: string, elementStart: number, cls: string): S
|
|
|
695
603
|
};
|
|
696
604
|
}
|
|
697
605
|
|
|
698
|
-
/**
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* 1. the element has a literal `className` the class can be merged into, AND
|
|
703
|
-
* 2. the PROJECT was observed to interpret utility classes
|
|
704
|
-
* (`support.supported` — see `utility-class-support.ts` for the probe and
|
|
705
|
-
* why it is strict).
|
|
706
|
-
*
|
|
707
|
-
* Without (2) a `bg-[#059669]` written into a plain-CSS project is inert: it
|
|
708
|
-
* paints nothing in the editor after a remount and nothing in the game's own
|
|
709
|
-
* build, because no transformer exists on either side to compile it. So no
|
|
710
|
-
* evidence ⇒ the inline `style={{…}}` route, which paints in EVERY project,
|
|
711
|
-
* leaving the element's literal className untouched.
|
|
712
|
-
*
|
|
713
|
-
* When neither target is honest — the element's style attribute is a dynamic
|
|
714
|
-
* expression and there is no compiled class route to fall back to — this
|
|
715
|
-
* REFUSES with a named reason (`changed: false`, `dynamic: true`, `error`)
|
|
716
|
-
* rather than writing bytes that never paint.
|
|
717
|
-
*/
|
|
606
|
+
/** Route to utility classes only for a literal className and proved project
|
|
607
|
+
* support. Otherwise write inline CSS. Refuse a dynamic style expression when
|
|
608
|
+
* there is no supported class route. */
|
|
718
609
|
export function writeStyleAuto(
|
|
719
610
|
code: string,
|
|
720
611
|
elementStart: number,
|
|
@@ -754,7 +645,7 @@ export function writeStyleAuto(
|
|
|
754
645
|
return { ...inline, route: 'inline' };
|
|
755
646
|
}
|
|
756
647
|
|
|
757
|
-
//
|
|
648
|
+
// CSS-file editing.
|
|
758
649
|
|
|
759
650
|
/** camelCase CSS prop -> kebab-case, for matching a declaration in a CSS rule body. */
|
|
760
651
|
export function cssCamelToKebab(s: string): string {
|
|
@@ -802,9 +693,8 @@ function findRuleBody(
|
|
|
802
693
|
|
|
803
694
|
/**
|
|
804
695
|
* Surgically replace (or append) a property value inside a CSS RULE BODY, editing the
|
|
805
|
-
* source in place and preserving
|
|
806
|
-
*
|
|
807
|
-
* present in the file (i.e. it's GENERATED CSS — a Tailwind utility, styled-components —
|
|
696
|
+
* source in place and preserving other declarations and formatting. Returns the edited
|
|
697
|
+
* source, or null when the selector is not present in the file (i.e. it's GENERATED CSS — a Tailwind utility, styled-components —
|
|
808
698
|
* that must not be hand-edited; the caller falls back to class/inline routing).
|
|
809
699
|
*/
|
|
810
700
|
export function surgicalCssEdit(
|
|
@@ -846,14 +736,8 @@ export function surgicalCssEdit(
|
|
|
846
736
|
return cssSource.slice(0, bodyEnd) + insertion + cssSource.slice(bodyEnd);
|
|
847
737
|
}
|
|
848
738
|
|
|
849
|
-
/**
|
|
850
|
-
*
|
|
851
|
-
* create — `selector`'s rule INSIDE a top-level `@media <media>` block,
|
|
852
|
-
* creating the block itself when the stylesheet has none. The source keeps
|
|
853
|
-
* real `@media` truth; the dev server's serve-time rewrite is what turns it
|
|
854
|
-
* into a per-frame container query for preview. Conditions compare
|
|
855
|
-
* whitespace-normalized, so `(max-width:767px)` finds `(max-width: 767px)`.
|
|
856
|
-
*/
|
|
739
|
+
/** Edit or create a selector inside a top-level media block. Conditions
|
|
740
|
+
* compare after whitespace normalization; source remains ordinary CSS. */
|
|
857
741
|
export function surgicalCssEditInMedia(
|
|
858
742
|
cssSource: string,
|
|
859
743
|
media: string,
|
|
@@ -911,21 +795,19 @@ export interface CssRuleTarget {
|
|
|
911
795
|
|
|
912
796
|
/**
|
|
913
797
|
* Pick the target rule for a property write: the LAST matched rule that declares the
|
|
914
|
-
* property (cascade — last declaration wins),
|
|
915
|
-
* :1198. Returns the rule + the prior value (for the undo inverse), or null when no
|
|
798
|
+
* property (cascade — last declaration wins). Returns the rule, or null when no
|
|
916
799
|
* matched rule declares the property (⇒ caller routes to class/inline instead).
|
|
917
800
|
*/
|
|
918
801
|
export function pickCssRuleTarget(
|
|
919
802
|
matchedRules: readonly CssRuleTarget[],
|
|
920
803
|
prop: string,
|
|
921
|
-
):
|
|
804
|
+
): CssRuleTarget | null {
|
|
922
805
|
const kebabProp = cssCamelToKebab(prop);
|
|
923
806
|
let target: CssRuleTarget | null = null;
|
|
924
807
|
for (const rule of matchedRules) {
|
|
925
808
|
if (kebabProp in rule.properties) target = rule;
|
|
926
809
|
}
|
|
927
|
-
|
|
928
|
-
return { rule: target, prevValue: target.properties[kebabProp] ?? '' };
|
|
810
|
+
return target;
|
|
929
811
|
}
|
|
930
812
|
|
|
931
813
|
/**
|
|
@@ -1057,8 +939,7 @@ export function removeClassNameToken(
|
|
|
1057
939
|
return { code: code.slice(0, attrStart) + code.slice(attrEnd), changed: true, dynamic: false };
|
|
1058
940
|
}
|
|
1059
941
|
|
|
1060
|
-
//
|
|
1061
|
-
// editTextContent :861) ---
|
|
942
|
+
// Text-content editing.
|
|
1062
943
|
|
|
1063
944
|
/** The `<`-relative content range (between the opening tag's `>` and the closing `</`)
|
|
1064
945
|
* of the element at `elementStart`, plus whether it is a pure-text (editable) body. */
|
|
@@ -1083,19 +964,6 @@ function findTextContentRange(
|
|
|
1083
964
|
return { contentStart, closingTagStart, content: code.slice(contentStart, closingTagStart) };
|
|
1084
965
|
}
|
|
1085
966
|
|
|
1086
|
-
/**
|
|
1087
|
-
* Return an element's editable text — its content trimmed — IFF the body is pure text (no
|
|
1088
|
-
* `{expression}` and no child `<element>`); else null (visual-edit `getEditableText` :817).
|
|
1089
|
-
* This is the double-click-to-edit gate: only a leaf text node is editable in place.
|
|
1090
|
-
*/
|
|
1091
|
-
export function getEditableText(code: string, elementStart: number): string | null {
|
|
1092
|
-
const rng = findTextContentRange(code, elementStart);
|
|
1093
|
-
if (!rng) return null;
|
|
1094
|
-
if (rng.content.includes('{') || rng.content.includes('<')) return null; // dynamic/children
|
|
1095
|
-
const trimmed = rng.content.trim();
|
|
1096
|
-
return trimmed ? trimmed : null;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
967
|
/** Escape the JSX-structural characters so replacement text can't break the source tree
|
|
1100
968
|
* (`<`/`{`/`}` become entities; `&` first so we don't double-encode). */
|
|
1101
969
|
function escapeJsxText(text: string): string {
|
|
@@ -1107,9 +975,8 @@ function escapeJsxText(text: string): string {
|
|
|
1107
975
|
}
|
|
1108
976
|
|
|
1109
977
|
/**
|
|
1110
|
-
* Replace an element's text
|
|
1111
|
-
*
|
|
1112
|
-
* (guard) when the body is not pure text (`dynamic: true`), exactly like the read gate.
|
|
978
|
+
* Replace only an element's trimmed text, preserving surrounding whitespace.
|
|
979
|
+
* Refuse bodies containing expressions or child elements (`dynamic: true`).
|
|
1113
980
|
*/
|
|
1114
981
|
export function editTextContent(
|
|
1115
982
|
code: string,
|
|
@@ -1129,8 +996,7 @@ export function editTextContent(
|
|
|
1129
996
|
return { code: next, changed: next !== code, dynamic: false };
|
|
1130
997
|
}
|
|
1131
998
|
|
|
1132
|
-
//
|
|
1133
|
-
// :913 / writePropChange :1018 / fetchPropAnalysis :1069) ---
|
|
999
|
+
// JSX prop and attribute editing.
|
|
1134
1000
|
|
|
1135
1001
|
export interface JsxAttrInfo {
|
|
1136
1002
|
name: string;
|
|
@@ -1181,14 +1047,6 @@ function readQuotedValue(code: string, openQuote: number, delim: string): [numbe
|
|
|
1181
1047
|
const NUMBER_LITERAL_SOURCE = String.raw`[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?`;
|
|
1182
1048
|
const NUMBER_LITERAL_RE = new RegExp(`^${NUMBER_LITERAL_SOURCE}$`);
|
|
1183
1049
|
|
|
1184
|
-
/**
|
|
1185
|
-
* A NUMBER-TUPLE literal (`[0, 1.5, 0]`, any arity incl. empty,
|
|
1186
|
-
* ints/floats/negatives, whitespace/newlines/trailing comma tolerated). This
|
|
1187
|
-
* is the 3D value shape R3F props use for
|
|
1188
|
-
* `position`/`rotation`/`scale`/`args`; a tuple containing anything but a
|
|
1189
|
-
* plain signed number literal (`[x, 1, 0]`, `[f(), 0]`) is DYNAMIC and stays
|
|
1190
|
-
* behind the literal-vs-dynamic guard.
|
|
1191
|
-
*/
|
|
1192
1050
|
/**
|
|
1193
1051
|
* A NUMBER-POINT literal (`{ x: 1.2, y: 0.8 }`): an object whose properties are all plain
|
|
1194
1052
|
* identifiers with number values — the `PointData` a 2D scene's `scale` or `pivot` takes, and the
|
|
@@ -1207,6 +1065,14 @@ export function isNumberPointLiteral(raw: string): boolean {
|
|
|
1207
1065
|
});
|
|
1208
1066
|
}
|
|
1209
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* A NUMBER-TUPLE literal (`[0, 1.5, 0]`, any arity incl. empty,
|
|
1070
|
+
* ints/floats/negatives, whitespace/newlines/trailing comma tolerated). This
|
|
1071
|
+
* is the 3D value shape R3F props use for
|
|
1072
|
+
* `position`/`rotation`/`scale`/`args`; a tuple containing anything but a
|
|
1073
|
+
* plain signed number literal (`[x, 1, 0]`, `[f(), 0]`) is DYNAMIC and stays
|
|
1074
|
+
* behind the literal-vs-dynamic guard.
|
|
1075
|
+
*/
|
|
1210
1076
|
export function isNumberTupleLiteral(raw: string): boolean {
|
|
1211
1077
|
const trimmed = raw.trim();
|
|
1212
1078
|
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return false;
|
|
@@ -1221,8 +1087,8 @@ export function isNumberTupleLiteral(raw: string): boolean {
|
|
|
1221
1087
|
const LITERAL_EXPR_RE = new RegExp(`^(?:${NUMBER_LITERAL_SOURCE}|true|false|'[^']*'|"[^"]*")$`);
|
|
1222
1088
|
|
|
1223
1089
|
/**
|
|
1224
|
-
* Parse the attributes of a JSX opening tag `<Tag
|
|
1225
|
-
*
|
|
1090
|
+
* Parse the attributes of a JSX opening tag `<Tag …>`: string values,
|
|
1091
|
+
* `{expression}` values (literal detection), boolean
|
|
1226
1092
|
* shorthand, and `{...spread}` (skipped). Pure string scan — no parser.
|
|
1227
1093
|
*/
|
|
1228
1094
|
export function analyzeJsxAttributes(
|
|
@@ -1328,55 +1194,23 @@ function makeAttr(
|
|
|
1328
1194
|
};
|
|
1329
1195
|
}
|
|
1330
1196
|
|
|
1331
|
-
/**
|
|
1332
|
-
* set + on* handlers — event handlers are always dynamic). */
|
|
1197
|
+
/** Internal props and event handlers excluded from component-prop editing. */
|
|
1333
1198
|
const PROP_SKIP = new Set(['key', 'ref', 'data-oid', 'style', 'className', 'children']);
|
|
1334
1199
|
const isSkippedProp = (name: string): boolean => PROP_SKIP.has(name) || /^on[A-Z]/.test(name);
|
|
1335
1200
|
|
|
1336
|
-
/**
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
export function analyzeProps(
|
|
1340
|
-
code: string,
|
|
1341
|
-
elementStart: number,
|
|
1342
|
-
): { editable: string[]; dynamic: string[] } {
|
|
1343
|
-
const empty = { editable: [], dynamic: [] };
|
|
1344
|
-
if (code[elementStart] !== '<') return empty;
|
|
1345
|
-
const tagEnd = findTagEnd(code, elementStart);
|
|
1346
|
-
if (tagEnd < 0) return empty;
|
|
1347
|
-
const editable: string[] = [];
|
|
1348
|
-
const dynamic: string[] = [];
|
|
1349
|
-
for (const attr of analyzeJsxAttributes(code, elementStart, tagEnd)) {
|
|
1350
|
-
if (isSkippedProp(attr.name)) continue;
|
|
1351
|
-
(attr.isLiteral ? editable : dynamic).push(attr.name);
|
|
1352
|
-
}
|
|
1353
|
-
return { editable, dynamic };
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
/** Format the replacement for a prop write, matching the original value's shape (number/
|
|
1357
|
-
* boolean/tuple/quoted expression, or a plain string attribute). Returns `null` when the
|
|
1358
|
-
* new value cannot take the original's shape (a non-tuple replacing a tuple) — the caller
|
|
1359
|
-
* refuses rather than clobbering a structured literal with a quoted string.
|
|
1360
|
-
*
|
|
1361
|
-
* `allowShapeUpgrade` (R2 — OPT-IN, default off so react-dom and every existing path are
|
|
1362
|
-
* byte-identical): permits exactly ONE shape change — a plain number literal replaced by
|
|
1363
|
-
* a number-TUPLE literal for the same prop (the R3F `scale={1.5}` scalar shorthand
|
|
1364
|
-
* upgraded to `scale={[x, y, z]}` by a non-uniform gizmo drag). Dynamic values stay
|
|
1365
|
-
* refused upstream (the `isLiteral` guard in {@link writePropChange} runs before this
|
|
1366
|
-
* function is ever reached), and no other shape pair is upgraded. */
|
|
1201
|
+
/** Match a replacement to the original literal shape. An explicit
|
|
1202
|
+
* allowShapeUpgrade permits a scalar number to become a numeric tuple or point;
|
|
1203
|
+
* unsupported shapes return null. Dynamic values are refused by the caller. */
|
|
1367
1204
|
function formatPropReplacement(
|
|
1368
1205
|
attr: JsxAttrInfo,
|
|
1369
1206
|
newValue: string,
|
|
1370
|
-
allowShapeUpgrade
|
|
1207
|
+
allowShapeUpgrade: boolean,
|
|
1371
1208
|
): string | null {
|
|
1372
1209
|
if (!attr.isExpression) return newValue; // string attribute — content between the quotes
|
|
1373
1210
|
const raw = attr.rawValue;
|
|
1374
1211
|
if (NUMBER_LITERAL_RE.test(raw)) {
|
|
1375
1212
|
if (isNumberTupleLiteral(newValue) || isNumberPointLiteral(newValue)) {
|
|
1376
|
-
//
|
|
1377
|
-
// under the explicit opt-in; otherwise refused outright (never the
|
|
1378
|
-
// quoted-string fallback below, which would clobber `scale={1.5}` with
|
|
1379
|
-
// a junk string literal).
|
|
1213
|
+
// Scalar-to-tuple changes require explicit permission.
|
|
1380
1214
|
return allowShapeUpgrade ? newValue.trim() : null;
|
|
1381
1215
|
}
|
|
1382
1216
|
const num = Number(newValue);
|
|
@@ -1385,8 +1219,7 @@ function formatPropReplacement(
|
|
|
1385
1219
|
if (raw === 'true' || raw === 'false') {
|
|
1386
1220
|
return newValue === 'true' || newValue === 'false' ? newValue : `'${newValue}'`;
|
|
1387
1221
|
}
|
|
1388
|
-
//
|
|
1389
|
-
// literal, verbatim (the caller controls spacing inside the brackets).
|
|
1222
|
+
// Replace a number tuple only with another number tuple, preserving its spacing.
|
|
1390
1223
|
if (isNumberTupleLiteral(raw)) {
|
|
1391
1224
|
return isNumberTupleLiteral(newValue) ? newValue.trim() : null;
|
|
1392
1225
|
}
|
|
@@ -1418,28 +1251,8 @@ function formatNewAttr(propName: string, newValue: string): string {
|
|
|
1418
1251
|
return `${propName}="${newValue.replace(/"/g, '"')}"`;
|
|
1419
1252
|
}
|
|
1420
1253
|
|
|
1421
|
-
/**
|
|
1422
|
-
*
|
|
1423
|
-
* `writePropChange` :1018). Refuses (guards) a dynamic (non-literal) prop and skips the
|
|
1424
|
-
* event-handler / structural props. `elementStart` is the call-site element's `<`.
|
|
1425
|
-
*
|
|
1426
|
-
* `opts.addIfMissing` (W2 — OPT-IN, default off so react-dom prop-write behavior is
|
|
1427
|
-
* unchanged): when the prop is absent from the opening tag, APPEND it (` prop={value}`
|
|
1428
|
-
* / ` prop="value"`) before the tag end instead of no-op'ing — the R3F gizmo path needs
|
|
1429
|
-
* this for a mesh whose source never authored a `position`/`rotation`/`scale` tuple
|
|
1430
|
-
* yet.
|
|
1431
|
-
*
|
|
1432
|
-
* `opts.allowShapeUpgrade` (R2 — OPT-IN, default off): permit a number-tuple literal to
|
|
1433
|
-
* replace a plain NUMBER literal for the same prop (scalar→tuple upgrade; see {@link
|
|
1434
|
-
* formatPropReplacement}). The R3F adapter passes it only for `scale` writes coming
|
|
1435
|
-
* from a non-uniform gizmo drag.
|
|
1436
|
-
*/
|
|
1437
|
-
/**
|
|
1438
|
-
* Where to put a new attribute on a tag whose attributes each occupy their own
|
|
1439
|
-
* line, and with what indentation — or null when the tag is single-line and the
|
|
1440
|
-
* inline insert is right. Keyed off the LAST attribute actually starting a
|
|
1441
|
-
* line, so a tag mixing both styles follows its own majority tail.
|
|
1442
|
-
*/
|
|
1254
|
+
/** Find the insertion position and indentation for a new multiline JSX
|
|
1255
|
+
* attribute. Return null for a tag that uses inline attributes. */
|
|
1443
1256
|
function ownLineAttributeInsert(
|
|
1444
1257
|
code: string,
|
|
1445
1258
|
elementStart: number,
|
|
@@ -1558,9 +1371,7 @@ export function removePropAttribute(
|
|
|
1558
1371
|
return { code: next, changed: next !== code, dynamic: false };
|
|
1559
1372
|
}
|
|
1560
1373
|
|
|
1561
|
-
//
|
|
1562
|
-
// duplicateElement :559 / wrapElement :634 / unwrapElement :679 /
|
|
1563
|
-
// reparentElement :743) ---
|
|
1374
|
+
// Source structural operations.
|
|
1564
1375
|
|
|
1565
1376
|
export interface StructEditResult {
|
|
1566
1377
|
code: string;
|
|
@@ -1586,8 +1397,7 @@ function lineBlockRange(
|
|
|
1586
1397
|
return { lineStart, lineEnd };
|
|
1587
1398
|
}
|
|
1588
1399
|
|
|
1589
|
-
/**
|
|
1590
|
-
* backwards scan from the element end). */
|
|
1400
|
+
/** Find a parent's closing tag by scanning backward from its element end. */
|
|
1591
1401
|
function closingTagStartOf(code: string, parentStart: number): number | null {
|
|
1592
1402
|
const parentEnd = findElementEnd(code, parentStart);
|
|
1593
1403
|
if (parentEnd < 0) return null;
|
|
@@ -1630,32 +1440,8 @@ function offsetElementPosition(
|
|
|
1630
1440
|
).code;
|
|
1631
1441
|
}
|
|
1632
1442
|
|
|
1633
|
-
/**
|
|
1634
|
-
*
|
|
1635
|
-
* moves as a unit, so relative arrangement inside the copy survives. A paste
|
|
1636
|
-
* that landed byte-identically ON its source read as "nothing was pasted"
|
|
1637
|
-
* to a human tester until they happened to drag the top one off.
|
|
1638
|
-
*/
|
|
1639
|
-
export function offsetSnippetPositions(
|
|
1640
|
-
snippet: string,
|
|
1641
|
-
offset: readonly [number, number, number],
|
|
1642
|
-
): string {
|
|
1643
|
-
let out = snippet;
|
|
1644
|
-
let searchFrom = 0;
|
|
1645
|
-
while (searchFrom < out.length) {
|
|
1646
|
-
const start = out.indexOf('<', searchFrom);
|
|
1647
|
-
if (start < 0) break;
|
|
1648
|
-
if (findElementEnd(out, start) < 0) break;
|
|
1649
|
-
out = offsetElementPosition(out, start, offset);
|
|
1650
|
-
const end = findElementEnd(out, start);
|
|
1651
|
-
if (end <= start) break;
|
|
1652
|
-
searchFrom = end + 1;
|
|
1653
|
-
}
|
|
1654
|
-
return out;
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
/** Duplicate the element's line block, inserting the copy immediately after it (visual-edit
|
|
1658
|
-
* `duplicateElement` :559). `rewrite` edits the copy only — see {@link DuplicateRewrite}. */
|
|
1443
|
+
/** Duplicate the element's line block immediately after it.
|
|
1444
|
+
* `rewrite` edits only the copy — see {@link DuplicateRewrite}. */
|
|
1659
1445
|
export function duplicateElement(
|
|
1660
1446
|
code: string,
|
|
1661
1447
|
elementStart: number,
|
|
@@ -1677,7 +1463,7 @@ export function duplicateElement(
|
|
|
1677
1463
|
}
|
|
1678
1464
|
|
|
1679
1465
|
/** Wrap the element in a new `<wrapperTag>…</wrapperTag>` container, re-indenting the
|
|
1680
|
-
* wrapped block one level
|
|
1466
|
+
* wrapped block one level. */
|
|
1681
1467
|
export function wrapElement(
|
|
1682
1468
|
code: string,
|
|
1683
1469
|
elementStart: number,
|
|
@@ -1695,14 +1481,13 @@ export function wrapElement(
|
|
|
1695
1481
|
return { code: code.slice(0, r.lineStart) + wrapper + code.slice(r.lineEnd), changed: true };
|
|
1696
1482
|
}
|
|
1697
1483
|
|
|
1698
|
-
/** Unwrap the element
|
|
1699
|
-
*
|
|
1484
|
+
/** Unwrap the element, replacing it with its children dedented one level.
|
|
1485
|
+
* A self-closing element simply deletes.
|
|
1700
1486
|
*
|
|
1701
1487
|
* For the LINE-BLOCK shape — the opening tag ends its line and the closing tag sits alone
|
|
1702
1488
|
* on its own line, exactly what {@link wrapElement} writes — the newline after the opening
|
|
1703
1489
|
* tag and the closing tag's leading indent are consumed too, making unwrap the exact BYTE
|
|
1704
|
-
* INVERSE of wrap
|
|
1705
|
-
* merged into the following line). Inline children keep the original splice behavior. */
|
|
1490
|
+
* INVERSE of wrap. Inline children are spliced without line-boundary adjustment. */
|
|
1706
1491
|
export function unwrapElement(code: string, elementStart: number): StructEditResult {
|
|
1707
1492
|
if (code[elementStart] !== '<') return { code, changed: false };
|
|
1708
1493
|
const end = findElementEnd(code, elementStart);
|
|
@@ -1735,16 +1520,9 @@ export function unwrapElement(code: string, elementStart: number): StructEditRes
|
|
|
1735
1520
|
return { code: code.slice(0, r.lineStart) + dedented + code.slice(r.lineEnd), changed: true };
|
|
1736
1521
|
}
|
|
1737
1522
|
|
|
1738
|
-
/** Insert a
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
* `snippet` (R3 — optional; `tag` is ignored when it is provided): a MULTI-LINE JSX
|
|
1742
|
-
* snippet inserted verbatim instead of the bare `<tag />`, each line re-indented one
|
|
1743
|
-
* level under the parent (the R3F preset-create path needs a `<mesh>` with
|
|
1744
|
-
* geometry+material children, not an empty tag). The snippet is authored flush-left with
|
|
1745
|
-
* its own 2-space internal nesting; blank lines stay blank (no trailing whitespace).
|
|
1746
|
-
* Must start with `<` — anything else is refused (`changed: false`), the same
|
|
1747
|
-
* honest-refusal convention every other writer op uses. */
|
|
1523
|
+
/** Insert a child before the closing tag, matching child indentation.
|
|
1524
|
+
* A supplied JSX snippet replaces the bare tag and is reindented by line;
|
|
1525
|
+
* blank lines remain blank. Refuse snippets that do not start with <. */
|
|
1748
1526
|
export function insertChildElement(
|
|
1749
1527
|
code: string,
|
|
1750
1528
|
parentStart: number,
|
|
@@ -1835,8 +1613,8 @@ export function insertSiblingElement(
|
|
|
1835
1613
|
}
|
|
1836
1614
|
|
|
1837
1615
|
/** Reorder the element among its siblings: before `beforeStart` (an OID-resolved sibling
|
|
1838
|
-
* offset), or
|
|
1839
|
-
*
|
|
1616
|
+
* offset), or at the end of `parentStart` when `beforeStart` is null.
|
|
1617
|
+
* All offsets refer to the same source snapshot. */
|
|
1840
1618
|
export function reorderChild(
|
|
1841
1619
|
code: string,
|
|
1842
1620
|
elementStart: number,
|
|
@@ -1869,20 +1647,8 @@ export function reorderChild(
|
|
|
1869
1647
|
return { code: without.slice(0, adj) + block + without.slice(adj), changed: true };
|
|
1870
1648
|
}
|
|
1871
1649
|
|
|
1872
|
-
/**
|
|
1873
|
-
*
|
|
1874
|
-
* authored children slot to receive one.
|
|
1875
|
-
*
|
|
1876
|
-
* Why this exists (R3F reparent hardening, R3): in idiomatic R3F source almost every
|
|
1877
|
-
* hierarchy row IS a self-closing component callsite (`<JumpPad name="West" … />`), and
|
|
1878
|
-
* {@link reparentElement}'s `closingTagStartOf` scan finds the element's OWN `<` for such
|
|
1879
|
-
* a tag — so "drop into this row" silently became "insert a PRECEDING SIBLING". Opening
|
|
1880
|
-
* the slot is the honest edit an author would make by hand; it writes no value and
|
|
1881
|
-
* invents no data, it only gives the tag a body. The caller
|
|
1882
|
-
* (`reparent-guard.ts`) has already proven the destination renders its children.
|
|
1883
|
-
*
|
|
1884
|
-
* Refuses (`changed: false`) anything that is not a well-formed self-closing tag.
|
|
1885
|
-
*/
|
|
1650
|
+
/** Open a well-formed self-closing tag into a paired tag with a children
|
|
1651
|
+
* slot. The caller must establish that the destination renders children. */
|
|
1886
1652
|
export function openChildrenSlot(code: string, elementStart: number): StructEditResult {
|
|
1887
1653
|
if (code[elementStart] !== '<') return { code, changed: false };
|
|
1888
1654
|
const tagEnd = findTagEnd(code, elementStart);
|
|
@@ -1904,7 +1670,7 @@ export function openChildrenSlot(code: string, elementStart: number): StructEdit
|
|
|
1904
1670
|
}
|
|
1905
1671
|
|
|
1906
1672
|
/** Reparent the element to be the last child of `parentStart`, re-indented to the parent's
|
|
1907
|
-
* child level
|
|
1673
|
+
* child level. The caller enforces same-file moves.
|
|
1908
1674
|
*
|
|
1909
1675
|
* The R3F hierarchy calls this through `planReparent` (`reparent-guard.ts`), which owns
|
|
1910
1676
|
* the scope/transform/legality gates; this function stays the raw textual move. */
|
|
@@ -1943,7 +1709,7 @@ export function reparentElement(
|
|
|
1943
1709
|
};
|
|
1944
1710
|
}
|
|
1945
1711
|
|
|
1946
|
-
/** Delete the element
|
|
1712
|
+
/** Delete the element beginning at elementStart. */
|
|
1947
1713
|
export function deleteElement(
|
|
1948
1714
|
code: string,
|
|
1949
1715
|
elementStart: number,
|
|
@@ -1958,27 +1724,9 @@ export function deleteElement(
|
|
|
1958
1724
|
return { code: code.slice(0, s) + code.slice(e), changed: true };
|
|
1959
1725
|
}
|
|
1960
1726
|
|
|
1961
|
-
/**
|
|
1962
|
-
*
|
|
1963
|
-
*
|
|
1964
|
-
* fix — `vite-plugin-ui-oid.ts`'s `handleStructMany`). Sound and caller-order-
|
|
1965
|
-
* independent: every offset is resolved from the file ONCE, up front, then applied
|
|
1966
|
-
* highest-offset-first — each `deleteElement` call only ever removes text AT OR
|
|
1967
|
-
* AFTER its own target, so every offset still queued (all of them numerically
|
|
1968
|
-
* SMALLER, since we process descending) is untouched by it and stays valid. A
|
|
1969
|
-
* nested descendant's `elementStart` is always numerically greater than its
|
|
1970
|
-
* ancestor's (it appears later in source), so this single offset-value rule also
|
|
1971
|
-
* deletes a selected descendant before a selected ancestor, with no separate
|
|
1972
|
-
* ancestor/descendant case to reason about. Unlike re-resolving each target by
|
|
1973
|
-
* re-scanning/re-transforming the file between deletes, this never recomputes an
|
|
1974
|
-
* OID's identity mid-batch — it only ever reads the ALREADY-RESOLVED offsets handed
|
|
1975
|
-
* to it, so it cannot suffer the occurrence-index churn a live re-transform would
|
|
1976
|
-
* introduce (deleting one same-tag sibling shifts every later sibling's own
|
|
1977
|
-
* `component:tag:nthOccurrence` signature, which would silently relabel it as a
|
|
1978
|
-
* DIFFERENT, already-used oid if resolution were re-derived from a fresh transform
|
|
1979
|
-
* mid-batch — the reason `handleStruct`'s per-call resolution is not simply looped
|
|
1980
|
-
* here without change).
|
|
1981
|
-
*/
|
|
1727
|
+
/** Delete pre-resolved elements from one source snapshot, highest offset first.
|
|
1728
|
+
* Earlier offsets remain valid as later elements disappear; descendants are
|
|
1729
|
+
* removed before ancestors without reassigning occurrence-based OIDs. */
|
|
1982
1730
|
export function deleteElements(
|
|
1983
1731
|
code: string,
|
|
1984
1732
|
elementStarts: readonly number[],
|