@loro-extended/change 1.1.0 → 3.0.0
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 +135 -56
- package/dist/index.d.ts +192 -94
- package/dist/index.js +271 -77
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/any-shape.test.ts +164 -0
- package/src/change.test.ts +4 -2
- package/src/derive-placeholder.ts +8 -0
- package/src/index.ts +13 -2
- package/src/overlay.ts +10 -0
- package/src/path-builder.ts +131 -0
- package/src/path-compiler.ts +64 -0
- package/src/path-evaluator.ts +76 -0
- package/src/path-selector.test.ts +322 -0
- package/src/path-selector.ts +131 -0
- package/src/readonly.test.ts +5 -4
- package/src/shape.ts +120 -5
- package/src/typed-refs/base.ts +6 -0
- package/src/typed-refs/counter.test.ts +2 -1
- package/src/typed-refs/doc.ts +13 -2
- package/src/typed-refs/json-compatibility.test.ts +27 -0
- package/src/typed-refs/list-base.ts +1 -1
- package/src/typed-refs/list.test.ts +1 -1
- package/src/typed-refs/list.ts +5 -2
- package/src/typed-refs/movable-list.test.ts +1 -1
- package/src/typed-refs/movable-list.ts +2 -2
- package/src/typed-refs/record.ts +11 -2
- package/src/typed-refs/struct.ts +9 -0
- package/src/typed-refs/tree.ts +6 -0
- package/src/typed-refs/utils.ts +13 -0
- package/src/validation.ts +9 -0
- package/src/presence-interface.ts +0 -52
- package/src/typed-presence.ts +0 -96
package/dist/index.js
CHANGED
|
@@ -8,6 +8,9 @@ function derivePlaceholder(schema) {
|
|
|
8
8
|
}
|
|
9
9
|
function deriveShapePlaceholder(shape) {
|
|
10
10
|
switch (shape._type) {
|
|
11
|
+
// Any container - no placeholder (undefined)
|
|
12
|
+
case "any":
|
|
13
|
+
return void 0;
|
|
11
14
|
// Leaf containers - use _placeholder directly
|
|
12
15
|
case "text":
|
|
13
16
|
return shape._placeholder;
|
|
@@ -36,6 +39,9 @@ function deriveShapePlaceholder(shape) {
|
|
|
36
39
|
}
|
|
37
40
|
function deriveValueShapePlaceholder(shape) {
|
|
38
41
|
switch (shape.valueType) {
|
|
42
|
+
// Any value - no placeholder (undefined)
|
|
43
|
+
case "any":
|
|
44
|
+
return void 0;
|
|
39
45
|
// Leaf values - use _placeholder directly
|
|
40
46
|
case "string":
|
|
41
47
|
return shape._placeholder;
|
|
@@ -146,6 +152,12 @@ function overlayPlaceholder(shape, crdtValue, placeholderValue) {
|
|
|
146
152
|
return result;
|
|
147
153
|
}
|
|
148
154
|
function mergeValue(shape, crdtValue, placeholderValue) {
|
|
155
|
+
if (shape._type === "any") {
|
|
156
|
+
return crdtValue;
|
|
157
|
+
}
|
|
158
|
+
if (shape._type === "value" && shape.valueType === "any") {
|
|
159
|
+
return crdtValue;
|
|
160
|
+
}
|
|
149
161
|
if (crdtValue === void 0 && placeholderValue === void 0) {
|
|
150
162
|
throw new Error("either crdt or placeholder value must be defined");
|
|
151
163
|
}
|
|
@@ -247,6 +259,154 @@ function mergeDiscriminatedUnion(shape, crdtValue, placeholderValue) {
|
|
|
247
259
|
return mergeValue(variantShape, crdtValue, effectivePlaceholderValue);
|
|
248
260
|
}
|
|
249
261
|
|
|
262
|
+
// src/path-builder.ts
|
|
263
|
+
function createPathSelector(segments) {
|
|
264
|
+
return {
|
|
265
|
+
__resultType: void 0,
|
|
266
|
+
__segments: segments
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function createPathNode(shape, segments) {
|
|
270
|
+
const selector = createPathSelector(segments);
|
|
271
|
+
if (shape._type === "text" || shape._type === "counter") {
|
|
272
|
+
return selector;
|
|
273
|
+
}
|
|
274
|
+
if (shape._type === "value") {
|
|
275
|
+
return selector;
|
|
276
|
+
}
|
|
277
|
+
if (shape._type === "list" || shape._type === "movableList") {
|
|
278
|
+
return Object.assign(selector, {
|
|
279
|
+
get $each() {
|
|
280
|
+
return createPathNode(shape.shape, [...segments, { type: "each" }]);
|
|
281
|
+
},
|
|
282
|
+
$at(index) {
|
|
283
|
+
return createPathNode(shape.shape, [
|
|
284
|
+
...segments,
|
|
285
|
+
{ type: "index", index }
|
|
286
|
+
]);
|
|
287
|
+
},
|
|
288
|
+
get $first() {
|
|
289
|
+
return createPathNode(shape.shape, [
|
|
290
|
+
...segments,
|
|
291
|
+
{ type: "index", index: 0 }
|
|
292
|
+
]);
|
|
293
|
+
},
|
|
294
|
+
get $last() {
|
|
295
|
+
return createPathNode(shape.shape, [
|
|
296
|
+
...segments,
|
|
297
|
+
{ type: "index", index: -1 }
|
|
298
|
+
]);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (shape._type === "struct") {
|
|
303
|
+
const props = {};
|
|
304
|
+
for (const key in shape.shapes) {
|
|
305
|
+
Object.defineProperty(props, key, {
|
|
306
|
+
get() {
|
|
307
|
+
return createPathNode(shape.shapes[key], [
|
|
308
|
+
...segments,
|
|
309
|
+
{ type: "property", key }
|
|
310
|
+
]);
|
|
311
|
+
},
|
|
312
|
+
enumerable: true
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return Object.assign(selector, props);
|
|
316
|
+
}
|
|
317
|
+
if (shape._type === "record") {
|
|
318
|
+
return Object.assign(selector, {
|
|
319
|
+
get $each() {
|
|
320
|
+
return createPathNode(shape.shape, [...segments, { type: "each" }]);
|
|
321
|
+
},
|
|
322
|
+
$key(key) {
|
|
323
|
+
return createPathNode(shape.shape, [...segments, { type: "key", key }]);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return selector;
|
|
328
|
+
}
|
|
329
|
+
function createPathBuilder(docShape) {
|
|
330
|
+
const builder = {};
|
|
331
|
+
for (const key in docShape.shapes) {
|
|
332
|
+
Object.defineProperty(builder, key, {
|
|
333
|
+
get() {
|
|
334
|
+
return createPathNode(docShape.shapes[key], [{ type: "property", key }]);
|
|
335
|
+
},
|
|
336
|
+
enumerable: true
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return builder;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/path-compiler.ts
|
|
343
|
+
function compileToJsonPath(segments) {
|
|
344
|
+
let path = "$";
|
|
345
|
+
for (const segment of segments) {
|
|
346
|
+
switch (segment.type) {
|
|
347
|
+
case "property":
|
|
348
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(segment.key)) {
|
|
349
|
+
path += `.${segment.key}`;
|
|
350
|
+
} else {
|
|
351
|
+
path += `["${escapeJsonPathKey(segment.key)}"]`;
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
case "each":
|
|
355
|
+
path += "[*]";
|
|
356
|
+
break;
|
|
357
|
+
case "index":
|
|
358
|
+
path += `[${segment.index}]`;
|
|
359
|
+
break;
|
|
360
|
+
case "key":
|
|
361
|
+
path += `["${escapeJsonPathKey(segment.key)}"]`;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return path;
|
|
366
|
+
}
|
|
367
|
+
function escapeJsonPathKey(key) {
|
|
368
|
+
return key.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
369
|
+
}
|
|
370
|
+
function hasWildcard(segments) {
|
|
371
|
+
return segments.some((s) => s.type === "each");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/path-evaluator.ts
|
|
375
|
+
function evaluatePath(doc, selector) {
|
|
376
|
+
const json = doc.$.toJSON();
|
|
377
|
+
return evaluatePathOnValue(json, selector.__segments);
|
|
378
|
+
}
|
|
379
|
+
function evaluatePathOnValue(value, segments) {
|
|
380
|
+
if (segments.length === 0) {
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
const [segment, ...rest] = segments;
|
|
384
|
+
switch (segment.type) {
|
|
385
|
+
case "property":
|
|
386
|
+
case "key":
|
|
387
|
+
if (value == null) return void 0;
|
|
388
|
+
if (typeof value !== "object") return void 0;
|
|
389
|
+
return evaluatePathOnValue(
|
|
390
|
+
value[segment.key],
|
|
391
|
+
rest
|
|
392
|
+
);
|
|
393
|
+
case "index": {
|
|
394
|
+
if (!Array.isArray(value)) return void 0;
|
|
395
|
+
const index = segment.index < 0 ? value.length + segment.index : segment.index;
|
|
396
|
+
if (index < 0 || index >= value.length) return void 0;
|
|
397
|
+
return evaluatePathOnValue(value[index], rest);
|
|
398
|
+
}
|
|
399
|
+
case "each":
|
|
400
|
+
if (Array.isArray(value)) {
|
|
401
|
+
return value.map((item) => evaluatePathOnValue(item, rest));
|
|
402
|
+
}
|
|
403
|
+
if (typeof value === "object" && value !== null) {
|
|
404
|
+
return Object.values(value).map((item) => evaluatePathOnValue(item, rest));
|
|
405
|
+
}
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
250
410
|
// src/placeholder-proxy.ts
|
|
251
411
|
function createPlaceholderProxy(target) {
|
|
252
412
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -301,6 +461,27 @@ var Shape = {
|
|
|
301
461
|
_mutable: {},
|
|
302
462
|
_placeholder: {}
|
|
303
463
|
}),
|
|
464
|
+
/**
|
|
465
|
+
* Creates an "any" container shape - an escape hatch for untyped containers.
|
|
466
|
+
* Use this when integrating with external libraries that manage their own document structure.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* ```typescript
|
|
470
|
+
* // loro-prosemirror manages its own structure
|
|
471
|
+
* const ProseMirrorDocShape = Shape.doc({
|
|
472
|
+
* doc: Shape.any(), // opt out of typing for this container
|
|
473
|
+
* })
|
|
474
|
+
*
|
|
475
|
+
* const handle = repo.get(docId, ProseMirrorDocShape, CursorPresenceShape)
|
|
476
|
+
* // handle.doc.doc is typed as `unknown` - you're on your own
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
any: () => ({
|
|
480
|
+
_type: "any",
|
|
481
|
+
_plain: void 0,
|
|
482
|
+
_mutable: void 0,
|
|
483
|
+
_placeholder: void 0
|
|
484
|
+
}),
|
|
304
485
|
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
305
486
|
// various CRDT algorithms
|
|
306
487
|
counter: () => {
|
|
@@ -459,12 +640,63 @@ var Shape = {
|
|
|
459
640
|
_mutable: void 0,
|
|
460
641
|
_placeholder: void 0
|
|
461
642
|
}),
|
|
462
|
-
uint8Array: () =>
|
|
643
|
+
uint8Array: () => {
|
|
644
|
+
const base = {
|
|
645
|
+
_type: "value",
|
|
646
|
+
valueType: "uint8array",
|
|
647
|
+
_plain: new Uint8Array(),
|
|
648
|
+
_mutable: new Uint8Array(),
|
|
649
|
+
_placeholder: new Uint8Array()
|
|
650
|
+
};
|
|
651
|
+
return Object.assign(base, {
|
|
652
|
+
nullable() {
|
|
653
|
+
return makeNullable(base);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
},
|
|
657
|
+
/**
|
|
658
|
+
* Alias for `uint8Array()` - creates a shape for binary data.
|
|
659
|
+
* Use this for better discoverability when working with binary data like cursor positions.
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* ```typescript
|
|
663
|
+
* const CursorPresenceShape = Shape.plain.struct({
|
|
664
|
+
* anchor: Shape.plain.bytes().nullable(),
|
|
665
|
+
* focus: Shape.plain.bytes().nullable(),
|
|
666
|
+
* })
|
|
667
|
+
* ```
|
|
668
|
+
*/
|
|
669
|
+
bytes: () => {
|
|
670
|
+
const base = {
|
|
671
|
+
_type: "value",
|
|
672
|
+
valueType: "uint8array",
|
|
673
|
+
_plain: new Uint8Array(),
|
|
674
|
+
_mutable: new Uint8Array(),
|
|
675
|
+
_placeholder: new Uint8Array()
|
|
676
|
+
};
|
|
677
|
+
return Object.assign(base, {
|
|
678
|
+
nullable() {
|
|
679
|
+
return makeNullable(base);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
},
|
|
683
|
+
/**
|
|
684
|
+
* Creates an "any" value shape - an escape hatch for untyped values.
|
|
685
|
+
* Use this when you need to accept any valid Loro value type.
|
|
686
|
+
*
|
|
687
|
+
* @example
|
|
688
|
+
* ```typescript
|
|
689
|
+
* const FlexiblePresenceShape = Shape.plain.struct({
|
|
690
|
+
* metadata: Shape.plain.any(), // accept any value type
|
|
691
|
+
* })
|
|
692
|
+
* ```
|
|
693
|
+
*/
|
|
694
|
+
any: () => ({
|
|
463
695
|
_type: "value",
|
|
464
|
-
valueType: "
|
|
465
|
-
_plain:
|
|
466
|
-
_mutable:
|
|
467
|
-
_placeholder:
|
|
696
|
+
valueType: "any",
|
|
697
|
+
_plain: void 0,
|
|
698
|
+
_mutable: void 0,
|
|
699
|
+
_placeholder: void 0
|
|
468
700
|
}),
|
|
469
701
|
/**
|
|
470
702
|
* Creates a struct value shape for plain objects with fixed keys.
|
|
@@ -1468,6 +1700,11 @@ var RecordRef = class extends TypedRef {
|
|
|
1468
1700
|
if (placeholder === void 0) {
|
|
1469
1701
|
placeholder = deriveShapePlaceholder(shape);
|
|
1470
1702
|
}
|
|
1703
|
+
if (!hasContainerConstructor(shape._type)) {
|
|
1704
|
+
throw new Error(
|
|
1705
|
+
`Cannot create typed ref for shape type "${shape._type}". Use Shape.any() only at the document root level.`
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1471
1708
|
const LoroContainer = containerConstructor[shape._type];
|
|
1472
1709
|
return {
|
|
1473
1710
|
shape,
|
|
@@ -1610,6 +1847,11 @@ var StructRef = class extends TypedRef {
|
|
|
1610
1847
|
}
|
|
1611
1848
|
getTypedRefParams(key, shape) {
|
|
1612
1849
|
const placeholder = this.placeholder?.[key];
|
|
1850
|
+
if (!hasContainerConstructor(shape._type)) {
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
`Cannot create typed ref for shape type "${shape._type}". Use Shape.any() only at the document root level.`
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1613
1855
|
const LoroContainer = containerConstructor[shape._type];
|
|
1614
1856
|
return {
|
|
1615
1857
|
shape,
|
|
@@ -1806,6 +2048,9 @@ var TextRef = class extends TypedRef {
|
|
|
1806
2048
|
var TreeRef = class extends TypedRef {
|
|
1807
2049
|
absorbPlainValues() {
|
|
1808
2050
|
}
|
|
2051
|
+
toJSON() {
|
|
2052
|
+
return this.container.toJSON();
|
|
2053
|
+
}
|
|
1809
2054
|
createNode(parent, index) {
|
|
1810
2055
|
this.assertMutable();
|
|
1811
2056
|
return this.container.createNode(parent, index);
|
|
@@ -1838,6 +2083,9 @@ var containerConstructor = {
|
|
|
1838
2083
|
text: LoroText2,
|
|
1839
2084
|
tree: LoroTree
|
|
1840
2085
|
};
|
|
2086
|
+
function hasContainerConstructor(type) {
|
|
2087
|
+
return type in containerConstructor;
|
|
2088
|
+
}
|
|
1841
2089
|
function unwrapReadonlyPrimitive(ref, shape) {
|
|
1842
2090
|
if (shape._type === "counter") {
|
|
1843
2091
|
return ref.value;
|
|
@@ -1977,7 +2225,13 @@ var DocRef = class extends TypedRef {
|
|
|
1977
2225
|
this.createLazyProperties();
|
|
1978
2226
|
}
|
|
1979
2227
|
getTypedRefParams(key, shape) {
|
|
1980
|
-
|
|
2228
|
+
if (shape._type === "any") {
|
|
2229
|
+
throw new Error(
|
|
2230
|
+
`Cannot get typed ref params for "any" shape type. The "any" shape is an escape hatch for untyped containers and should be accessed directly via loroDoc.`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
const getterName = containerGetter[shape._type];
|
|
2234
|
+
const getter = this._doc[getterName].bind(this._doc);
|
|
1981
2235
|
return {
|
|
1982
2236
|
shape,
|
|
1983
2237
|
placeholder: this.requiredPlaceholder[key],
|
|
@@ -2032,6 +2286,9 @@ function validateValue(value, schema, path = "") {
|
|
|
2032
2286
|
throw new Error(`Invalid schema at path ${path}: missing _type`);
|
|
2033
2287
|
}
|
|
2034
2288
|
const currentPath = path || "root";
|
|
2289
|
+
if (schema._type === "any") {
|
|
2290
|
+
return value;
|
|
2291
|
+
}
|
|
2035
2292
|
if (schema._type === "text") {
|
|
2036
2293
|
if (typeof value !== "string") {
|
|
2037
2294
|
throw new Error(
|
|
@@ -2099,6 +2356,9 @@ function validateValue(value, schema, path = "") {
|
|
|
2099
2356
|
if (schema._type === "value") {
|
|
2100
2357
|
const valueSchema = schema;
|
|
2101
2358
|
switch (valueSchema.valueType) {
|
|
2359
|
+
// AnyValueShape - no validation, accept anything
|
|
2360
|
+
case "any":
|
|
2361
|
+
return value;
|
|
2102
2362
|
case "string": {
|
|
2103
2363
|
if (typeof value !== "string") {
|
|
2104
2364
|
throw new Error(
|
|
@@ -2412,85 +2672,19 @@ function createTypedDoc(shape, existingDoc) {
|
|
|
2412
2672
|
internal.proxy = proxy;
|
|
2413
2673
|
return proxy;
|
|
2414
2674
|
}
|
|
2415
|
-
|
|
2416
|
-
// src/typed-presence.ts
|
|
2417
|
-
var TypedPresence = class {
|
|
2418
|
-
constructor(shape, presence) {
|
|
2419
|
-
this.shape = shape;
|
|
2420
|
-
this.presence = presence;
|
|
2421
|
-
this.placeholder = deriveShapePlaceholder(shape);
|
|
2422
|
-
}
|
|
2423
|
-
placeholder;
|
|
2424
|
-
/**
|
|
2425
|
-
* Get the current peer's presence state with placeholder values merged in.
|
|
2426
|
-
*/
|
|
2427
|
-
get self() {
|
|
2428
|
-
return mergeValue(
|
|
2429
|
-
this.shape,
|
|
2430
|
-
this.presence.self,
|
|
2431
|
-
this.placeholder
|
|
2432
|
-
);
|
|
2433
|
-
}
|
|
2434
|
-
/**
|
|
2435
|
-
* Get other peers' presence states with placeholder values merged in.
|
|
2436
|
-
* Does NOT include self. Use this for iterating over remote peers.
|
|
2437
|
-
*/
|
|
2438
|
-
get peers() {
|
|
2439
|
-
const result = /* @__PURE__ */ new Map();
|
|
2440
|
-
for (const [peerId, value] of this.presence.peers) {
|
|
2441
|
-
result.set(
|
|
2442
|
-
peerId,
|
|
2443
|
-
mergeValue(this.shape, value, this.placeholder)
|
|
2444
|
-
);
|
|
2445
|
-
}
|
|
2446
|
-
return result;
|
|
2447
|
-
}
|
|
2448
|
-
/**
|
|
2449
|
-
* Get all peers' presence states with placeholder values merged in.
|
|
2450
|
-
* @deprecated Use `peers` and `self` separately. This property is synthesized
|
|
2451
|
-
* from `peers` and `self` for backward compatibility.
|
|
2452
|
-
*/
|
|
2453
|
-
get all() {
|
|
2454
|
-
const result = {};
|
|
2455
|
-
const all = this.presence.all;
|
|
2456
|
-
for (const peerId of Object.keys(all)) {
|
|
2457
|
-
result[peerId] = mergeValue(
|
|
2458
|
-
this.shape,
|
|
2459
|
-
all[peerId],
|
|
2460
|
-
this.placeholder
|
|
2461
|
-
);
|
|
2462
|
-
}
|
|
2463
|
-
return result;
|
|
2464
|
-
}
|
|
2465
|
-
/**
|
|
2466
|
-
* Set presence values for the current peer.
|
|
2467
|
-
*/
|
|
2468
|
-
set(value) {
|
|
2469
|
-
this.presence.set(value);
|
|
2470
|
-
}
|
|
2471
|
-
/**
|
|
2472
|
-
* Subscribe to presence changes.
|
|
2473
|
-
* The callback is called immediately with the current state, then on each change.
|
|
2474
|
-
*
|
|
2475
|
-
* @param cb Callback that receives the typed presence state
|
|
2476
|
-
* @returns Unsubscribe function
|
|
2477
|
-
*/
|
|
2478
|
-
subscribe(cb) {
|
|
2479
|
-
cb({ self: this.self, peers: this.peers, all: this.all });
|
|
2480
|
-
return this.presence.subscribe(() => {
|
|
2481
|
-
cb({ self: this.self, peers: this.peers, all: this.all });
|
|
2482
|
-
});
|
|
2483
|
-
}
|
|
2484
|
-
};
|
|
2485
2675
|
export {
|
|
2486
2676
|
Shape,
|
|
2487
|
-
TypedPresence,
|
|
2488
2677
|
change,
|
|
2678
|
+
compileToJsonPath,
|
|
2679
|
+
createPathBuilder,
|
|
2489
2680
|
createPlaceholderProxy,
|
|
2490
2681
|
createTypedDoc,
|
|
2491
2682
|
derivePlaceholder,
|
|
2492
2683
|
deriveShapePlaceholder,
|
|
2684
|
+
evaluatePath,
|
|
2685
|
+
evaluatePathOnValue,
|
|
2493
2686
|
getLoroDoc,
|
|
2687
|
+
hasWildcard,
|
|
2494
2688
|
mergeValue,
|
|
2495
2689
|
overlayPlaceholder,
|
|
2496
2690
|
validatePlaceholder
|