docblocks-core 0.1.0 → 0.1.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.
- package/CHANGELOG.md +69 -0
- package/README.md +8 -6
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +115 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# docblocks-core
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bf92634: The README's snapshot shape stands the actual JSON form: the document
|
|
8
|
+
fields (`id`, `title`, the optional `avatar`, `createdAt`) stand at the
|
|
9
|
+
top level of the snapshot next to the `blocks` map — the former `meta` /
|
|
10
|
+
`children` top-level description was wrong (the `children` order lives
|
|
11
|
+
inside the blocks, the `meta` object does not exist in the JSON).
|
|
12
|
+
- b66e45e: The cross-minor as-is stand reaches the KNOWN block types: an additive
|
|
13
|
+
top-level field (a future minor's data) on a known type no longer stands
|
|
14
|
+
dropped on `fromJSON` — the layout writer keeps it with the generic
|
|
15
|
+
storage semantics (the opaque writer's parity) and the snapshot reader
|
|
16
|
+
passes it through (sorted keys, the `Y.Text` field reads as runs), so a
|
|
17
|
+
load → save cycle never loses unknown data.
|
|
18
|
+
|
|
19
|
+
The mark value's as-is stand completes too: an unknown mark with a
|
|
20
|
+
non-boolean primitive value (string / number) is preserved on read —
|
|
21
|
+
the reader previously kept only the `true` and the object value shapes,
|
|
22
|
+
silently dropping the primitive ones on save.
|
|
23
|
+
- c5a38d2: The link href's gate mirrors the WHATWG URL parser's full input
|
|
24
|
+
normalization: ASCII tab / LF / CR (U+0009–U+000D) are removed from ANY
|
|
25
|
+
position (plus the edge C0 / space trim) before the scheme detection. A
|
|
26
|
+
mid-string control could not hide the scheme before — `java\tscript:…`
|
|
27
|
+
carried no scheme per the `^[a-z][a-z0-9+.-]*:` test, while the browser's
|
|
28
|
+
URL parser strips the control and resolves the `javascript:` scheme, so a
|
|
29
|
+
documented link executed as script. The gate is the single stand of the
|
|
30
|
+
validator's link check, the renderer's href gate, and the paste's
|
|
31
|
+
markdown link re-gate.
|
|
32
|
+
- 769c12a: The reactivity's handler isolation: a subscription handler's throw (the
|
|
33
|
+
handlers fire inside the Yjs transaction, the afterTransaction hook) no
|
|
34
|
+
longer rolls back the caller's committed batch (the `transact`'s rollback
|
|
35
|
+
guard would catch the listener's throw otherwise) and no longer aborts the
|
|
36
|
+
remaining handlers of the emission — the throw stands the logged error,
|
|
37
|
+
the emission continues, and the batch stays committed and undoable.
|
|
38
|
+
|
|
39
|
+
The export's stable key order: `toJSON` assembles the blocks in sorted id
|
|
40
|
+
order — the Y.Map's iteration order depends on the creation history, so
|
|
41
|
+
converged peers stood the same data in different key orders before. The
|
|
42
|
+
converged peers' exports are byte-identical now (the `JSON.stringify`'s
|
|
43
|
+
bytes stand the convergence's check, not just the deep equality); the JSON's
|
|
44
|
+
object order is semantically irrelevant — the children's arrays carry the
|
|
45
|
+
document order.
|
|
46
|
+
- 72fdfd3: The block and mark registry lookups stand the own-property stand over the
|
|
47
|
+
prototype-key values (`constructor`, `toString`, ...): the unknown block
|
|
48
|
+
type named by a prototype key keeps its data over the `fromJSON` /
|
|
49
|
+
`toJSON` round-trip (the opaque stand — the reader's lenient contract),
|
|
50
|
+
the strict commands refuse the unknown type and mark, and the export's
|
|
51
|
+
attr assembly writes own properties only (no prototype swap over a
|
|
52
|
+
hostile attr key).
|
|
53
|
+
|
|
54
|
+
The same stand over the remaining lookup points: the `changeBlockType`'s
|
|
55
|
+
attrs merge (the target spec's whitelist membership over the own
|
|
56
|
+
properties — the hostile key stands reset like the type-specific attrs,
|
|
57
|
+
the patch's hostile key stands the strict rejection, the own writes keep
|
|
58
|
+
the prototype's setters out), the strict mark-field validation (an
|
|
59
|
+
unknown field named like an `Object.prototype` member stands the
|
|
60
|
+
rejection — the strict posture rejects rather than preserves it), and the
|
|
61
|
+
export's
|
|
62
|
+
id-keyed assemblies (the own writes over the `blocks` / `rows`
|
|
63
|
+
dictionaries — the wire's defense in depth).
|
|
64
|
+
|
|
65
|
+
## 0.1.1
|
|
66
|
+
|
|
67
|
+
### Patch Changes
|
|
68
|
+
|
|
69
|
+
- The README's `createDoc`'s contract stands the actual initial state (only
|
|
70
|
+
the root block — no auto first paragraph).
|
|
71
|
+
|
|
3
72
|
## 0.1.0
|
|
4
73
|
|
|
5
74
|
### Added
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ const para = doc.insertBlock("paragraph");
|
|
|
31
31
|
doc.insertText(para, 0, "hello world");
|
|
32
32
|
doc.setMark(para, 0, 5, { type: "bold" });
|
|
33
33
|
|
|
34
|
-
const json = doc.toJSON(); // { v,
|
|
34
|
+
const json = doc.toJSON(); // { v, id, title, avatar?, createdAt, blocks }
|
|
35
35
|
// …persist or send `json` …
|
|
36
36
|
const restored = fromJSON(json); // a fresh Doc over the same data
|
|
37
37
|
```
|
|
@@ -56,13 +56,14 @@ silently.
|
|
|
56
56
|
|
|
57
57
|
## The Doc
|
|
58
58
|
|
|
59
|
-
`createDoc(options?)` builds a document with
|
|
60
|
-
|
|
59
|
+
`createDoc(options?)` builds a document with only the root block (an empty
|
|
60
|
+
page — no auto first paragraph; the react-ui editor guarantees the trailing
|
|
61
|
+
editable paragraph). The other entry points:
|
|
61
62
|
|
|
62
63
|
- `fromJSON(json)` — build a `Doc` from a JSON snapshot.
|
|
63
64
|
- `loadDoc(bytes)` — build a `Doc` from an encoded full state (a base
|
|
64
65
|
snapshot; apply diffs with `applyUpdate`).
|
|
65
|
-
- `doc.toJSON()` — the JSON snapshot (`{ v,
|
|
66
|
+
- `doc.toJSON()` — the JSON snapshot (`{ v, id, title, avatar?, createdAt, blocks }`).
|
|
66
67
|
- `doc.encode()` / `doc.encodeUpdate(stateVector?)` — the encoded full
|
|
67
68
|
state / the incremental update.
|
|
68
69
|
- `doc.applyUpdate(bytes)` — apply an encoded update (the remote-apply path).
|
|
@@ -127,8 +128,9 @@ projects a selection into the presence payload shape.
|
|
|
127
128
|
|
|
128
129
|
## Data format
|
|
129
130
|
|
|
130
|
-
The JSON format is `v: 1`:
|
|
131
|
-
`avatar`) plus a **flat block map**
|
|
131
|
+
The JSON format is `v: 1`: the document fields (`id`, `title`, the
|
|
132
|
+
optional `avatar`, `createdAt`) at the top level, plus a **flat block map**
|
|
133
|
+
(`id → block`) with explicit order via
|
|
132
134
|
`children: [blockId]`; nesting (lists, table cells) is `children`. The text
|
|
133
135
|
of a text block is `content: [{ text, marks }]` runs. JSON ↔ Yjs is
|
|
134
136
|
isomorphic — every JSON field has exactly one Yjs image.
|
package/dist/index.d.mts
CHANGED
|
@@ -757,7 +757,7 @@ interface ValidationError {
|
|
|
757
757
|
//#region src/schema/validate/utils/common.d.ts
|
|
758
758
|
declare const isHexColor: (value: unknown) => boolean;
|
|
759
759
|
/** The public link-href contract (the validator's and the consumer
|
|
760
|
-
* renderer's gate): the
|
|
760
|
+
* renderer's gate): the normalized form is an http/https URL or a
|
|
761
761
|
* scheme-less (relative) path; any other scheme is rejected. */
|
|
762
762
|
declare const isSafeLinkHref: (value: string) => boolean;
|
|
763
763
|
/**
|
|
@@ -1132,6 +1132,9 @@ declare class Doc {
|
|
|
1132
1132
|
* propagates. A failed batch still emits a transient `onChange` emission
|
|
1133
1133
|
* and a partial wire update before the rollback (CRDT property: the
|
|
1134
1134
|
* rollback is best-effort on the wire; observable state converges).
|
|
1135
|
+
* A subscription handler's throw (the handlers fire inside the
|
|
1136
|
+
* transaction) is isolated — it stands the logged error and never rolls
|
|
1137
|
+
* back a committed batch.
|
|
1135
1138
|
*/
|
|
1136
1139
|
transact(fn: () => void): void;
|
|
1137
1140
|
/** Undo the last local edit; a safe no-op when there is nothing to undo. */
|
package/dist/index.mjs
CHANGED
|
@@ -875,6 +875,20 @@ const MARK_REGISTRY = {
|
|
|
875
875
|
attrOptional: true
|
|
876
876
|
}
|
|
877
877
|
};
|
|
878
|
+
/**
|
|
879
|
+
* The registry's membership checks: the `in` operator is prototype-aware,
|
|
880
|
+
* so an Object.prototype-reserved key (`constructor`, `toString`, …) would
|
|
881
|
+
* pass it as a registered entry (the inherited function, not a spec).
|
|
882
|
+
* The data-key stand is the own-property lookup: the reserved key stands
|
|
883
|
+
* unknown (the lenient reader's pass-through / the strict writer's
|
|
884
|
+
* rejection), never a spec.
|
|
885
|
+
*/
|
|
886
|
+
function isRegisteredBlockType(type) {
|
|
887
|
+
return typeof type === "string" && Object.hasOwn(BLOCK_REGISTRY, type);
|
|
888
|
+
}
|
|
889
|
+
function isRegisteredMarkName(name) {
|
|
890
|
+
return typeof name === "string" && Object.hasOwn(MARK_REGISTRY, name);
|
|
891
|
+
}
|
|
878
892
|
//#endregion
|
|
879
893
|
//#region src/schema/utils.ts
|
|
880
894
|
/** The schema's shared helpers: the plain-object check + the block's
|
|
@@ -899,7 +913,7 @@ function indentOf(block) {
|
|
|
899
913
|
function legacyChildrenOf(block) {
|
|
900
914
|
if (!Array.isArray(block.children)) return null;
|
|
901
915
|
const type = block.type;
|
|
902
|
-
if (
|
|
916
|
+
if (!isRegisteredBlockType(type)) return null;
|
|
903
917
|
return BLOCK_REGISTRY[type].legacyChildrenTypes !== void 0 ? block.children : null;
|
|
904
918
|
}
|
|
905
919
|
/**
|
|
@@ -956,7 +970,7 @@ function projectBlock(block, id, input, state) {
|
|
|
956
970
|
const ordered = block.children;
|
|
957
971
|
if (!Array.isArray(ordered)) return withIndent(block, id, state);
|
|
958
972
|
const type = block.type;
|
|
959
|
-
const spec =
|
|
973
|
+
const spec = isRegisteredBlockType(type) ? BLOCK_REGISTRY[type] : void 0;
|
|
960
974
|
if (spec === void 0) return withIndent(block, id, state);
|
|
961
975
|
if (!spec.children) {
|
|
962
976
|
const withoutChildren = { ...block };
|
|
@@ -1131,16 +1145,19 @@ const show = (value) => {
|
|
|
1131
1145
|
const isIso8601 = (value) => typeof value === "string" && ISO_8601.test(value) && !Number.isNaN(Date.parse(value));
|
|
1132
1146
|
const isHexColor = (value) => typeof value === "string" && HEX_COLOR.test(value);
|
|
1133
1147
|
/** `http://` / `https://` URLs and relative paths; other schemes are
|
|
1134
|
-
* rejected. The check runs over the
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1148
|
+
* rejected. The check runs over the browser's normalization of the input:
|
|
1149
|
+
* the WHATWG URL parser removes ASCII tab / LF / CR (U+0009–U+000D) from
|
|
1150
|
+
* ANY position and trims the leading / trailing C0 controls and spaces
|
|
1151
|
+
* (U+0000–U+0020) before the scheme detection — so an unnormalized check
|
|
1152
|
+
* stands the `"java\tscript:…"` bypass (the raw string carries no scheme
|
|
1153
|
+
* per the `^[a-z][a-z0-9+.-]*:` test, the browser's does). */
|
|
1138
1154
|
const CONTROL_TRIM = /^[\u0000-\u0020]+|[\u0000-\u0020]+$/g;
|
|
1155
|
+
const MID_STRING_CONTROLS = /[\u0009\u000A\u000D]/g;
|
|
1139
1156
|
/** The public link-href contract (the validator's and the consumer
|
|
1140
|
-
* renderer's gate): the
|
|
1157
|
+
* renderer's gate): the normalized form is an http/https URL or a
|
|
1141
1158
|
* scheme-less (relative) path; any other scheme is rejected. */
|
|
1142
1159
|
const isSafeLinkHref = (value) => {
|
|
1143
|
-
const trimmed = value.replace(CONTROL_TRIM, "");
|
|
1160
|
+
const trimmed = value.replace(MID_STRING_CONTROLS, "").replace(CONTROL_TRIM, "");
|
|
1144
1161
|
return trimmed.length > 0 && (WEB_URL.test(trimmed) || !SCHEME_PREFIX.test(trimmed));
|
|
1145
1162
|
};
|
|
1146
1163
|
const isLinkHref = (value) => isSafeLinkHref(value);
|
|
@@ -1733,7 +1750,7 @@ function validateMarkFields(name, fields, value, markPath, errors, lenient = fal
|
|
|
1733
1750
|
message: `Mark "${name}" field "${key}" must be a non-empty string.`
|
|
1734
1751
|
});
|
|
1735
1752
|
}
|
|
1736
|
-
for (const key of Object.keys(value)) if (!(key
|
|
1753
|
+
for (const key of Object.keys(value)) if (!Object.hasOwn(fields, key)) {
|
|
1737
1754
|
if (!lenient) errors.push({
|
|
1738
1755
|
path: `${markPath}.${key}`,
|
|
1739
1756
|
message: `Unknown attribute "${key}" for mark "${name}".`
|
|
@@ -1757,20 +1774,20 @@ function validateMarks(blockType, marks, path, errors, lenient = false) {
|
|
|
1757
1774
|
}
|
|
1758
1775
|
const names = Object.keys(marks);
|
|
1759
1776
|
if (blockType === "code" && names.length > 0) {
|
|
1760
|
-
if (!lenient || names.some((name) => name
|
|
1777
|
+
if (!lenient || names.some((name) => isRegisteredMarkName(name))) errors.push({
|
|
1761
1778
|
path,
|
|
1762
1779
|
message: "Runs in a code block must not carry marks."
|
|
1763
1780
|
});
|
|
1764
1781
|
}
|
|
1765
1782
|
for (const name of names) {
|
|
1766
|
-
|
|
1767
|
-
if (!spec) {
|
|
1783
|
+
if (!isRegisteredMarkName(name)) {
|
|
1768
1784
|
if (!lenient) errors.push({
|
|
1769
1785
|
path: `${path}.${name}`,
|
|
1770
1786
|
message: `Unknown mark "${name}".`
|
|
1771
1787
|
});
|
|
1772
1788
|
continue;
|
|
1773
1789
|
}
|
|
1790
|
+
const spec = MARK_REGISTRY[name];
|
|
1774
1791
|
const markPath = `${path}.${name}`;
|
|
1775
1792
|
const value = marks[name];
|
|
1776
1793
|
if (spec.kind === "toggle") {
|
|
@@ -1819,7 +1836,7 @@ function validateMarks(blockType, marks, path, errors, lenient = false) {
|
|
|
1819
1836
|
});
|
|
1820
1837
|
}
|
|
1821
1838
|
}
|
|
1822
|
-
const exclusive = names.find((name) => MARK_REGISTRY[name]
|
|
1839
|
+
const exclusive = names.find((name) => isRegisteredMarkName(name) && MARK_REGISTRY[name].exclusive);
|
|
1823
1840
|
if (exclusive && names.length > 1) errors.push({
|
|
1824
1841
|
path,
|
|
1825
1842
|
message: `A run with exclusive mark "${exclusive}" must not carry other marks.`
|
|
@@ -2439,13 +2456,14 @@ function markSetToAttrs(marks) {
|
|
|
2439
2456
|
* A Y.Text attribute object as a `MarkSet` (mirrors the snapshot reader:
|
|
2440
2457
|
* `true` toggles kept, object values reduced to their string fields). The
|
|
2441
2458
|
* cross-minor stand: an unknown mark name is preserved (its fields pass
|
|
2442
|
-
* through as-is, not only the string ones)
|
|
2443
|
-
*
|
|
2444
|
-
*
|
|
2445
|
-
*
|
|
2446
|
-
*
|
|
2447
|
-
*
|
|
2448
|
-
*
|
|
2459
|
+
* through as-is, not only the string ones), and a non-boolean primitive
|
|
2460
|
+
* value (a future minor's additive value shape) is kept as-is — the data
|
|
2461
|
+
* is not lost on save, the reader/renderer stand the text without it, the
|
|
2462
|
+
* strict validator rejects it. A known mark's unknown fields stand the
|
|
2463
|
+
* same pass-through (a future minor's additive field on a known mark, any
|
|
2464
|
+
* value shape); a known field keeps the string stand (a non-string value
|
|
2465
|
+
* on a known field is corrupt data — a future minor cannot change a known
|
|
2466
|
+
* field's contract — and is dropped protectively).
|
|
2449
2467
|
*/
|
|
2450
2468
|
function attrsToMarkSet(attributes) {
|
|
2451
2469
|
const marks = {};
|
|
@@ -2461,7 +2479,7 @@ function attrsToMarkSet(attributes) {
|
|
|
2461
2479
|
setOwn(attrs, key, attrValue);
|
|
2462
2480
|
}
|
|
2463
2481
|
setOwn(marks, name, attrs);
|
|
2464
|
-
}
|
|
2482
|
+
} else setOwn(marks, name, value);
|
|
2465
2483
|
return marks;
|
|
2466
2484
|
}
|
|
2467
2485
|
/**
|
|
@@ -2491,6 +2509,20 @@ function exclusiveFreeSpans(content, from, to) {
|
|
|
2491
2509
|
* registry's fields), the parents' write, the full-document write, and the
|
|
2492
2510
|
* opaque block write (the cross-minor stand). */
|
|
2493
2511
|
const isPlainObject$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2512
|
+
/** The layout's own top-level keys (the spec-gated writers + the CRDT
|
|
2513
|
+
* owner field): everything else on a block entry is additive data of a
|
|
2514
|
+
* future minor and stands the generic as-is write (the cross-minor stand). */
|
|
2515
|
+
const LAYOUT_BLOCK_KEYS = /* @__PURE__ */ new Set([
|
|
2516
|
+
"type",
|
|
2517
|
+
"attrs",
|
|
2518
|
+
"geometry",
|
|
2519
|
+
"content",
|
|
2520
|
+
"children",
|
|
2521
|
+
"rows",
|
|
2522
|
+
"rowOrder",
|
|
2523
|
+
"columnOrder",
|
|
2524
|
+
"parent"
|
|
2525
|
+
]);
|
|
2494
2526
|
/** Primitive attr values are stored directly; plain objects become nested Y.Maps, and arrays become Y.Arrays (array elements are stored as-is — the schema keeps them plain). */
|
|
2495
2527
|
function writeAttrValue(target, key, value) {
|
|
2496
2528
|
if (value === void 0) return;
|
|
@@ -2584,6 +2616,10 @@ function writeBlock(blocks, id, block) {
|
|
|
2584
2616
|
}
|
|
2585
2617
|
}
|
|
2586
2618
|
}
|
|
2619
|
+
for (const [key, value] of Object.entries(block)) {
|
|
2620
|
+
if (LAYOUT_BLOCK_KEYS.has(key)) continue;
|
|
2621
|
+
writeAttrValue(yblock, key, value);
|
|
2622
|
+
}
|
|
2587
2623
|
blocks.set(id, yblock);
|
|
2588
2624
|
}
|
|
2589
2625
|
/**
|
|
@@ -2617,7 +2653,7 @@ function writeDocument(ydoc, json) {
|
|
|
2617
2653
|
for (const [id, block] of Object.entries(json.blocks)) {
|
|
2618
2654
|
const writable = block;
|
|
2619
2655
|
if (writable.type === "root") for (const childId of writable.children ?? []) rootOrder.push([childId]);
|
|
2620
|
-
if (writable.type
|
|
2656
|
+
if (isRegisteredBlockType(writable.type)) writeBlock(blocks, id, writable);
|
|
2621
2657
|
else writeOpaqueBlock(blocks, id, block);
|
|
2622
2658
|
}
|
|
2623
2659
|
setParents(json, blocks);
|
|
@@ -2736,7 +2772,7 @@ function normalizeRuns(runs) {
|
|
|
2736
2772
|
/** The read unit's value helpers: the known-type check, the order dedup, and
|
|
2737
2773
|
* the plain-value read. */
|
|
2738
2774
|
function isKnownBlockType(value) {
|
|
2739
|
-
return
|
|
2775
|
+
return isRegisteredBlockType(value);
|
|
2740
2776
|
}
|
|
2741
2777
|
/**
|
|
2742
2778
|
* Read-side dedupe of an order array: duplicate entries can only arise from
|
|
@@ -2859,7 +2895,7 @@ function readOpaqueBlock(yblock, type) {
|
|
|
2859
2895
|
attrs.forEach((value, key) => entries.push([key, readPlainValue(value)]));
|
|
2860
2896
|
entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2861
2897
|
const plain = {};
|
|
2862
|
-
for (const [key, value] of entries) plain
|
|
2898
|
+
for (const [key, value] of entries) setOwn(plain, key, value);
|
|
2863
2899
|
block.attrs = plain;
|
|
2864
2900
|
}
|
|
2865
2901
|
const geometry = yblock.get("geometry");
|
|
@@ -2964,7 +3000,7 @@ function readBlock(ydoc, id) {
|
|
|
2964
3000
|
const rows = {};
|
|
2965
3001
|
const rowsMap = yblock.get("rows");
|
|
2966
3002
|
if (rowsMap instanceof Y.Map) rowsMap.forEach((row, rowId) => {
|
|
2967
|
-
rows
|
|
3003
|
+
setOwn(rows, rowId, row instanceof Y.Map ? readPlainValue(row) : {});
|
|
2968
3004
|
});
|
|
2969
3005
|
const rowOrder = yblock.get("rowOrder");
|
|
2970
3006
|
const hasCells = (rowId) => rowsMap instanceof Y.Map && rowsMap.has(rowId);
|
|
@@ -2985,6 +3021,27 @@ function readBlock(ydoc, id) {
|
|
|
2985
3021
|
columnWidths: derived
|
|
2986
3022
|
};
|
|
2987
3023
|
}
|
|
3024
|
+
const extra = [];
|
|
3025
|
+
yblock.forEach((value, key) => {
|
|
3026
|
+
if (typeof key === "string" && !LAYOUT_BLOCK_KEYS.has(key)) extra.push([key, value]);
|
|
3027
|
+
});
|
|
3028
|
+
extra.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
3029
|
+
for (const [key, value] of extra) {
|
|
3030
|
+
if (value instanceof Y.Text) {
|
|
3031
|
+
const raw = [];
|
|
3032
|
+
for (const op of value.toDelta()) {
|
|
3033
|
+
if (typeof op.insert !== "string") continue;
|
|
3034
|
+
raw.push({
|
|
3035
|
+
text: op.insert,
|
|
3036
|
+
marks: attrsToMarkSet(op.attributes)
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
const runs = normalizeRuns(raw);
|
|
3040
|
+
if (runs.length > 0) setOwn(block, key, runs);
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
setOwn(block, key, readPlainValue(value));
|
|
3044
|
+
}
|
|
2988
3045
|
return block;
|
|
2989
3046
|
}
|
|
2990
3047
|
/**
|
|
@@ -3005,10 +3062,11 @@ function readBlockSnapshot(ydoc, id) {
|
|
|
3005
3062
|
function toDocJSON(ydoc) {
|
|
3006
3063
|
const meta = readMeta(ydoc);
|
|
3007
3064
|
const blocks = {};
|
|
3008
|
-
ydoc.getMap("blocks").
|
|
3065
|
+
const ids = [...ydoc.getMap("blocks").keys()].sort();
|
|
3066
|
+
for (const id of ids) {
|
|
3009
3067
|
const block = readBlockSnapshot(ydoc, id);
|
|
3010
|
-
if (block) blocks
|
|
3011
|
-
}
|
|
3068
|
+
if (block) setOwn(blocks, id, block);
|
|
3069
|
+
}
|
|
3012
3070
|
return {
|
|
3013
3071
|
...meta,
|
|
3014
3072
|
blocks
|
|
@@ -3037,7 +3095,7 @@ function blockTypeOf(ydoc, id) {
|
|
|
3037
3095
|
const block = getBlockMap(ydoc, id);
|
|
3038
3096
|
if (!block) return null;
|
|
3039
3097
|
const type = block.get("type");
|
|
3040
|
-
return
|
|
3098
|
+
return isRegisteredBlockType(type) ? type : null;
|
|
3041
3099
|
}
|
|
3042
3100
|
/**
|
|
3043
3101
|
* The block's raw children array; null when the block has none. The root
|
|
@@ -3100,7 +3158,7 @@ function findParentInArrays(ydoc, id) {
|
|
|
3100
3158
|
blocks.forEach((block, blockId) => {
|
|
3101
3159
|
if (parent !== null || !(block instanceof Y.Map)) return;
|
|
3102
3160
|
const type = block.get("type");
|
|
3103
|
-
if (
|
|
3161
|
+
if (!isRegisteredBlockType(type) || !BLOCK_REGISTRY[type].children) return;
|
|
3104
3162
|
const children = block.get("children");
|
|
3105
3163
|
if (children instanceof Y.Array && children.toArray().includes(id)) parent = blockId;
|
|
3106
3164
|
});
|
|
@@ -3511,7 +3569,7 @@ function changeBlockType(ydoc, generateId, id, type, attrs) {
|
|
|
3511
3569
|
const currentType = blockTypeOf(ydoc, id);
|
|
3512
3570
|
if (currentType === null) throw new UnknownBlockError(`Unknown block "${id}".`);
|
|
3513
3571
|
if (currentType === "root") throw new InvalidOperationError("The root block's type cannot be changed.");
|
|
3514
|
-
if (!(type
|
|
3572
|
+
if (!isRegisteredBlockType(type)) throw new UnknownBlockTypeError(`Unknown block type "${String(type)}".`);
|
|
3515
3573
|
if (type === currentType) throw new InvalidOperationError(`Block "${id}" is already of type "${type}".`);
|
|
3516
3574
|
const from = BLOCK_REGISTRY[currentType];
|
|
3517
3575
|
const to = BLOCK_REGISTRY[type];
|
|
@@ -3538,8 +3596,8 @@ function changeBlockType(ydoc, generateId, id, type, attrs) {
|
|
|
3538
3596
|
const snapshot = readBlock(ydoc, id);
|
|
3539
3597
|
const currentAttrs = snapshot !== null && "attrs" in snapshot ? snapshot.attrs : void 0;
|
|
3540
3598
|
const merged = {};
|
|
3541
|
-
for (const [key, value] of Object.entries(currentAttrs ?? {})) if (to.attrs !== void 0 &&
|
|
3542
|
-
for (const [key, value] of Object.entries(isPlainObject$1(attrs) ? attrs : {})) if (value !== void 0) merged
|
|
3599
|
+
for (const [key, value] of Object.entries(currentAttrs ?? {})) if (to.attrs !== void 0 && Object.hasOwn(to.attrs, key)) setOwn(merged, key, value);
|
|
3600
|
+
for (const [key, value] of Object.entries(isPlainObject$1(attrs) ? attrs : {})) if (value !== void 0) setOwn(merged, key, value);
|
|
3543
3601
|
if (to.attrs !== void 0) {
|
|
3544
3602
|
const errors = validateBlockAttrs(type, merged);
|
|
3545
3603
|
if (errors.length > 0) throw new InvalidAttrsError(`Invalid attrs for type "${type}" on block "${id}": ${formatValidationErrors(errors)}`);
|
|
@@ -3948,7 +4006,7 @@ function writeNewBlock(blocks, id, type, attrs) {
|
|
|
3948
4006
|
}
|
|
3949
4007
|
/** Inserts a new block under `at.parent` (default: root) after `at.after` (default: appended). */
|
|
3950
4008
|
function insertBlock(ydoc, generateId, type, attrs, at) {
|
|
3951
|
-
if (!(type
|
|
4009
|
+
if (!isRegisteredBlockType(type)) throw new UnknownBlockTypeError(`Unknown block type "${String(type)}".`);
|
|
3952
4010
|
if (type === "root") throw new InvalidOperationError("The root block already exists and cannot be inserted.");
|
|
3953
4011
|
const explicitId = at?.id;
|
|
3954
4012
|
if (explicitId !== void 0) {
|
|
@@ -4406,7 +4464,7 @@ function setMark(ydoc, blockId, from, to, mark) {
|
|
|
4406
4464
|
}
|
|
4407
4465
|
/** Removes `name` from the marks of the characters in [from, to); other marks stay. */
|
|
4408
4466
|
function unsetMark(ydoc, blockId, from, to, name) {
|
|
4409
|
-
if (!(name
|
|
4467
|
+
if (!isRegisteredMarkName(name)) throw new InvalidMarkError(`Unknown mark "${String(name)}".`);
|
|
4410
4468
|
const { content } = requireTextBlock(ydoc, blockId);
|
|
4411
4469
|
const total = contentTextLength(content);
|
|
4412
4470
|
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || from > to || to > total) throw new InvalidRangeError(`Range must satisfy 0 <= from < to <= ${total} (got from=${from}, to=${to}).`);
|
|
@@ -4876,20 +4934,35 @@ function emitPending(ydoc, hub, isRemote) {
|
|
|
4876
4934
|
metaChanged: pending.metaChanged
|
|
4877
4935
|
}, isRemote);
|
|
4878
4936
|
}
|
|
4937
|
+
/**
|
|
4938
|
+
* Run the handler with its throw isolated: a listener fires inside the
|
|
4939
|
+
* Yjs transaction (the afterTransaction hook), so a listener's throw must
|
|
4940
|
+
* not abort the remaining handlers of the emission nor roll back the
|
|
4941
|
+
* caller's committed batch (the `transact`'s rollback guard would catch
|
|
4942
|
+
* it otherwise). The throw stands the logged error; the emission
|
|
4943
|
+
* continues with the next handler.
|
|
4944
|
+
*/
|
|
4945
|
+
function safeCall(label, handler) {
|
|
4946
|
+
try {
|
|
4947
|
+
handler();
|
|
4948
|
+
} catch (error) {
|
|
4949
|
+
console.error(`docblocks: a ${label} handler threw — the emission continues`, error);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4879
4952
|
function emitChange(ydoc, change, isRemote) {
|
|
4880
4953
|
const hub = getHub(ydoc);
|
|
4881
|
-
for (const handler of [...hub.changeHandlers]) handler(change);
|
|
4882
|
-
if (isRemote) for (const handler of [...hub.remoteChangeHandlers]) handler(change);
|
|
4954
|
+
for (const handler of [...hub.changeHandlers]) safeCall("change", () => handler(change));
|
|
4955
|
+
if (isRemote) for (const handler of [...hub.remoteChangeHandlers]) safeCall("remote-change", () => handler(change));
|
|
4883
4956
|
for (const blockId of change.blockIds) {
|
|
4884
4957
|
const blockHandlers = hub.blockHandlers.get(blockId);
|
|
4885
4958
|
if (blockHandlers !== void 0 && blockHandlers.size > 0) {
|
|
4886
4959
|
const block = readBlock(ydoc, blockId);
|
|
4887
|
-
if (block !== null) for (const handler of [...blockHandlers]) handler(block);
|
|
4960
|
+
if (block !== null) for (const handler of [...blockHandlers]) safeCall("block", () => handler(block));
|
|
4888
4961
|
}
|
|
4889
4962
|
const textHandlers = hub.textHandlers.get(blockId);
|
|
4890
4963
|
if (textHandlers !== void 0 && textHandlers.size > 0) {
|
|
4891
4964
|
const text = readText(ydoc, blockId);
|
|
4892
|
-
for (const handler of [...textHandlers]) handler(text);
|
|
4965
|
+
for (const handler of [...textHandlers]) safeCall("text", () => handler(text));
|
|
4893
4966
|
}
|
|
4894
4967
|
}
|
|
4895
4968
|
}
|
|
@@ -5949,6 +6022,9 @@ var Doc = class Doc {
|
|
|
5949
6022
|
* propagates. A failed batch still emits a transient `onChange` emission
|
|
5950
6023
|
* and a partial wire update before the rollback (CRDT property: the
|
|
5951
6024
|
* rollback is best-effort on the wire; observable state converges).
|
|
6025
|
+
* A subscription handler's throw (the handlers fire inside the
|
|
6026
|
+
* transaction) is isolated — it stands the logged error and never rolls
|
|
6027
|
+
* back a committed batch.
|
|
5952
6028
|
*/
|
|
5953
6029
|
transact(fn) {
|
|
5954
6030
|
const ydoc = this.#ydoc;
|
package/package.json
CHANGED