bireactive 0.3.0 → 0.3.2

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.
Files changed (117) hide show
  1. package/README.md +14 -7
  2. package/dist/automerge/doc-cell.d.ts +20 -0
  3. package/dist/automerge/doc-cell.js +80 -0
  4. package/dist/automerge/index.d.ts +3 -0
  5. package/dist/automerge/index.js +12 -0
  6. package/dist/automerge/reconcile.d.ts +5 -0
  7. package/dist/automerge/reconcile.js +63 -0
  8. package/dist/core/_counts.d.ts +48 -0
  9. package/dist/core/_counts.js +51 -0
  10. package/dist/core/cell.d.ts +148 -112
  11. package/dist/core/cell.js +945 -768
  12. package/dist/core/debug.d.ts +25 -0
  13. package/dist/core/debug.js +121 -0
  14. package/dist/core/derived-geometry.js +4 -7
  15. package/dist/core/index.d.ts +9 -2
  16. package/dist/core/index.js +8 -1
  17. package/dist/core/lenses/aggregates.d.ts +42 -52
  18. package/dist/core/lenses/aggregates.js +225 -116
  19. package/dist/core/lenses/geometry.d.ts +22 -4
  20. package/dist/core/lenses/geometry.js +59 -27
  21. package/dist/core/lenses/index.d.ts +6 -6
  22. package/dist/core/lenses/index.js +6 -6
  23. package/dist/core/lenses/memory.js +4 -17
  24. package/dist/core/lenses/numerical.d.ts +100 -0
  25. package/dist/core/lenses/{typed-factor.js → numerical.js} +136 -34
  26. package/dist/core/lenses/point-cloud.d.ts +67 -0
  27. package/dist/core/lenses/{closed-form-policies.js → point-cloud.js} +226 -84
  28. package/dist/core/lenses/snap.d.ts +18 -0
  29. package/dist/core/lenses/snap.js +138 -0
  30. package/dist/core/lenses/text.d.ts +40 -0
  31. package/dist/core/lenses/text.js +202 -0
  32. package/dist/core/lifecycle.js +3 -6
  33. package/dist/core/linalg.js +5 -11
  34. package/dist/core/optic.d.ts +13 -0
  35. package/dist/core/optic.js +39 -0
  36. package/dist/core/optics.d.ts +10 -0
  37. package/dist/core/optics.js +26 -0
  38. package/dist/core/store.d.ts +9 -0
  39. package/dist/core/store.js +77 -0
  40. package/dist/core/traits.d.ts +4 -7
  41. package/dist/core/traits.js +8 -12
  42. package/dist/core/values/anchor.js +0 -4
  43. package/dist/core/values/arr.d.ts +110 -0
  44. package/dist/core/values/arr.js +336 -0
  45. package/dist/core/values/audio.d.ts +8 -9
  46. package/dist/core/values/audio.js +11 -28
  47. package/dist/core/values/bool.d.ts +11 -11
  48. package/dist/core/values/bool.js +12 -22
  49. package/dist/core/values/box.d.ts +15 -20
  50. package/dist/core/values/box.js +20 -33
  51. package/dist/core/values/canvas.d.ts +18 -25
  52. package/dist/core/values/canvas.js +32 -66
  53. package/dist/core/values/color.d.ts +5 -7
  54. package/dist/core/values/color.js +5 -11
  55. package/dist/core/values/field.d.ts +6 -7
  56. package/dist/core/values/field.js +10 -35
  57. package/dist/core/values/flags.d.ts +1 -2
  58. package/dist/core/values/flags.js +1 -17
  59. package/dist/core/values/gpu.d.ts +6 -10
  60. package/dist/core/values/gpu.js +8 -22
  61. package/dist/core/values/matrix.d.ts +2 -4
  62. package/dist/core/values/matrix.js +2 -12
  63. package/dist/core/values/num.d.ts +19 -28
  64. package/dist/core/values/num.js +23 -41
  65. package/dist/core/values/pose.d.ts +2 -4
  66. package/dist/core/values/pose.js +3 -12
  67. package/dist/core/values/range.d.ts +18 -26
  68. package/dist/core/values/range.js +22 -39
  69. package/dist/core/values/reg/ambiguity.d.ts +8 -0
  70. package/dist/core/values/reg/ambiguity.js +131 -0
  71. package/dist/core/values/reg/engine.d.ts +91 -0
  72. package/dist/core/values/reg/engine.js +373 -0
  73. package/dist/core/values/reg/nfa.d.ts +42 -0
  74. package/dist/core/values/reg/nfa.js +391 -0
  75. package/dist/core/values/reg/regex.d.ts +7 -0
  76. package/dist/core/values/reg/regex.js +318 -0
  77. package/dist/core/values/reg/types.d.ts +60 -0
  78. package/dist/core/values/reg/types.js +3 -0
  79. package/dist/core/values/reg.d.ts +250 -0
  80. package/dist/core/values/reg.js +649 -0
  81. package/dist/core/values/str.d.ts +16 -60
  82. package/dist/core/values/str.js +133 -315
  83. package/dist/core/values/template.js +1 -24
  84. package/dist/core/values/transform.d.ts +3 -5
  85. package/dist/core/values/transform.js +3 -12
  86. package/dist/core/values/tri.d.ts +9 -10
  87. package/dist/core/values/tri.js +9 -15
  88. package/dist/core/values/vec.d.ts +9 -24
  89. package/dist/core/values/vec.js +9 -64
  90. package/dist/formats/lens.js +6 -9
  91. package/dist/index.d.ts +0 -11
  92. package/dist/index.js +1 -11
  93. package/dist/jsx-dev-runtime.d.ts +2 -0
  94. package/dist/jsx-dev-runtime.js +5 -0
  95. package/dist/jsx-runtime.d.ts +54 -0
  96. package/dist/jsx-runtime.js +219 -0
  97. package/dist/schema/lens.js +5 -5
  98. package/dist/shapes/drag-behaviors.d.ts +56 -0
  99. package/dist/shapes/drag-behaviors.js +102 -0
  100. package/dist/shapes/drag-spec.d.ts +52 -0
  101. package/dist/shapes/drag-spec.js +112 -0
  102. package/dist/shapes/index.d.ts +3 -1
  103. package/dist/shapes/index.js +3 -1
  104. package/dist/shapes/interaction.d.ts +2 -3
  105. package/dist/shapes/interaction.js +77 -56
  106. package/dist/shapes/label.js +6 -0
  107. package/dist/shapes/layout.d.ts +47 -1
  108. package/dist/shapes/layout.js +59 -1
  109. package/package.json +22 -1
  110. package/dist/coll.d.ts +0 -74
  111. package/dist/coll.js +0 -210
  112. package/dist/core/lenses/closed-form-policies.d.ts +0 -57
  113. package/dist/core/lenses/decompositions.d.ts +0 -14
  114. package/dist/core/lenses/decompositions.js +0 -224
  115. package/dist/core/lenses/domain-aggregates.d.ts +0 -42
  116. package/dist/core/lenses/domain-aggregates.js +0 -245
  117. package/dist/core/lenses/typed-factor.d.ts +0 -40
@@ -1,76 +1,32 @@
1
1
  import { Cell, type Init, type Writable } from "../cell.js";
2
+ import { Arr } from "./arr.js";
2
3
  type V = string;
3
4
  export declare const equals: (a: V, b: V) => boolean;
4
5
  /** Reverse a string by Unicode code points. */
5
6
  export declare const reverseStr: (s: V) => V;
6
- /** ROT13 cipher. Involutive: `rot13(rot13(s)) === s`. */
7
- export declare const rot13Str: (s: V) => V;
8
- /** Per-character case mask: `U` upper letter, `L` lower letter,
9
- * `" "` non-letter. Length matches the source. */
10
- export declare function caseMaskOf(s: V): string;
11
- /** Apply a case mask to `target`, position by position. Mask positions
12
- * beyond `target.length` are ignored; target positions beyond the
13
- * mask keep their native case (e.g. user appended a longer word). */
14
- export declare function applyCaseMask(target: V, mask: string): V;
15
- /** Apply the case pattern of a source word to a target word. Detects
16
- * all-upper / all-lower / title case, else falls back to position-wise
17
- * `applyCaseMask`. Non-letter target chars always pass through unchanged
18
- * (title-casing "-gng" → "-Gng"); this letter-awareness is what makes
19
- * GetPut hold when source words contain non-letters. */
20
- export declare function applyCasePattern(target: V, mask: string): V;
21
- /** Split `s` into words and separators. Returns:
22
- *
23
- * words[i] — the i-th run of word characters
24
- * seps[0] — leading non-word characters (possibly empty)
25
- * seps[i] — for 1 ≤ i ≤ words.length-1, the separator BETWEEN
26
- * `words[i-1]` and `words[i]`
27
- * seps[words.length] — trailing non-word characters
28
- *
29
- * Always satisfies `seps.length === words.length + 1`. */
30
- export declare function parseWords(s: V): {
31
- words: V[];
32
- seps: V[];
33
- };
34
- /** Inverse of `parseWords`. Interleaves words with `seps`; added words
35
- * get `" "` gaps, removed words keep the original trailing separator.
36
- * A zero-word original (`seps.length === 1`) treats its one entry as
37
- * lead only, so words append after it without double-counting as trail. */
38
- export declare function rebuildWords(words: V[], seps: V[]): V;
39
7
  export declare class Str extends Cell<V> {
40
8
  static traits: {
41
9
  equals: (a: V, b: V) => boolean;
42
10
  };
43
11
  readonly _t: typeof Str.traits;
44
12
  constructor(v?: V);
45
- /** Reverse. Involution. */
13
+ /** Reverse. */
46
14
  reverse(): this;
47
- /** ROT13. Involution. */
48
- rot13(): this;
49
- /** Trim edge whitespace; the complement restores the original padding
50
- * on write. Edge whitespace in a write is stripped first (the view's
51
- * contract is "no edge whitespace", else the complement grows). */
15
+ /** Trim edge whitespace; the complement restores the original padding on
16
+ * write. A write's own edge whitespace is stripped first. */
52
17
  trim(): Writable<Str>;
53
- /** Lowercase view with word-aware case recovery on write. Lookup
54
- * priority: (1) content match recover the source mask by word
55
- * (FIFO across duplicates); (2) per-position fallback for new content;
56
- * (3) native for content beyond the source structure. */
57
- lowercase(): Writable<Str>;
58
- /** Uppercase view. Dual of `lowercase`; same per-word case recovery. */
59
- uppercase(): Writable<Str>;
60
- /** Words view, one word per line. Read splits on non-word chars; the
61
- * complement is the separator layout, restored on write (added words
62
- * get single spaces). Non-word chars typed into a line are stripped —
63
- * edit `Trimmed` / `Lowercased` / `Source` to add punctuation. */
64
- words(): Writable<Str>;
65
- /** Sorted, case-insensitively-unique words, one per line. The
66
- * complement records each entry's source positions and original case,
67
- * so editing one line broadcasts to every occurrence in the source —
68
- * each rebuilt with its own original casing. */
69
- sortedUnique(): Writable<Str>;
18
+ /** Windowed view `s.slice(start, end)` (JS semantics). A write splices the
19
+ * text back into `[start, end)` of the live source, which grows or shrinks
20
+ * to fit; no stored complement. */
21
+ slice(start: number, end?: number): Writable<Str>;
22
+ /** Split into an editable `Arr<string>`. Each segment is a positional lens:
23
+ * reading index `i` re-splits the live source, writing it splices that piece
24
+ * back, so separators need no complement. Structural edits (insert/remove/
25
+ * move) re-split and rebuild the source; added boundaries use `joiner`
26
+ * (defaults to `sep`, or `" "` for a pattern). Identity is by position. */
27
+ split(sep: V | RegExp, joiner?: V): Arr<V>;
70
28
  }
71
- /** Writable `Str`. Strict factory: literal seeds a fresh cell;
72
- * existing `Writable<Str>` passes through by identity. RO sources
73
- * are rejected at the type level — use `Str.derive(...)` for
74
- * reactive RO tracking, or `cell.value` to snapshot. */
29
+ /** Writable `Str` from a literal (new cell) or existing writable (passed
30
+ * through). For read-only sources use `Str.derive`. */
75
31
  export declare function str(v?: Init<Str>): Writable<Str>;
76
32
  export {};
@@ -1,344 +1,162 @@
1
- // str.ts — reactive string with a symmetric lens chain.
2
- //
3
- // String projections are the canonical use of the engine's stateful-lens
4
- // primitive (`statefulLens`). Every useful view loses information the
5
- // engine recovers on write via a per-cell `complement`, so editing through
6
- // ANY view round-trips with case, whitespace, separators, and duplicate
7
- // positions preserved (the Foster/Pierce case-preserving find-and-replace
8
- // demo).
9
- //
10
- // `reverse()` and `rot13()` are involutions on the plain endo
11
- // `.lens(fwd, bwd)` — no complement; they chain like any endo lens.
12
- // Everything else is `Str.lens(parent, spec)` with a complement:
13
- //
14
- // trim — leading + trailing whitespace
15
- // lowercase — per-character case mask of the source
16
- // uppercase — dual of lowercase
17
- // words — separator pattern between words
18
- // sortedUnique — source positions + original case per unique word
19
1
  import { Cell } from "../cell.js";
20
- // Complement-carrying endo lens.
21
- //
22
- // The complement is state recorded forward from the source and consumed
23
- // on write-back. It persists across the lens's own writes (so `trim`
24
- // keeps its padding even when the view is emptied) and refreshes on
25
- // external source changes — the engine's `external` flag drives `step`.
26
- /** Endo lens backed by a complement recorded from the source. `record`
27
- * rebuilds the complement (kept on the lens's own writes), `project`
28
- * is the forward view, `reconstruct` is the backward source. */
29
- function complementLens(parent, record, project, reconstruct) {
30
- return Str.lens([parent], {
31
- init: ([s]) => record(s),
32
- step: ([s], c, external) => (external ? record(s) : c),
33
- fwd: ([s]) => project(s),
34
- bwd: (target, _s, c) => ({ updates: [reconstruct(target, c)], complement: c }),
35
- });
36
- }
2
+ import { Arr } from "./arr.js";
37
3
  export const equals = (a, b) => a === b;
38
4
  /** Reverse a string by Unicode code points. */
39
5
  export const reverseStr = (s) => [...s].reverse().join("");
40
- /** ROT13 cipher. Involutive: `rot13(rot13(s)) === s`. */
41
- export const rot13Str = (s) => s.replace(/[a-zA-Z]/g, c => {
42
- const code = c.charCodeAt(0);
43
- const base = code >= 97 ? 97 : 65;
44
- return String.fromCharCode(((code - base + 13) % 26) + base);
45
- });
46
- /** Per-character case mask: `U` upper letter, `L` lower letter,
47
- * `" "` non-letter. Length matches the source. */
48
- export function caseMaskOf(s) {
49
- let mask = "";
50
- for (let i = 0; i < s.length; i++) {
51
- const c = s[i];
52
- if (c >= "A" && c <= "Z")
53
- mask += "U";
54
- else if (c >= "a" && c <= "z")
55
- mask += "L";
56
- else
57
- mask += " ";
58
- }
59
- return mask;
60
- }
61
- /** Apply a case mask to `target`, position by position. Mask positions
62
- * beyond `target.length` are ignored; target positions beyond the
63
- * mask keep their native case (e.g. user appended a longer word). */
64
- export function applyCaseMask(target, mask) {
65
- let out = "";
66
- for (let i = 0; i < target.length; i++) {
67
- const c = target[i];
68
- const m = i < mask.length ? mask[i] : " ";
69
- if (m === "U")
70
- out += c.toUpperCase();
71
- else if (m === "L")
72
- out += c.toLowerCase();
73
- else
74
- out += c;
75
- }
76
- return out;
77
- }
78
- const ASCII_LETTER = (c) => (c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
79
- /** Apply the case pattern of a source word to a target word. Detects
80
- * all-upper / all-lower / title case, else falls back to position-wise
81
- * `applyCaseMask`. Non-letter target chars always pass through unchanged
82
- * (title-casing "-gng" → "-Gng"); this letter-awareness is what makes
83
- * GetPut hold when source words contain non-letters. */
84
- export function applyCasePattern(target, mask) {
85
- if (target.length === 0 || mask.length === 0)
86
- return target;
87
- const letters = [...mask].filter(c => c === "U" || c === "L");
88
- if (letters.length === 0)
89
- return target;
90
- if (letters.every(c => c === "U"))
91
- return target.toUpperCase();
92
- if (letters.every(c => c === "L"))
93
- return target.toLowerCase();
94
- if (letters[0] === "U" && letters.slice(1).every(c => c === "L")) {
95
- // Title case: uppercase the first letter (skipping leading
96
- // non-letters), lowercase the rest, pass non-letters through.
97
- let out = "";
98
- let firstLetterDone = false;
99
- for (let i = 0; i < target.length; i++) {
100
- const c = target[i];
101
- if (ASCII_LETTER(c)) {
102
- out += firstLetterDone ? c.toLowerCase() : c.toUpperCase();
103
- firstLetterDone = true;
104
- }
105
- else {
106
- out += c;
107
- }
108
- }
109
- return out;
110
- }
111
- return applyCaseMask(target, mask);
112
- }
113
- /** A "word" character: letters, digits, underscore, apostrophe, hyphen
114
- * (handles "don't", "co-op"). Everything else is a separator. */
115
- const WORD_CHAR = /[\p{L}\p{N}_'-]/u;
116
- /** Strip every non-word character. Keeps user-typed punctuation in the
117
- * `words` / `sortedUnique` views from leaking into the source via the
118
- * separator complement (where it would accumulate across edits). */
119
- const stripNonWord = (s) => s.replace(/[^\p{L}\p{N}_'-]/gu, "");
120
- /** Split `s` into words and separators. Returns:
121
- *
122
- * words[i] — the i-th run of word characters
123
- * seps[0] — leading non-word characters (possibly empty)
124
- * seps[i] — for 1 ≤ i ≤ words.length-1, the separator BETWEEN
125
- * `words[i-1]` and `words[i]`
126
- * seps[words.length] — trailing non-word characters
127
- *
128
- * Always satisfies `seps.length === words.length + 1`. */
129
- export function parseWords(s) {
130
- const words = [];
6
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
+ /** Resolve a (possibly negative / out-of-range) index against `len`,
8
+ * JS `slice`-style: negatives count from the end, then clamp to [0, len]. */
9
+ const resolveIndex = (i, len) => i < 0 ? Math.max(len + i, 0) : Math.min(i, len);
10
+ /** Split `s` on `sep`, keeping matched separators so it round-trips
11
+ * (`seps.length === parts.length - 1`). Zero-width matches are skipped, so
12
+ * `parts` is never empty. */
13
+ function scanSplit(s, sep) {
14
+ const flags = sep.flags.includes("g") ? sep.flags : `${sep.flags}g`;
15
+ const re = new RegExp(sep.source, flags);
16
+ const parts = [];
131
17
  const seps = [];
132
- let cur = "";
133
- let inWord = false;
134
- for (let i = 0; i < s.length; i++) {
135
- const c = s[i];
136
- if (WORD_CHAR.test(c)) {
137
- if (!inWord) {
138
- seps.push(cur);
139
- cur = "";
140
- inWord = true;
141
- }
142
- cur += c;
18
+ let last = 0;
19
+ let m = re.exec(s);
20
+ while (m !== null) {
21
+ if (m[0].length === 0) {
22
+ re.lastIndex++;
23
+ if (re.lastIndex > s.length)
24
+ break;
25
+ m = re.exec(s);
26
+ continue;
143
27
  }
144
- else {
145
- if (inWord) {
146
- words.push(cur);
147
- cur = "";
148
- inWord = false;
149
- }
150
- cur += c;
151
- }
152
- }
153
- if (inWord) {
154
- words.push(cur);
155
- seps.push("");
28
+ parts.push(s.slice(last, m.index));
29
+ seps.push(m[0]);
30
+ last = m.index + m[0].length;
31
+ m = re.exec(s);
156
32
  }
157
- else {
158
- seps.push(cur);
159
- }
160
- return { words, seps };
33
+ parts.push(s.slice(last));
34
+ return { parts, seps };
161
35
  }
162
- /** Inverse of `parseWords`. Interleaves words with `seps`; added words
163
- * get `" "` gaps, removed words keep the original trailing separator.
164
- * A zero-word original (`seps.length === 1`) treats its one entry as
165
- * lead only, so words append after it without double-counting as trail. */
166
- export function rebuildWords(words, seps) {
167
- const n = words.length;
168
- if (n === 0)
169
- return seps[0] ?? "";
170
- const lead = seps[0] ?? "";
171
- const trail = seps.length > 1 ? (seps[seps.length - 1] ?? "") : "";
172
- let out = lead;
173
- for (let i = 0; i < n; i++) {
174
- out += words[i];
175
- if (i < n - 1) {
176
- const idx = i + 1;
177
- // Interior separators only; the final `seps` entry is the trail.
178
- const sep = idx < seps.length - 1 ? seps[idx] : undefined;
179
- out += sep !== undefined ? sep : " ";
180
- }
181
- else {
182
- out += trail;
183
- }
184
- }
36
+ /** Interleave `parts` with `seps`; boundaries with no recorded separator use
37
+ * `joiner`. */
38
+ function joinParts(parts, seps, joiner) {
39
+ if (parts.length === 0)
40
+ return "";
41
+ let out = parts[0];
42
+ for (let i = 1; i < parts.length; i++)
43
+ out += (seps[i - 1] ?? joiner) + parts[i];
185
44
  return out;
186
45
  }
187
- /** (Re)build the case complement: positional `wordMasks` and content-keyed
188
- * `byContent` in one pass. `byContent` lists stay in source order for FIFO
189
- * consumption in `putl`. */
190
- function refreshCaseComplement(s, c) {
191
- const { words } = parseWords(s);
192
- const wordMasks = words.map(caseMaskOf);
193
- const byContent = new Map();
194
- for (let i = 0; i < words.length; i++) {
195
- const key = words[i].toLowerCase();
196
- let arr = byContent.get(key);
197
- if (arr === undefined) {
198
- arr = [];
199
- byContent.set(key, arr);
200
- }
201
- arr.push(wordMasks[i]);
202
- }
203
- c.wordMasks = wordMasks;
204
- c.byContent = byContent;
205
- }
206
- /** Apply the case complement to a target string and rebuild. Each
207
- * target word goes through three lookup tiers — content match
208
- * (FIFO-consumed from a per-call clone), positional fallback, then
209
- * native pass-through. */
210
- function applyCaseComplement(target, c) {
211
- const { words, seps } = parseWords(target);
212
- // Per-call clone: consume FIFO without mutating the stored map, so
213
- // repeated `putl` calls start from the same state.
214
- const remaining = new Map();
215
- for (const [k, arr] of c.byContent)
216
- remaining.set(k, arr.slice());
217
- const cased = words.map((w, i) => {
218
- const key = w.toLowerCase();
219
- const matches = remaining.get(key);
220
- if (matches !== undefined && matches.length > 0) {
221
- return applyCasePattern(w, matches.shift());
222
- }
223
- const mask = i < c.wordMasks.length ? c.wordMasks[i] : "";
224
- return mask.length === 0 ? w : applyCasePattern(w, mask);
225
- });
226
- return rebuildWords(cased, seps);
227
- }
228
- /** Build a fresh case complement from a source string. */
229
- function buildCaseComplement(s) {
230
- const c = { wordMasks: [], byContent: new Map() };
231
- refreshCaseComplement(s, c);
232
- return c;
233
- }
234
46
  export class Str extends Cell {
235
47
  static traits = { equals };
236
48
  constructor(v = "") {
237
49
  super(v, { equals });
238
50
  }
239
- /** Reverse. Involution. */
51
+ /** Reverse. */
240
52
  reverse() {
241
53
  return this.lens(reverseStr, reverseStr);
242
54
  }
243
- /** ROT13. Involution. */
244
- rot13() {
245
- return this.lens(rot13Str, rot13Str);
246
- }
247
- /** Trim edge whitespace; the complement restores the original padding
248
- * on write. Edge whitespace in a write is stripped first (the view's
249
- * contract is "no edge whitespace", else the complement grows). */
55
+ /** Trim edge whitespace; the complement restores the original padding on
56
+ * write. A write's own edge whitespace is stripped first. */
250
57
  trim() {
251
- return complementLens(this, s => {
252
- const lead = /^\s*/.exec(s)?.[0] ?? "";
253
- // Slice lead off first so trail can't overlap it on all-whitespace.
254
- const remain = s.slice(lead.length);
255
- const trail = /\s*$/.exec(remain)?.[0] ?? "";
256
- return { lead, trail };
257
- }, s => {
258
- const lead = /^\s*/.exec(s)?.[0] ?? "";
259
- const remain = s.slice(lead.length);
260
- const trail = /\s*$/.exec(remain)?.[0] ?? "";
261
- return remain.slice(0, remain.length - trail.length);
262
- },
263
- // Edge whitespace in the write is dropped; complement restores pad.
264
- (target, c) => c.lead + target.replace(/^\s+/, "").replace(/\s+$/, "") + c.trail);
265
- }
266
- /** Lowercase view with word-aware case recovery on write. Lookup
267
- * priority: (1) content match — recover the source mask by word
268
- * (FIFO across duplicates); (2) per-position fallback for new content;
269
- * (3) native for content beyond the source structure. */
270
- lowercase() {
271
- return complementLens(this, s => buildCaseComplement(s), s => s.toLowerCase(), (target, c) => applyCaseComplement(target, c));
272
- }
273
- /** Uppercase view. Dual of `lowercase`; same per-word case recovery. */
274
- uppercase() {
275
- return complementLens(this, s => buildCaseComplement(s), s => s.toUpperCase(), (target, c) => applyCaseComplement(target, c));
58
+ return Str.lens(this, {
59
+ init: (s) => {
60
+ const lead = /^\s*/.exec(s)?.[0] ?? "";
61
+ // Slice lead off first so trail can't overlap it on all-whitespace.
62
+ const remain = s.slice(lead.length);
63
+ const trail = /\s*$/.exec(remain)?.[0] ?? "";
64
+ return { lead, trail };
65
+ },
66
+ fwd: (s) => {
67
+ const lead = /^\s*/.exec(s)?.[0] ?? "";
68
+ const remain = s.slice(lead.length);
69
+ const trail = /\s*$/.exec(remain)?.[0] ?? "";
70
+ return remain.slice(0, remain.length - trail.length);
71
+ },
72
+ bwd: (target, _s, c) => ({
73
+ update: c.lead + target.replace(/^\s+/, "").replace(/\s+$/, "") + c.trail,
74
+ complement: c,
75
+ }),
76
+ });
276
77
  }
277
- /** Words view, one word per line. Read splits on non-word chars; the
278
- * complement is the separator layout, restored on write (added words
279
- * get single spaces). Non-word chars typed into a line are stripped —
280
- * edit `Trimmed` / `Lowercased` / `Source` to add punctuation. */
281
- words() {
282
- return complementLens(this, s => ({ separators: parseWords(s).seps }), s => parseWords(s).words.join("\n"), (target, c) => {
283
- const words = target
284
- .split(/\n/)
285
- .map(stripNonWord)
286
- .filter(w => w.length > 0);
287
- return rebuildWords(words, c.separators);
78
+ /** Windowed view `s.slice(start, end)` (JS semantics). A write splices the
79
+ * text back into `[start, end)` of the live source, which grows or shrinks
80
+ * to fit; no stored complement. */
81
+ slice(start, end) {
82
+ return Str.lens(this, (s) => s.slice(start, end), (target, s) => {
83
+ const len = s.length;
84
+ const a = resolveIndex(start, len);
85
+ const b = end === undefined ? len : resolveIndex(end, len);
86
+ const hi = b < a ? a : b;
87
+ return s.slice(0, a) + target + s.slice(hi);
288
88
  });
289
89
  }
290
- /** Sorted, case-insensitively-unique words, one per line. The
291
- * complement records each entry's source positions and original case,
292
- * so editing one line broadcasts to every occurrence in the source —
293
- * each rebuilt with its own original casing. */
294
- sortedUnique() {
295
- return complementLens(this, s => {
296
- const { words, seps } = parseWords(s);
297
- const buckets = new Map();
298
- for (let i = 0; i < words.length; i++) {
299
- const w = words[i];
300
- const key = w.toLowerCase();
301
- let arr = buckets.get(key);
302
- if (arr === undefined) {
303
- arr = [];
304
- buckets.set(key, arr);
305
- }
306
- arr.push({ index: i, sourceCase: w });
307
- }
308
- const unique = [...buckets.keys()].sort();
309
- return {
310
- unique,
311
- positions: unique.map(k => buckets.get(k)),
312
- separators: seps,
313
- sourceWords: words,
314
- };
315
- }, s => {
316
- const { words } = parseWords(s);
317
- return [...new Set(words.map(w => w.toLowerCase()))].sort().join("\n");
318
- }, (target, c) => {
319
- // Strip non-word chars typed into the view, same as `words`.
320
- const edited = target
321
- .split(/\n/)
322
- .map(stripNonWord)
323
- .filter(w => w.length > 0);
324
- const sourceWords = c.sourceWords.slice();
325
- const n = Math.min(edited.length, c.unique.length);
326
- for (let i = 0; i < n; i++) {
327
- const newWord = edited[i];
328
- for (const { index, sourceCase } of c.positions[i]) {
329
- if (index >= sourceWords.length)
330
- continue;
331
- sourceWords[index] = applyCasePattern(newWord, caseMaskOf(sourceCase));
332
- }
90
+ /** Split into an editable `Arr<string>`. Each segment is a positional lens:
91
+ * reading index `i` re-splits the live source, writing it splices that piece
92
+ * back, so separators need no complement. Structural edits (insert/remove/
93
+ * move) re-split and rebuild the source; added boundaries use `joiner`
94
+ * (defaults to `sep`, or `" "` for a pattern). Identity is by position. */
95
+ split(sep, joiner) {
96
+ const re = typeof sep === "string" ? new RegExp(escapeRegExp(sep)) : sep;
97
+ const join = joiner ?? (typeof sep === "string" ? sep : " ");
98
+ const source = this;
99
+ const segCache = new Map();
100
+ const indexOfCell = new WeakMap();
101
+ const seg = (i) => {
102
+ let c = segCache.get(i);
103
+ if (c === undefined) {
104
+ c = Str.lens(source, (s) => scanSplit(s, re).parts[i] ?? "", (target, s) => {
105
+ const { parts, seps } = scanSplit(s, re);
106
+ if (i >= parts.length)
107
+ return s;
108
+ parts[i] = target;
109
+ return joinParts(parts, seps, join);
110
+ });
111
+ segCache.set(i, c);
112
+ indexOfCell.set(c, i);
333
113
  }
334
- return rebuildWords(sourceWords, c.separators);
114
+ return c;
115
+ };
116
+ const write = (parts, seps) => {
117
+ source.value = joinParts(parts, seps, join);
118
+ };
119
+ return Arr.fromSource(source, (s) => scanSplit(s, re).parts.map((_, i) => seg(i)), {
120
+ insert: (v, at) => {
121
+ const text = v instanceof Cell ? v.value : v;
122
+ const { parts, seps } = scanSplit(source.peek(), re);
123
+ const idx = at == null || at > parts.length ? parts.length : Math.max(0, at);
124
+ parts.splice(idx, 0, text);
125
+ if (parts.length > 1)
126
+ seps.splice(Math.min(idx, seps.length), 0, join);
127
+ write(parts, seps);
128
+ return seg(idx);
129
+ },
130
+ remove: e => {
131
+ const idx = indexOfCell.get(e);
132
+ if (idx === undefined)
133
+ return;
134
+ const { parts, seps } = scanSplit(source.peek(), re);
135
+ if (idx >= parts.length)
136
+ return;
137
+ parts.splice(idx, 1);
138
+ if (seps.length > 0)
139
+ seps.splice(Math.min(idx, seps.length - 1), 1);
140
+ write(parts, seps);
141
+ },
142
+ moveBefore: (e, anchor) => {
143
+ const from = indexOfCell.get(e);
144
+ if (from === undefined)
145
+ return;
146
+ const { parts, seps } = scanSplit(source.peek(), re);
147
+ if (from >= parts.length)
148
+ return;
149
+ const [moved] = parts.splice(from, 1);
150
+ const ai = anchor == null ? undefined : indexOfCell.get(anchor);
151
+ const at = ai === undefined ? parts.length : ai > from ? ai - 1 : ai;
152
+ parts.splice(at, 0, moved);
153
+ write(parts, seps);
154
+ },
335
155
  });
336
156
  }
337
157
  }
338
- /** Writable `Str`. Strict factory: literal seeds a fresh cell;
339
- * existing `Writable<Str>` passes through by identity. RO sources
340
- * are rejected at the type level — use `Str.derive(...)` for
341
- * reactive RO tracking, or `cell.value` to snapshot. */
158
+ /** Writable `Str` from a literal (new cell) or existing writable (passed
159
+ * through). For read-only sources use `Str.derive`. */
342
160
  export function str(v = "") {
343
161
  if (v instanceof Str)
344
162
  return v;
@@ -1,25 +1,3 @@
1
- // template.ts — bidirectional text templates: typed slots ⇄ rendered string.
2
- //
3
- // A template is `lit₀ slot₀ lit₁ slot₁ … litₙ` — a multi-parent `Str.lens`
4
- // over the slot cells. Forward RENDERS (interleave literals with each
5
- // slot's formatted value); backward PARSES (split the edited string on the
6
- // literal delimiters, decode each segment, write the changed slots back).
7
- // Slots-as-source: the typed cells are canonical and the string is one
8
- // view, so the same slots can drive several renderings at once (edit any,
9
- // all stay in sync) — the thing `Str`'s views can't do.
10
- //
11
- // Each slot carries a `Codec<T>` (string ⇄ T). That codec is the textual
12
- // dual of the `pack` trait: `pack : factor :: codec : template` — both
13
- // serialize a value class into a medium (Float64Array vs string) so one
14
- // generic lens (LSQ-solve vs parse/print) works over any class that
15
- // supplies it. Kept local here rather than as a `TraitDict` entry; it can
16
- // graduate to a trait once a second consumer wants it.
17
- //
18
- // Parse is rejection-tolerant: a segment that fails its codec (typing
19
- // "banana" into a number slot) yields `undefined`, the engine's GetPut
20
- // stop prunes the no-op, and that slot stays put. A missing delimiter
21
- // rejects the whole edit. Adjacent slots with no literal between them are
22
- // ambiguous to split — avoid empty inter-slot literals.
23
1
  import { SKIP } from "../cell.js";
24
2
  import { num } from "./num.js";
25
3
  import { Str, str } from "./str.js";
@@ -101,8 +79,7 @@ export function template(literals, slots) {
101
79
  return str(literals.join(""));
102
80
  const cells = slots.map(s => s.cell);
103
81
  // Heterogeneous, dynamic-length parents don't fit the static N-tuple
104
- // overload; bind a flat signature at the boundary. `.bind(Str)` keeps the
105
- // static `this` (the lens reads it as the constructor).
82
+ // overload; bind a flat signature. `.bind(Str)` keeps the static `this`.
106
83
  const lensN = Str.lens.bind(Str);
107
84
  return lensN(cells, vals => render(literals, slots, vals), edited => parse(literals, slots, edited));
108
85
  }
@@ -25,8 +25,6 @@ export declare class Transform extends Cell<V> {
25
25
  equals: (a: V, b: V) => boolean;
26
26
  };
27
27
  readonly _t: typeof Transform.traits;
28
- /** Scalar `scale` is the `.scale` Vec field lens, not an eager method;
29
- * scalar-multiply via `Transform.lens(...)` or field writes. */
30
28
  constructor(v?: V);
31
29
  add(b: Val<V>): this;
32
30
  sub(b: Val<V>): this;
@@ -36,13 +34,13 @@ export declare class Transform extends Cell<V> {
36
34
  get origin(): this extends import("../index.js").WritableBrand ? Writable<Vec> : Vec;
37
35
  get rotate(): this extends import("../index.js").WritableBrand ? Writable<Num> : Num;
38
36
  get opacity(): this extends import("../index.js").WritableBrand ? Writable<Num> : Num;
39
- /** Tween-builder, implied by the lerp trait. */
37
+ /** Tween-builder. */
40
38
  to(this: Writable<Transform>, target: V, dur: Val<number>, ease?: Easing): Tween<V>;
41
39
  }
42
40
  export type TransformInit = {
43
41
  [K in keyof V]?: V[K];
44
42
  };
45
- /** Seed a `Writable<Transform>` from literal values. For reactive
46
- * sources, use `Transform.lens(...)` or field-write composition. */
43
+ /** Writable `Transform` from literal fields. For reactive sources use
44
+ * `Transform.lens` or field writes. */
47
45
  export declare function transform(init?: TransformInit): Writable<Transform>;
48
46
  export {};