@sigrun/shared 0.0.0-stage → 0.1.1
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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/access.d.ts +55 -0
- package/dist/access.js +72 -0
- package/dist/ambient.d.ts +111 -0
- package/dist/ambient.js +23 -0
- package/dist/blocks/format.d.ts +28 -0
- package/dist/blocks/format.js +199 -0
- package/dist/blocks/icons.d.ts +12 -0
- package/dist/blocks/icons.js +64 -0
- package/dist/blocks/index.d.ts +24 -0
- package/dist/blocks/index.js +20 -0
- package/dist/blocks/lttb.d.ts +16 -0
- package/dist/blocks/lttb.js +67 -0
- package/dist/blocks/ops.d.ts +44 -0
- package/dist/blocks/ops.js +294 -0
- package/dist/blocks/text.d.ts +43 -0
- package/dist/blocks/text.js +790 -0
- package/dist/blocks/transport.d.ts +77 -0
- package/dist/blocks/transport.js +11 -0
- package/dist/blocks/types.d.ts +541 -0
- package/dist/blocks/types.js +17 -0
- package/dist/commands.d.ts +75 -0
- package/dist/commands.js +25 -0
- package/dist/conversations.d.ts +31 -0
- package/dist/conversations.js +94 -0
- package/dist/interactions.d.ts +91 -0
- package/dist/interactions.js +21 -0
- package/dist/jobs.d.ts +109 -0
- package/dist/jobs.js +19 -0
- package/dist/json.d.ts +20 -0
- package/dist/json.js +13 -0
- package/dist/live.d.ts +71 -0
- package/dist/live.js +82 -0
- package/dist/session-meta.d.ts +119 -0
- package/dist/session-meta.js +13 -0
- package/dist/settings.d.ts +408 -0
- package/dist/settings.js +19 -0
- package/dist/themes.d.ts +50 -0
- package/dist/themes.js +156 -0
- package/dist/turns.d.ts +119 -0
- package/dist/turns.js +197 -0
- package/dist/view-delta.d.ts +54 -0
- package/dist/view-delta.js +135 -0
- package/dist/views.d.ts +784 -0
- package/dist/views.js +38 -0
- package/package.json +90 -5
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Largest-triangle-three-buckets downsampling (UI-blocks spec §5.4):
|
|
3
|
+
* the display-side downsample channels apply to line and area series
|
|
4
|
+
* (at most 2,000 points per series on the wire; a channel shows fewer).
|
|
5
|
+
*
|
|
6
|
+
* Pure: returns a fresh array of fresh tuples; never mutates `points`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Downsample `points` to at most `threshold` points, always keeping the
|
|
10
|
+
* first and the last point. The interior is bucketed into `threshold -
|
|
11
|
+
* 2` buckets; each bucket keeps the point forming the largest triangle
|
|
12
|
+
* with the previously selected point and the next bucket's average —
|
|
13
|
+
* which preserves visual shape (peaks and troughs) rather than
|
|
14
|
+
* sampling on a fixed interval.
|
|
15
|
+
*/
|
|
16
|
+
export declare function lttb(points: readonly (readonly [number, number])[], threshold: number): [number, number][];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Largest-triangle-three-buckets downsampling (UI-blocks spec §5.4):
|
|
3
|
+
* the display-side downsample channels apply to line and area series
|
|
4
|
+
* (at most 2,000 points per series on the wire; a channel shows fewer).
|
|
5
|
+
*
|
|
6
|
+
* Pure: returns a fresh array of fresh tuples; never mutates `points`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Downsample `points` to at most `threshold` points, always keeping the
|
|
10
|
+
* first and the last point. The interior is bucketed into `threshold -
|
|
11
|
+
* 2` buckets; each bucket keeps the point forming the largest triangle
|
|
12
|
+
* with the previously selected point and the next bucket's average —
|
|
13
|
+
* which preserves visual shape (peaks and troughs) rather than
|
|
14
|
+
* sampling on a fixed interval.
|
|
15
|
+
*/
|
|
16
|
+
export function lttb(points, threshold) {
|
|
17
|
+
const count = points.length;
|
|
18
|
+
if (count === 0 || threshold <= 0)
|
|
19
|
+
return [];
|
|
20
|
+
if (threshold === 1)
|
|
21
|
+
return [[points[0][0], points[0][1]]];
|
|
22
|
+
if (threshold === 2) {
|
|
23
|
+
return [
|
|
24
|
+
[points[0][0], points[0][1]],
|
|
25
|
+
[count === 1 ? points[0][0] : points[count - 1][0],
|
|
26
|
+
count === 1 ? points[0][1] : points[count - 1][1]],
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
if (threshold >= count) {
|
|
30
|
+
return points.map((point) => [point[0], point[1]]);
|
|
31
|
+
}
|
|
32
|
+
const sampled = [[points[0][0], points[0][1]]];
|
|
33
|
+
const bucketSize = (count - 2) / (threshold - 2);
|
|
34
|
+
for (let bucket = 0; bucket < threshold - 2; bucket++) {
|
|
35
|
+
const start = Math.floor(1 + bucket * bucketSize);
|
|
36
|
+
const end = Math.min(Math.floor(1 + (bucket + 1) * bucketSize), count - 1);
|
|
37
|
+
// The average of the NEXT bucket's points — the third corner of
|
|
38
|
+
// every candidate triangle. The last bucket averages the final
|
|
39
|
+
// point's window [count-1, count).
|
|
40
|
+
const nextStart = end;
|
|
41
|
+
const nextEnd = Math.min(Math.floor(1 + (bucket + 2) * bucketSize), count);
|
|
42
|
+
let avgX = 0;
|
|
43
|
+
let avgY = 0;
|
|
44
|
+
for (let i = nextStart; i < nextEnd; i++) {
|
|
45
|
+
avgX += points[i][0];
|
|
46
|
+
avgY += points[i][1];
|
|
47
|
+
}
|
|
48
|
+
avgX /= nextEnd - nextStart;
|
|
49
|
+
avgY /= nextEnd - nextStart;
|
|
50
|
+
const previous = sampled[sampled.length - 1];
|
|
51
|
+
let bestArea = -1;
|
|
52
|
+
let bestIndex = start;
|
|
53
|
+
for (let i = start; i < end; i++) {
|
|
54
|
+
// Twice the signed area of the triangle (previous, candidate,
|
|
55
|
+
// average) — the factor is constant, so abs is enough to rank.
|
|
56
|
+
const area = Math.abs((previous[0] - avgX) * (points[i][1] - previous[1]) -
|
|
57
|
+
(previous[0] - points[i][0]) * (avgY - previous[1]));
|
|
58
|
+
if (area > bestArea) {
|
|
59
|
+
bestArea = area;
|
|
60
|
+
bestIndex = i;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
sampled.push([points[bestIndex][0], points[bestIndex][1]]);
|
|
64
|
+
}
|
|
65
|
+
sampled.push([points[count - 1][0], points[count - 1][1]]);
|
|
66
|
+
return sampled;
|
|
67
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `applyOps` and `resolvePointer` (UI-blocks spec §4.2/§4.3): the single
|
|
3
|
+
* applier every surface's data updates go through, and the pointer
|
|
4
|
+
* reader bindings resolve with.
|
|
5
|
+
*
|
|
6
|
+
* Purity contract:
|
|
7
|
+
*
|
|
8
|
+
* - `applyOps` NEVER mutates its input — it clones along the touched
|
|
9
|
+
* path only (copy-on-write), so a batch of ops over a large document
|
|
10
|
+
* shares every untouched branch by reference with the input. The
|
|
11
|
+
* property suite pins this (frozen inputs, snapshot equality).
|
|
12
|
+
* - Pointers are RFC 6901 with `~0` (`~`) and `~1` (`/`) escaping, plus
|
|
13
|
+
* the one extension (spec §4.2): a pointer starting `./` resolves
|
|
14
|
+
* against the passed `item` (a collection's item template), which
|
|
15
|
+
* only `resolvePointer` takes — the DATA side never sees `./`.
|
|
16
|
+
* - A failing op throws `DataOpError` carrying the op's INDEX, so the
|
|
17
|
+
* caller (U3/U5) can treat it as the full-refetch signal (spec §16).
|
|
18
|
+
*/
|
|
19
|
+
import type { JsonObject, JsonValue } from "../json.js";
|
|
20
|
+
import type { DataOp } from "./types.js";
|
|
21
|
+
/** An op that could not be applied; `opIndex` names which one failed. */
|
|
22
|
+
export declare class DataOpError extends Error {
|
|
23
|
+
readonly opIndex: number;
|
|
24
|
+
constructor(opIndex: number, message: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Apply a batch of `DataOp`s (spec §4.3) to `data`, returning a NEW
|
|
28
|
+
* object. Pure: untouched branches are shared by reference with the
|
|
29
|
+
* input; the input is never mutated. Ops apply sequentially — an
|
|
30
|
+
* earlier op's write is visible to the next.
|
|
31
|
+
*
|
|
32
|
+
* Throws `DataOpError` (naming the op's index) on any op whose path
|
|
33
|
+
* does not exist, or whose target is not an array where one is required.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyOps(data: JsonObject, ops: readonly DataOp[]): JsonObject;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve an RFC 6901 `pointer` against `data`; a pointer starting
|
|
38
|
+
* `./` resolves against `item` instead (spec §4.2's one extension,
|
|
39
|
+
* used inside collection item templates). A missing path — including a
|
|
40
|
+
* path THROUGH a JSON null, and a malformed pointer (no `/` or `./`
|
|
41
|
+
* prefix) — is `undefined`, never an error: "a binding to a missing
|
|
42
|
+
* path renders as empty, never as an error".
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolvePointer(data: JsonValue, pointer: string, item?: JsonValue): JsonValue | undefined;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `applyOps` and `resolvePointer` (UI-blocks spec §4.2/§4.3): the single
|
|
3
|
+
* applier every surface's data updates go through, and the pointer
|
|
4
|
+
* reader bindings resolve with.
|
|
5
|
+
*
|
|
6
|
+
* Purity contract:
|
|
7
|
+
*
|
|
8
|
+
* - `applyOps` NEVER mutates its input — it clones along the touched
|
|
9
|
+
* path only (copy-on-write), so a batch of ops over a large document
|
|
10
|
+
* shares every untouched branch by reference with the input. The
|
|
11
|
+
* property suite pins this (frozen inputs, snapshot equality).
|
|
12
|
+
* - Pointers are RFC 6901 with `~0` (`~`) and `~1` (`/`) escaping, plus
|
|
13
|
+
* the one extension (spec §4.2): a pointer starting `./` resolves
|
|
14
|
+
* against the passed `item` (a collection's item template), which
|
|
15
|
+
* only `resolvePointer` takes — the DATA side never sees `./`.
|
|
16
|
+
* - A failing op throws `DataOpError` carrying the op's INDEX, so the
|
|
17
|
+
* caller (U3/U5) can treat it as the full-refetch signal (spec §16).
|
|
18
|
+
*/
|
|
19
|
+
/** An op that could not be applied; `opIndex` names which one failed. */
|
|
20
|
+
export class DataOpError extends Error {
|
|
21
|
+
opIndex;
|
|
22
|
+
constructor(opIndex, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "DataOpError";
|
|
25
|
+
this.opIndex = opIndex;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isContainer(value) {
|
|
29
|
+
// `typeof null === "object"` — a JSON null is NOT a container; a path
|
|
30
|
+
// through it is a missing path (undefined), never a walk into it.
|
|
31
|
+
return Array.isArray(value) || (typeof value === "object" && value !== null);
|
|
32
|
+
}
|
|
33
|
+
/** Split an RFC 6901 pointer into unescaped reference tokens. */
|
|
34
|
+
function pointerTokens(pointer) {
|
|
35
|
+
if (pointer === "")
|
|
36
|
+
return [];
|
|
37
|
+
if (!pointer.startsWith("/")) {
|
|
38
|
+
throw new Error(`invalid pointer: ${pointer}`);
|
|
39
|
+
}
|
|
40
|
+
return pointer
|
|
41
|
+
.slice(1)
|
|
42
|
+
.split("/")
|
|
43
|
+
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
44
|
+
}
|
|
45
|
+
/** Read `token` from a container; `undefined` when absent. */
|
|
46
|
+
function readToken(container, token) {
|
|
47
|
+
if (Array.isArray(container)) {
|
|
48
|
+
if (!/^\d+$/.test(token))
|
|
49
|
+
return undefined;
|
|
50
|
+
const index = Number(token);
|
|
51
|
+
return index < container.length ? container[index] : undefined;
|
|
52
|
+
}
|
|
53
|
+
return Object.prototype.hasOwnProperty.call(container, token)
|
|
54
|
+
? container[token]
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
/** Parse `key` as an array index valid INSIDE `array` (not for append). */
|
|
58
|
+
function arrayIndex(array, key, pointer) {
|
|
59
|
+
if (!/^\d+$/.test(key)) {
|
|
60
|
+
throw new Error(`not an array index: ${pointer}`);
|
|
61
|
+
}
|
|
62
|
+
const index = Number(key);
|
|
63
|
+
if (index >= array.length) {
|
|
64
|
+
throw new Error(`index out of bounds: ${pointer}`);
|
|
65
|
+
}
|
|
66
|
+
return index;
|
|
67
|
+
}
|
|
68
|
+
/** Parse `key` as an array index valid for INSERTION (== length allowed). */
|
|
69
|
+
function insertIndex(array, key, pointer) {
|
|
70
|
+
if (key === "-")
|
|
71
|
+
return array.length;
|
|
72
|
+
if (!/^\d+$/.test(key)) {
|
|
73
|
+
throw new Error(`not an array index: ${pointer}`);
|
|
74
|
+
}
|
|
75
|
+
const index = Number(key);
|
|
76
|
+
if (index > array.length) {
|
|
77
|
+
throw new Error(`index out of bounds: ${pointer}`);
|
|
78
|
+
}
|
|
79
|
+
return index;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* READ-ONLY walk: resolve the tokens before the last and return the
|
|
83
|
+
* parent container plus the final token. Throws when any step is
|
|
84
|
+
* missing or not a container.
|
|
85
|
+
*/
|
|
86
|
+
function resolveParent(root, tokens, opIndex, pointer) {
|
|
87
|
+
let node = root;
|
|
88
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
89
|
+
if (!isContainer(node)) {
|
|
90
|
+
throw new DataOpError(opIndex, `path not found: ${pointer}`);
|
|
91
|
+
}
|
|
92
|
+
const next = readToken(node, tokens[i]);
|
|
93
|
+
if (next === undefined) {
|
|
94
|
+
throw new DataOpError(opIndex, `path not found: ${pointer}`);
|
|
95
|
+
}
|
|
96
|
+
node = next;
|
|
97
|
+
}
|
|
98
|
+
if (!isContainer(node)) {
|
|
99
|
+
throw new DataOpError(opIndex, `path not found: ${pointer}`);
|
|
100
|
+
}
|
|
101
|
+
return { parent: node, key: tokens[tokens.length - 1] };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Clone-on-write rewrite: copies every container along `tokens` from
|
|
105
|
+
* `data` down, hands the (fresh, cloned) final container to `mutate`,
|
|
106
|
+
* and returns the new root. The input is never touched; branches off
|
|
107
|
+
* the path are shared by reference with it.
|
|
108
|
+
*/
|
|
109
|
+
function withWrite(data, tokens, mutate) {
|
|
110
|
+
const rootClone = { ...data };
|
|
111
|
+
let node = rootClone;
|
|
112
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
113
|
+
const child = readToken(node, tokens[i]);
|
|
114
|
+
if (child === undefined || !isContainer(child)) {
|
|
115
|
+
throw new Error(`path not found: /${tokens.join("/")}`);
|
|
116
|
+
}
|
|
117
|
+
const childClone = Array.isArray(child)
|
|
118
|
+
? [...child]
|
|
119
|
+
: { ...child };
|
|
120
|
+
if (Array.isArray(node)) {
|
|
121
|
+
node[Number(tokens[i])] = childClone;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
node[tokens[i]] = childClone;
|
|
125
|
+
}
|
|
126
|
+
node = childClone;
|
|
127
|
+
}
|
|
128
|
+
mutate(node, tokens[tokens.length - 1]);
|
|
129
|
+
return rootClone;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Apply a batch of `DataOp`s (spec §4.3) to `data`, returning a NEW
|
|
133
|
+
* object. Pure: untouched branches are shared by reference with the
|
|
134
|
+
* input; the input is never mutated. Ops apply sequentially — an
|
|
135
|
+
* earlier op's write is visible to the next.
|
|
136
|
+
*
|
|
137
|
+
* Throws `DataOpError` (naming the op's index) on any op whose path
|
|
138
|
+
* does not exist, or whose target is not an array where one is required.
|
|
139
|
+
*/
|
|
140
|
+
export function applyOps(data, ops) {
|
|
141
|
+
let current = data;
|
|
142
|
+
for (let opIndex = 0; opIndex < ops.length; opIndex++) {
|
|
143
|
+
const op = ops[opIndex];
|
|
144
|
+
try {
|
|
145
|
+
current = applyOne(current, op, opIndex);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error instanceof DataOpError)
|
|
149
|
+
throw error;
|
|
150
|
+
throw new DataOpError(opIndex, `${op.op} ${"path" in op ? op.path : ""}: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return current;
|
|
154
|
+
}
|
|
155
|
+
function applyOne(data, op, opIndex) {
|
|
156
|
+
switch (op.op) {
|
|
157
|
+
case "set":
|
|
158
|
+
return opSet(data, op.path, op.value, opIndex);
|
|
159
|
+
case "remove":
|
|
160
|
+
return opRemove(data, op.path, opIndex);
|
|
161
|
+
case "insert":
|
|
162
|
+
return opInsert(data, op.path, op.value, opIndex);
|
|
163
|
+
case "move":
|
|
164
|
+
return opMove(data, op.from, op.path, opIndex);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** `set`: assign at `path` (an existing array slot, or a created object member). */
|
|
168
|
+
function opSet(data, path, value, opIndex) {
|
|
169
|
+
const tokens = pointerTokens(path);
|
|
170
|
+
if (tokens.length === 0) {
|
|
171
|
+
throw new DataOpError(opIndex, "set at the root is not allowed");
|
|
172
|
+
}
|
|
173
|
+
const { parent, key } = resolveParent(data, tokens, opIndex, path);
|
|
174
|
+
if (Array.isArray(parent)) {
|
|
175
|
+
arrayIndex(parent, key, path); // validate before cloning
|
|
176
|
+
}
|
|
177
|
+
return withWrite(data, tokens, (container, finalKey) => {
|
|
178
|
+
if (Array.isArray(container)) {
|
|
179
|
+
container[arrayIndex(container, finalKey, path)] = value;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
container[finalKey] = value;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** `remove`: delete the member at `path` (shifting array elements). */
|
|
187
|
+
function opRemove(data, path, opIndex) {
|
|
188
|
+
const tokens = pointerTokens(path);
|
|
189
|
+
if (tokens.length === 0) {
|
|
190
|
+
throw new DataOpError(opIndex, "remove at the root is not allowed");
|
|
191
|
+
}
|
|
192
|
+
const { parent, key } = resolveParent(data, tokens, opIndex, path);
|
|
193
|
+
if (Array.isArray(parent)) {
|
|
194
|
+
arrayIndex(parent, key, path); // throws (→ opIndex) when out of bounds
|
|
195
|
+
}
|
|
196
|
+
else if (!Object.prototype.hasOwnProperty.call(parent, key)) {
|
|
197
|
+
throw new DataOpError(opIndex, `path not found: ${path}`);
|
|
198
|
+
}
|
|
199
|
+
return withWrite(data, tokens, (container, finalKey) => {
|
|
200
|
+
if (Array.isArray(container)) {
|
|
201
|
+
container.splice(arrayIndex(container, finalKey, path), 1);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
delete container[finalKey];
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** `insert`: insert `value` into the array at `path` (`-` appends). */
|
|
209
|
+
function opInsert(data, path, value, opIndex) {
|
|
210
|
+
const tokens = pointerTokens(path);
|
|
211
|
+
if (tokens.length === 0) {
|
|
212
|
+
throw new DataOpError(opIndex, `not an array: ${path}`);
|
|
213
|
+
}
|
|
214
|
+
const { parent } = resolveParent(data, tokens, opIndex, path);
|
|
215
|
+
if (!Array.isArray(parent)) {
|
|
216
|
+
throw new DataOpError(opIndex, `not an array: ${path}`);
|
|
217
|
+
}
|
|
218
|
+
return withWrite(data, tokens, (container, finalKey) => {
|
|
219
|
+
if (Array.isArray(container)) {
|
|
220
|
+
container.splice(insertIndex(container, finalKey, path), 0, value);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* `move`: remove the item at `from`, then insert it at `path` — a
|
|
226
|
+
* reorder within one array or a move across arrays (spec §4.3: it
|
|
227
|
+
* exists so reorders don't re-send the moved item). Both ends must be
|
|
228
|
+
* array slots; the destination index is resolved against the
|
|
229
|
+
* post-removal array.
|
|
230
|
+
*/
|
|
231
|
+
function opMove(data, from, path, opIndex) {
|
|
232
|
+
const fromTokens = pointerTokens(from);
|
|
233
|
+
const toTokens = pointerTokens(path);
|
|
234
|
+
if (fromTokens.length === 0 || toTokens.length === 0) {
|
|
235
|
+
throw new DataOpError(opIndex, "move at the root is not allowed");
|
|
236
|
+
}
|
|
237
|
+
const source = resolveParent(data, fromTokens, opIndex, from);
|
|
238
|
+
if (!Array.isArray(source.parent)) {
|
|
239
|
+
throw new DataOpError(opIndex, `not an array: ${from}`);
|
|
240
|
+
}
|
|
241
|
+
const value = readToken(source.parent, source.key);
|
|
242
|
+
if (value === undefined) {
|
|
243
|
+
throw new DataOpError(opIndex, `path not found: ${from}`);
|
|
244
|
+
}
|
|
245
|
+
// Remove first: a move into the same array sees post-removal indices.
|
|
246
|
+
const removed = withWrite(data, fromTokens, (container, finalKey) => {
|
|
247
|
+
if (Array.isArray(container)) {
|
|
248
|
+
container.splice(arrayIndex(container, finalKey, from), 1);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const to = resolveParent(removed, toTokens, opIndex, path);
|
|
252
|
+
if (!Array.isArray(to.parent)) {
|
|
253
|
+
throw new DataOpError(opIndex, `not an array: ${path}`);
|
|
254
|
+
}
|
|
255
|
+
return withWrite(removed, toTokens, (container, finalKey) => {
|
|
256
|
+
if (Array.isArray(container)) {
|
|
257
|
+
container.splice(insertIndex(container, finalKey, path), 0, value);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Resolve an RFC 6901 `pointer` against `data`; a pointer starting
|
|
263
|
+
* `./` resolves against `item` instead (spec §4.2's one extension,
|
|
264
|
+
* used inside collection item templates). A missing path — including a
|
|
265
|
+
* path THROUGH a JSON null, and a malformed pointer (no `/` or `./`
|
|
266
|
+
* prefix) — is `undefined`, never an error: "a binding to a missing
|
|
267
|
+
* path renders as empty, never as an error".
|
|
268
|
+
*/
|
|
269
|
+
export function resolvePointer(data, pointer, item) {
|
|
270
|
+
const base = pointer.startsWith("./") ? item : data;
|
|
271
|
+
if (base === undefined)
|
|
272
|
+
return undefined;
|
|
273
|
+
const rest = pointer.startsWith("./") ? pointer.slice(1) : pointer;
|
|
274
|
+
let tokens;
|
|
275
|
+
try {
|
|
276
|
+
tokens = pointerTokens(rest);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// A malformed binding renders empty — the render contract is
|
|
280
|
+
// undefined-never-throws (only `applyOps`, the server-side applier,
|
|
281
|
+
// reports bad pointers, as `DataOpError`).
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
let node = base;
|
|
285
|
+
for (const token of tokens) {
|
|
286
|
+
if (!isContainer(node))
|
|
287
|
+
return undefined;
|
|
288
|
+
const next = readToken(node, token);
|
|
289
|
+
if (next === undefined)
|
|
290
|
+
return undefined;
|
|
291
|
+
node = next;
|
|
292
|
+
}
|
|
293
|
+
return node;
|
|
294
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toText` (UI-blocks spec §14.3): the deterministic plain-text
|
|
3
|
+
* projection of a surface — headings, aligned tables, lists with
|
|
4
|
+
* checkboxes and numbers, lanes as sections, charts as one summary
|
|
5
|
+
* line per series. Used for the model's view of its own `show_blocks`
|
|
6
|
+
* call, the default `fallback` text, extension unit tests, logs and
|
|
7
|
+
* notifications.
|
|
8
|
+
*
|
|
9
|
+
* Determinism (spec §14.3): the same input, width and locale give the
|
|
10
|
+
* same bytes, so model context stays cacheable. Three rules make that
|
|
11
|
+
* hold:
|
|
12
|
+
*
|
|
13
|
+
* - object keys are visited in SCHEMA order, never insertion order —
|
|
14
|
+
* card slots, log lines, tree nodes and table cells are read through
|
|
15
|
+
* their named props, and anything rendered as JSON goes through
|
|
16
|
+
* `formatValue`'s sorted-key serializer;
|
|
17
|
+
* - no clock reads in the render path: `TextOptions.timeZone` defaults
|
|
18
|
+
* to UTC (a datetime projection is stable across hosts; pass a zone
|
|
19
|
+
* to localize), and only the `relative` format consults `Date.now()`;
|
|
20
|
+
* - unknown blocks render their `fallback`, else
|
|
21
|
+
* `[unsupported block: <type>]` (§4.4) — never a throw.
|
|
22
|
+
*/
|
|
23
|
+
import type { Tree } from "./types.js";
|
|
24
|
+
import type { JsonObject } from "../json.js";
|
|
25
|
+
/** Options for `toText`: the width to wrap at, and the `Intl` locale. */
|
|
26
|
+
export interface TextOptions {
|
|
27
|
+
/** Wrap target in columns (the §14.3 snapshots use 80 and 160). */
|
|
28
|
+
width: number;
|
|
29
|
+
/** BCP-47 locale for every `Intl` format. */
|
|
30
|
+
locale: string;
|
|
31
|
+
/**
|
|
32
|
+
* Time zone for datetime renders. Defaults to UTC inside `toText`
|
|
33
|
+
* (byte-stable projections across hosts); pass one to localize.
|
|
34
|
+
*/
|
|
35
|
+
timeZone?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Render `tree` against `data` as plain text (spec §14.3).
|
|
39
|
+
* Deterministic per (input, width, locale): the same arguments give the
|
|
40
|
+
* same bytes. Unknown blocks render their `fallback`, else
|
|
41
|
+
* `[unsupported block: <type>]`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function toText(tree: Tree, data: JsonObject, opts: TextOptions): string;
|