bireactive 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -7
- package/dist/automerge/doc-cell.d.ts +24 -11
- package/dist/automerge/doc-cell.js +19 -13
- package/dist/automerge/index.d.ts +3 -2
- package/dist/automerge/index.js +6 -5
- package/dist/automerge/reconcile.d.ts +5 -2
- package/dist/automerge/reconcile.js +73 -15
- package/dist/core/_counts.js +5 -12
- package/dist/core/cell.d.ts +3 -3
- package/dist/core/cell.js +6 -7
- package/dist/core/derived-geometry.js +4 -7
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +3 -1
- package/dist/core/lenses/aggregates.d.ts +42 -52
- package/dist/core/lenses/aggregates.js +225 -116
- package/dist/core/lenses/geometry.d.ts +22 -4
- package/dist/core/lenses/geometry.js +59 -27
- package/dist/core/lenses/index.d.ts +5 -6
- package/dist/core/lenses/index.js +5 -6
- package/dist/core/lenses/memory.js +4 -17
- package/dist/core/lenses/numerical.d.ts +100 -0
- package/dist/core/lenses/{typed-factor.js → numerical.js} +136 -34
- package/dist/core/lenses/point-cloud.d.ts +67 -0
- package/dist/core/lenses/{closed-form-policies.js → point-cloud.js} +218 -81
- package/dist/core/lenses/snap.d.ts +1 -1
- package/dist/core/lenses/snap.js +3 -10
- package/dist/core/lenses/text.d.ts +40 -0
- package/dist/core/lenses/text.js +202 -0
- package/dist/core/lifecycle.js +3 -6
- package/dist/core/linalg.js +5 -11
- package/dist/core/optic.js +10 -15
- package/dist/core/optics.js +4 -8
- package/dist/core/store.d.ts +1 -2
- package/dist/core/store.js +7 -15
- package/dist/core/traits.d.ts +4 -7
- package/dist/core/traits.js +8 -12
- package/dist/core/values/anchor.js +0 -4
- package/dist/core/values/arr.d.ts +110 -0
- package/dist/core/values/arr.js +336 -0
- package/dist/core/values/audio.d.ts +8 -9
- package/dist/core/values/audio.js +7 -23
- package/dist/core/values/bool.d.ts +11 -11
- package/dist/core/values/bool.js +12 -22
- package/dist/core/values/box.d.ts +15 -20
- package/dist/core/values/box.js +20 -33
- package/dist/core/values/canvas.d.ts +18 -25
- package/dist/core/values/canvas.js +17 -48
- package/dist/core/values/color.d.ts +5 -7
- package/dist/core/values/color.js +5 -11
- package/dist/core/values/field.d.ts +6 -7
- package/dist/core/values/field.js +10 -35
- package/dist/core/values/flags.d.ts +1 -2
- package/dist/core/values/flags.js +1 -17
- package/dist/core/values/gpu.d.ts +6 -10
- package/dist/core/values/gpu.js +8 -22
- package/dist/core/values/matrix.d.ts +2 -4
- package/dist/core/values/matrix.js +2 -12
- package/dist/core/values/num.d.ts +19 -28
- package/dist/core/values/num.js +23 -41
- package/dist/core/values/pose.d.ts +2 -4
- package/dist/core/values/pose.js +3 -12
- package/dist/core/values/range.d.ts +18 -26
- package/dist/core/values/range.js +22 -39
- package/dist/core/values/reg/ambiguity.d.ts +8 -0
- package/dist/core/values/reg/ambiguity.js +131 -0
- package/dist/core/values/reg/engine.d.ts +91 -0
- package/dist/core/values/reg/engine.js +373 -0
- package/dist/core/values/reg/nfa.d.ts +42 -0
- package/dist/core/values/reg/nfa.js +391 -0
- package/dist/core/values/reg/regex.d.ts +7 -0
- package/dist/core/values/reg/regex.js +318 -0
- package/dist/core/values/reg/types.d.ts +60 -0
- package/dist/core/values/reg/types.js +3 -0
- package/dist/core/values/reg.d.ts +250 -0
- package/dist/core/values/reg.js +649 -0
- package/dist/core/values/str.d.ts +16 -60
- package/dist/core/values/str.js +133 -315
- package/dist/core/values/template.js +1 -24
- package/dist/core/values/transform.d.ts +3 -5
- package/dist/core/values/transform.js +3 -12
- package/dist/core/values/tri.d.ts +9 -10
- package/dist/core/values/tri.js +9 -15
- package/dist/core/values/vec.d.ts +9 -24
- package/dist/core/values/vec.js +9 -64
- package/dist/index.d.ts +0 -11
- package/dist/index.js +1 -11
- package/package.json +17 -10
- package/dist/coll.d.ts +0 -74
- package/dist/coll.js +0 -210
- package/dist/core/lenses/closed-form-policies.d.ts +0 -57
- package/dist/core/lenses/decompositions.d.ts +0 -14
- package/dist/core/lenses/decompositions.js +0 -224
- package/dist/core/lenses/domain-aggregates.d.ts +0 -42
- package/dist/core/lenses/domain-aggregates.js +0 -245
- 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.
|
|
13
|
+
/** Reverse. */
|
|
46
14
|
reverse(): this;
|
|
47
|
-
/**
|
|
48
|
-
|
|
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
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
|
|
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
|
|
72
|
-
*
|
|
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 {};
|
package/dist/core/values/str.js
CHANGED
|
@@ -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
|
-
|
|
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 re-runs `init` (the default `step`)
|
|
26
|
-
// only when the source actually moves.
|
|
27
|
-
/** Endo lens backed by a complement recorded from the source. `record`
|
|
28
|
-
* rebuilds the complement (kept on the lens's own writes; re-run on external
|
|
29
|
-
* source changes), `project` is the forward view, `reconstruct` the source. */
|
|
30
|
-
function complementLens(parent, record, project, reconstruct) {
|
|
31
|
-
return Str.lens(parent, {
|
|
32
|
-
init: (s) => record(s),
|
|
33
|
-
fwd: (s) => project(s),
|
|
34
|
-
bwd: (target, _s, c) => ({ update: 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
133
|
-
let
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
return { words, seps };
|
|
33
|
+
parts.push(s.slice(last));
|
|
34
|
+
return { parts, seps };
|
|
161
35
|
}
|
|
162
|
-
/**
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
51
|
+
/** Reverse. */
|
|
240
52
|
reverse() {
|
|
241
53
|
return this.lens(reverseStr, reverseStr);
|
|
242
54
|
}
|
|
243
|
-
/**
|
|
244
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
/**
|
|
291
|
-
*
|
|
292
|
-
* so
|
|
293
|
-
*
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
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
|
|
339
|
-
*
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
46
|
-
*
|
|
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 {};
|