@alloy-js/core 0.22.0-dev.1 → 0.22.0-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/components/Block.d.ts.map +1 -1
- package/dist/src/components/Block.js +24 -7
- package/dist/src/components/Block.js.map +1 -1
- package/dist/src/components/Indent.d.ts.map +1 -1
- package/dist/src/components/Indent.js +2 -1
- package/dist/src/components/Indent.js.map +1 -1
- package/dist/src/components/Prose.d.ts.map +1 -1
- package/dist/src/components/Prose.js +2 -1
- package/dist/src/components/Prose.js.map +1 -1
- package/dist/src/content-slot.d.ts +51 -0
- package/dist/src/content-slot.d.ts.map +1 -0
- package/dist/src/content-slot.js +69 -0
- package/dist/src/content-slot.js.map +1 -0
- package/dist/src/content-slot.test.d.ts +2 -0
- package/dist/src/content-slot.test.d.ts.map +1 -0
- package/dist/src/content-slot.test.js +57 -0
- package/dist/src/content-slot.test.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/reactivity.d.ts +15 -1
- package/dist/src/reactivity.d.ts.map +1 -1
- package/dist/src/reactivity.js +20 -8
- package/dist/src/reactivity.js.map +1 -1
- package/dist/src/render.d.ts +24 -0
- package/dist/src/render.d.ts.map +1 -1
- package/dist/src/render.js +98 -2
- package/dist/src/render.js.map +1 -1
- package/dist/src/symbols/decl.d.ts +8 -0
- package/dist/src/symbols/decl.d.ts.map +1 -0
- package/dist/src/symbols/decl.js +22 -0
- package/dist/src/symbols/decl.js.map +1 -0
- package/dist/src/symbols/index.d.ts +1 -0
- package/dist/src/symbols/index.d.ts.map +1 -1
- package/dist/src/symbols/index.js +1 -0
- package/dist/src/symbols/index.js.map +1 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +212 -15
- package/dist/src/utils.js.map +1 -1
- package/dist/test/components/block.test.d.ts.map +1 -1
- package/dist/test/components/block.test.js +18 -1
- package/dist/test/components/block.test.js.map +1 -1
- package/dist/test/components/list.test.d.ts.map +1 -1
- package/dist/test/components/list.test.js +80 -1
- package/dist/test/components/list.test.js.map +1 -1
- package/dist/test/control-flow/for.test.js +32 -2
- package/dist/test/control-flow/for.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/components/Block.tsx +18 -6
- package/src/components/Indent.tsx +4 -2
- package/src/components/Prose.tsx +2 -1
- package/src/content-slot.test.tsx +65 -0
- package/src/content-slot.tsx +91 -0
- package/src/index.ts +1 -0
- package/src/reactivity.ts +38 -5
- package/src/render.ts +112 -3
- package/src/symbols/decl.ts +25 -0
- package/src/symbols/index.ts +1 -0
- package/src/utils.tsx +240 -16
- package/temp/api.json +550 -4
- package/test/components/block.test.tsx +21 -1
- package/test/components/list.test.tsx +76 -1
- package/test/control-flow/for.test.tsx +43 -2
package/src/utils.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { toRaw } from "@vue/reactivity";
|
|
1
|
+
import { ref, Ref, toRaw } from "@vue/reactivity";
|
|
2
2
|
import { BaseListProps } from "./components/List.jsx";
|
|
3
3
|
import {
|
|
4
4
|
createCustomContext,
|
|
5
5
|
CustomContext,
|
|
6
6
|
Disposable,
|
|
7
|
+
effect,
|
|
8
|
+
getContext,
|
|
7
9
|
memo,
|
|
8
10
|
onCleanup,
|
|
9
11
|
root,
|
|
@@ -92,16 +94,173 @@ export function mapJoin<T, U, V>(
|
|
|
92
94
|
cb: (key: T, valueOrIndex: U | number, index: number) => V,
|
|
93
95
|
rawOptions: JoinOptions = {},
|
|
94
96
|
): () => Children {
|
|
97
|
+
/**
|
|
98
|
+
* Strategy overview:
|
|
99
|
+
* - Initial render: collect a stable array of mapper outputs, cache
|
|
100
|
+
* per-index slot metadata, and create joiner `Ref`s for the gaps between
|
|
101
|
+
* items so the rendered list can be mutated without recreating siblings.
|
|
102
|
+
* - List mutations: diff the incoming iterable against slot metadata, reuse
|
|
103
|
+
* entries with matching keys, recycle disposers past the change point, and
|
|
104
|
+
* prune caches while recalculating first/last non-empty indices so we only
|
|
105
|
+
* edit joiners that are affected by the change.
|
|
106
|
+
* - Content transitions: each child renders inside a custom context that
|
|
107
|
+
* exposes an `isEmpty` flag; when a child gains or loses content we update
|
|
108
|
+
* neighbor joiner refs so separators appear and disappear reactively
|
|
109
|
+
* without re-running the mapper.
|
|
110
|
+
*/
|
|
95
111
|
const options = { ...defaultJoinOptions, ...rawOptions };
|
|
96
112
|
const ender =
|
|
97
113
|
options.ender === true ? options.joiner : options.ender || undefined;
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
type MapJoinItem = T | [T, U];
|
|
115
|
+
interface MapJoinSlot {
|
|
116
|
+
item?: MapJoinItem;
|
|
117
|
+
disposer?: Disposable;
|
|
118
|
+
joiner?: Ref<Children | undefined>;
|
|
119
|
+
isEmpty: Ref<boolean>;
|
|
120
|
+
}
|
|
121
|
+
const itemSlots: MapJoinSlot[] = [];
|
|
122
|
+
|
|
123
|
+
function getOrCreateSlot(index: number): MapJoinSlot {
|
|
124
|
+
let slot = itemSlots[index];
|
|
125
|
+
if (!slot) {
|
|
126
|
+
slot = { isEmpty: ref(true) };
|
|
127
|
+
itemSlots[index] = slot;
|
|
128
|
+
}
|
|
129
|
+
return slot;
|
|
130
|
+
}
|
|
131
|
+
const firstNonEmptyIndex = ref(-1);
|
|
132
|
+
const lastNonEmptyIndex = ref(-1);
|
|
100
133
|
const mapped: Children[] = [];
|
|
134
|
+
let enderMemo: (() => Children) | undefined;
|
|
135
|
+
|
|
136
|
+
// Creates a ref placeholder that stores the joiner node for a boundary.
|
|
137
|
+
function createJoinerRef(): Ref<Children | undefined> {
|
|
138
|
+
return ref<unknown>(undefined) as Ref<Children | undefined>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Makes sure we have a joiner ref at the requested boundary index.
|
|
142
|
+
function ensureJoinerSlot(index: number) {
|
|
143
|
+
if (index === 0 || options.joiner === undefined) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const slot = getOrCreateSlot(index);
|
|
148
|
+
slot.joiner ??= createJoinerRef();
|
|
149
|
+
mapped[index * 2 - 1] = slot.joiner!;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Sets the joiner ref based on whether neighboring children have content.
|
|
153
|
+
function updateJoinerForIndex(index: number) {
|
|
154
|
+
if (index === 0 || options.joiner === undefined) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const slot = itemSlots[index];
|
|
159
|
+
const joinerRef = slot?.joiner;
|
|
160
|
+
if (!joinerRef) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (index >= itemSlots.length) {
|
|
165
|
+
if (joinerRef.value !== undefined) {
|
|
166
|
+
joinerRef.value = undefined;
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const previousNonEmpty = findPrevNonEmpty(index - 1);
|
|
172
|
+
const rightSlot = itemSlots[index];
|
|
173
|
+
const shouldShow =
|
|
174
|
+
previousNonEmpty !== -1 &&
|
|
175
|
+
rightSlot !== undefined &&
|
|
176
|
+
rightSlot.isEmpty.value === false;
|
|
177
|
+
|
|
178
|
+
const newValue = shouldShow ? options.joiner : undefined;
|
|
179
|
+
if (joinerRef.value !== newValue) {
|
|
180
|
+
joinerRef.value = newValue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Finds the next child that is not empty starting at or after `from`.
|
|
185
|
+
function findNextNonEmpty(from: number) {
|
|
186
|
+
for (let i = Math.max(from, 0); i < itemSlots.length; i++) {
|
|
187
|
+
const slot = itemSlots[i];
|
|
188
|
+
if (slot && slot.isEmpty.value === false) {
|
|
189
|
+
return i;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return -1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Finds the previous child that is not empty before or at `from`.
|
|
196
|
+
function findPrevNonEmpty(from: number) {
|
|
197
|
+
for (let i = Math.min(from, itemSlots.length - 1); i >= 0; i--) {
|
|
198
|
+
const slot = itemSlots[i];
|
|
199
|
+
if (slot && slot.isEmpty.value === false) {
|
|
200
|
+
return i;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return -1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Recomputes the cached first and last non-empty indices after changes.
|
|
207
|
+
function recalculateNonEmptyBounds() {
|
|
208
|
+
const first = findNextNonEmpty(0);
|
|
209
|
+
firstNonEmptyIndex.value = first;
|
|
210
|
+
lastNonEmptyIndex.value =
|
|
211
|
+
first === -1 ? -1 : findPrevNonEmpty(itemSlots.length - 1);
|
|
212
|
+
refreshJoiners();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Syncs all joiner refs after the structural shape of the list updates.
|
|
216
|
+
function refreshJoiners() {
|
|
217
|
+
if (options.joiner === undefined) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (let i = 1; i < itemSlots.length; i++) {
|
|
222
|
+
const slot = itemSlots[i];
|
|
223
|
+
if (slot?.joiner) {
|
|
224
|
+
updateJoinerForIndex(i);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Applies empty-state bookkeeping for an item and updates neighbor joiners.
|
|
230
|
+
function applyEmptyStateChange(
|
|
231
|
+
index: number,
|
|
232
|
+
isEmpty: boolean,
|
|
233
|
+
wasEmpty: boolean,
|
|
234
|
+
) {
|
|
235
|
+
if (wasEmpty === isEmpty) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isEmpty) {
|
|
240
|
+
if (firstNonEmptyIndex.value === index) {
|
|
241
|
+
firstNonEmptyIndex.value = findNextNonEmpty(index + 1);
|
|
242
|
+
}
|
|
243
|
+
if (lastNonEmptyIndex.value === index) {
|
|
244
|
+
lastNonEmptyIndex.value = findPrevNonEmpty(index - 1);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
if (firstNonEmptyIndex.value === -1 || index < firstNonEmptyIndex.value) {
|
|
248
|
+
firstNonEmptyIndex.value = index;
|
|
249
|
+
}
|
|
250
|
+
if (index > lastNonEmptyIndex.value) {
|
|
251
|
+
lastNonEmptyIndex.value = index;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
updateJoinerForIndex(index);
|
|
256
|
+
updateJoinerForIndex(index + 1);
|
|
257
|
+
}
|
|
101
258
|
let previousItemsLen = 0;
|
|
102
259
|
|
|
103
260
|
onCleanup(() => {
|
|
104
|
-
for (const
|
|
261
|
+
for (const slot of itemSlots) {
|
|
262
|
+
slot.disposer?.();
|
|
263
|
+
}
|
|
105
264
|
});
|
|
106
265
|
|
|
107
266
|
return () => {
|
|
@@ -117,6 +276,11 @@ export function mapJoin<T, U, V>(
|
|
|
117
276
|
item !== null && item !== undefined && typeof item !== "boolean",
|
|
118
277
|
);
|
|
119
278
|
}
|
|
279
|
+
|
|
280
|
+
const context = getContext();
|
|
281
|
+
if (context) {
|
|
282
|
+
context.isEmpty ??= ref(true);
|
|
283
|
+
}
|
|
120
284
|
// this is important to access here in reactive context so we are
|
|
121
285
|
// notified of new items from reactives.
|
|
122
286
|
const itemsLen = items.length;
|
|
@@ -127,55 +291,110 @@ export function mapJoin<T, U, V>(
|
|
|
127
291
|
let startIndex = 0;
|
|
128
292
|
for (
|
|
129
293
|
;
|
|
130
|
-
startIndex < itemsLen && startIndex <
|
|
294
|
+
startIndex < itemsLen && startIndex < itemSlots.length;
|
|
131
295
|
startIndex++
|
|
132
296
|
) {
|
|
133
|
-
|
|
297
|
+
const slot = itemSlots[startIndex];
|
|
298
|
+
if (!slot || !compare(items[startIndex], slot.item as any)) {
|
|
134
299
|
break;
|
|
135
300
|
}
|
|
136
301
|
}
|
|
137
302
|
|
|
138
303
|
if (startIndex > 0 && startIndex < itemsLen) {
|
|
139
304
|
// need to update the previous joiner (might be ender or absent)
|
|
140
|
-
|
|
305
|
+
ensureJoinerSlot(startIndex);
|
|
141
306
|
}
|
|
142
307
|
|
|
143
308
|
for (; startIndex < itemsLen; startIndex++) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
309
|
+
const slot = getOrCreateSlot(startIndex);
|
|
310
|
+
slot.item = items[startIndex];
|
|
311
|
+
const emptyFlag = slot.isEmpty;
|
|
312
|
+
|
|
313
|
+
if (slot.disposer) {
|
|
314
|
+
if (emptyFlag.value === false) {
|
|
315
|
+
if (context) {
|
|
316
|
+
context.childrenWithContent--;
|
|
317
|
+
}
|
|
318
|
+
emptyFlag.value = true;
|
|
319
|
+
applyEmptyStateChange(startIndex, true, false);
|
|
320
|
+
}
|
|
321
|
+
slot.disposer();
|
|
147
322
|
}
|
|
323
|
+
|
|
148
324
|
const cleanupIndex = startIndex;
|
|
325
|
+
|
|
149
326
|
mapped[startIndex * 2] = createCustomContext((cb) => {
|
|
150
327
|
return root((disposer) => {
|
|
151
|
-
|
|
328
|
+
const nestedContext = getContext()!;
|
|
329
|
+
const isEmptyFlag = nestedContext.isEmpty!;
|
|
330
|
+
|
|
331
|
+
slot.disposer = disposer;
|
|
152
332
|
disposer();
|
|
333
|
+
effect((prev?: boolean) => {
|
|
334
|
+
const isEmpty = isEmptyFlag.value;
|
|
335
|
+
return untrack(() => {
|
|
336
|
+
if (slot.isEmpty.value !== isEmpty) {
|
|
337
|
+
slot.isEmpty.value = isEmpty;
|
|
338
|
+
}
|
|
339
|
+
const wasEmpty = prev ?? true;
|
|
340
|
+
|
|
341
|
+
applyEmptyStateChange(cleanupIndex, isEmpty, wasEmpty);
|
|
342
|
+
|
|
343
|
+
return isEmpty;
|
|
344
|
+
});
|
|
345
|
+
});
|
|
153
346
|
cb(mapper(items[cleanupIndex], cleanupIndex));
|
|
154
347
|
});
|
|
155
348
|
});
|
|
156
349
|
|
|
157
|
-
|
|
158
|
-
startIndex < items.length - 1 ? options.joiner : ender;
|
|
350
|
+
ensureJoinerSlot(startIndex);
|
|
159
351
|
}
|
|
160
352
|
|
|
161
353
|
mapped.length = startIndex * 2;
|
|
162
|
-
mapped
|
|
354
|
+
if (mapped.length > 0) {
|
|
355
|
+
if (ender !== undefined) {
|
|
356
|
+
enderMemo ??= memo(() =>
|
|
357
|
+
lastNonEmptyIndex.value === -1 ? undefined : ender,
|
|
358
|
+
);
|
|
359
|
+
mapped[mapped.length - 1] = enderMemo;
|
|
360
|
+
} else {
|
|
361
|
+
mapped[mapped.length - 1] = undefined;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
163
364
|
for (; startIndex < previousItemsLen; startIndex++) {
|
|
164
|
-
|
|
365
|
+
const slot = itemSlots[startIndex];
|
|
366
|
+
if (!slot) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
slot.disposer?.();
|
|
371
|
+
if (slot.isEmpty.value === false) {
|
|
372
|
+
if (context) {
|
|
373
|
+
context.childrenWithContent--;
|
|
374
|
+
}
|
|
375
|
+
slot.isEmpty.value = true;
|
|
376
|
+
applyEmptyStateChange(startIndex, true, false);
|
|
377
|
+
}
|
|
165
378
|
}
|
|
166
379
|
|
|
167
|
-
previousItemsLen
|
|
380
|
+
if (previousItemsLen !== itemsLen) {
|
|
381
|
+
itemSlots.length = itemsLen;
|
|
382
|
+
recalculateNonEmptyBounds();
|
|
383
|
+
}
|
|
168
384
|
|
|
385
|
+
previousItemsLen = itemsLen;
|
|
169
386
|
return mapped;
|
|
170
387
|
});
|
|
171
388
|
};
|
|
172
389
|
|
|
390
|
+
// Chooses the equality function based on the collection type in use.
|
|
173
391
|
function getCompareFunction(itemsSource: Map<T, U> | T[] | Iterable<T>) {
|
|
174
392
|
return Array.isArray(itemsSource) || isIterable(itemsSource) ?
|
|
175
393
|
compareArray
|
|
176
394
|
: compareMap;
|
|
177
395
|
}
|
|
178
396
|
|
|
397
|
+
// Selects the mapper signature to match the collection type in use.
|
|
179
398
|
function getMapperFunction(itemsSource: Map<T, U> | T[] | Iterable<T>) {
|
|
180
399
|
return (
|
|
181
400
|
Array.isArray(itemsSource) ||
|
|
@@ -185,22 +404,27 @@ export function mapJoin<T, U, V>(
|
|
|
185
404
|
mapArray
|
|
186
405
|
: mapMap;
|
|
187
406
|
}
|
|
407
|
+
// Strict equality check for array-like collections.
|
|
188
408
|
function compareArray(elem1: T, elem2: T) {
|
|
189
409
|
return elem1 === elem2;
|
|
190
410
|
}
|
|
191
411
|
|
|
412
|
+
// Equality check for map entries that includes key and value.
|
|
192
413
|
function compareMap(record1: [T, U], record2: [T, U]) {
|
|
193
414
|
return record1[0] === record2[0] && record1[1] === record2[1];
|
|
194
415
|
}
|
|
195
416
|
|
|
417
|
+
// Invokes the user mapper for array-like collections.
|
|
196
418
|
function mapArray(item: T, index: number) {
|
|
197
419
|
return (cb as any)(item, index);
|
|
198
420
|
}
|
|
199
421
|
|
|
422
|
+
// Invokes the user mapper for map collections.
|
|
200
423
|
function mapMap(item: [T, U], index: number) {
|
|
201
424
|
return cb(item[0], item[1], index);
|
|
202
425
|
}
|
|
203
426
|
|
|
427
|
+
// Runtime type guard for iterables returned from custom sources.
|
|
204
428
|
function isIterable<T>(x: unknown): x is Iterable<T> {
|
|
205
429
|
return typeof (x as any).next === "function";
|
|
206
430
|
}
|