rmapi-js 12.0.2 → 13.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/dist/rm6.js CHANGED
@@ -5,8 +5,9 @@
5
5
  * of length-prefixed, tagged blocks. This module reads every block faithfully
6
6
  * — preserving `CrdtId`s, `LwwValue` wrappers, and each block's unread tail —
7
7
  * into an {@link RmScene | `RmScene`}, whose methods resolve the CRDT into
8
- * ordered layers, strokes, and text. Because nothing is dropped, the blocks
9
- * round-trip back to bytes.
8
+ * ordered layers, strokes, and text. Nothing is dropped, so
9
+ * {@link serializeRmScene | `serializeRmScene`} reproduces the original bytes
10
+ * exactly, including blocks it could not parse.
10
11
  *
11
12
  * @packageDocumentation
12
13
  */
@@ -146,7 +147,11 @@ class Reader {
146
147
  const subEnd = this.#offset + length;
147
148
  this.#bounds.push(subEnd);
148
149
  try {
149
- return fn();
150
+ const value = fn();
151
+ if (this.#offset !== subEnd) {
152
+ throw new Error(`subblock ${index} left ${subEnd - this.#offset} bytes unread`);
153
+ }
154
+ return value;
150
155
  }
151
156
  finally {
152
157
  this.#bounds.pop();
@@ -201,6 +206,125 @@ class Reader {
201
206
  });
202
207
  }
203
208
  }
209
+ /** the tagged-write counterpart to {@link Reader | `Reader`} */
210
+ class Writer {
211
+ #buffer = new Uint8Array(4096);
212
+ #view = new DataView(this.#buffer.buffer);
213
+ #length = 0;
214
+ #ensure(extra) {
215
+ if (this.#length + extra <= this.#buffer.length)
216
+ return;
217
+ let size = this.#buffer.length;
218
+ while (size < this.#length + extra)
219
+ size *= 2;
220
+ const grown = new Uint8Array(size);
221
+ grown.set(this.#buffer);
222
+ this.#buffer = grown;
223
+ this.#view = new DataView(grown.buffer);
224
+ }
225
+ get length() {
226
+ return this.#length;
227
+ }
228
+ u8(value) {
229
+ this.#ensure(1);
230
+ this.#view.setUint8(this.#length, value);
231
+ this.#length += 1;
232
+ }
233
+ u16(value) {
234
+ this.#ensure(2);
235
+ this.#view.setUint16(this.#length, value, true);
236
+ this.#length += 2;
237
+ }
238
+ u32(value) {
239
+ this.#ensure(4);
240
+ this.#view.setUint32(this.#length, value, true);
241
+ this.#length += 4;
242
+ }
243
+ f32(value) {
244
+ this.#ensure(4);
245
+ this.#view.setFloat32(this.#length, value, true);
246
+ this.#length += 4;
247
+ }
248
+ f64(value) {
249
+ this.#ensure(8);
250
+ this.#view.setFloat64(this.#length, value, true);
251
+ this.#length += 8;
252
+ }
253
+ varuint(value) {
254
+ let rest = value;
255
+ do {
256
+ let byte = rest % 128;
257
+ rest = Math.floor(rest / 128);
258
+ if (rest > 0)
259
+ byte |= 0x80;
260
+ this.u8(byte);
261
+ } while (rest > 0);
262
+ }
263
+ crdtId(id) {
264
+ this.u8(id.authorId);
265
+ this.varuint(id.counter);
266
+ }
267
+ bytes(data) {
268
+ this.#ensure(data.length);
269
+ this.#buffer.set(data, this.#length);
270
+ this.#length += data.length;
271
+ }
272
+ patchU32(at, value) {
273
+ this.#view.setUint32(at, value, true);
274
+ }
275
+ finish() {
276
+ return this.#buffer.slice(0, this.#length);
277
+ }
278
+ tag(index, type) {
279
+ this.varuint(index * 16 + type);
280
+ }
281
+ writeInt(index, value) {
282
+ this.tag(index, TAG_BYTE4);
283
+ this.u32(value);
284
+ }
285
+ writeFloat(index, value) {
286
+ this.tag(index, TAG_BYTE4);
287
+ this.f32(value);
288
+ }
289
+ writeDouble(index, value) {
290
+ this.tag(index, TAG_BYTE8);
291
+ this.f64(value);
292
+ }
293
+ writeId(index, id) {
294
+ this.tag(index, TAG_ID);
295
+ this.crdtId(id);
296
+ }
297
+ writeBool(index, value) {
298
+ this.tag(index, TAG_BYTE1);
299
+ this.u8(value ? 1 : 0);
300
+ }
301
+ writeByte(index, value) {
302
+ this.tag(index, TAG_BYTE1);
303
+ this.u8(value);
304
+ }
305
+ subblock(index, write) {
306
+ this.tag(index, TAG_LENGTH4);
307
+ const at = this.length;
308
+ this.u32(0);
309
+ write();
310
+ this.patchU32(at, this.length - at - 4);
311
+ }
312
+ writeLww(index, lww, write) {
313
+ this.subblock(index, () => {
314
+ this.writeId(1, lww.timestamp);
315
+ write(lww.value);
316
+ });
317
+ }
318
+ writeString(index, text) {
319
+ this.subblock(index, () => {
320
+ const encoded = new TextEncoder().encode(text);
321
+ this.varuint(encoded.length);
322
+ // the device writes this flag as 1 even for non-ascii text
323
+ this.u8(1);
324
+ this.bytes(encoded);
325
+ });
326
+ }
327
+ }
204
328
  /** read the `SceneItem` envelope shared by item blocks */
205
329
  function readItemEnvelope(reader, readValue) {
206
330
  const parentId = reader.readId(1);
@@ -236,7 +360,7 @@ function readLineValue(reader, version) {
236
360
  y,
237
361
  speed: reader.f32() * 4,
238
362
  direction: (reader.f32() * 255) / (2 * Math.PI),
239
- width: Math.round(reader.f32() * 4),
363
+ width: reader.f32() * 4,
240
364
  pressure: reader.f32() * 255,
241
365
  };
242
366
  }
@@ -257,12 +381,10 @@ function readLineValue(reader, version) {
257
381
  startingLength,
258
382
  points,
259
383
  };
260
- // optional trailing timestamp / move id / color are skipped by seeking to the
261
- // subblock end; only the highlighter rgba color is captured
262
384
  if (reader.hasTag(6, TAG_ID))
263
- reader.readId(6);
385
+ line.timestampId = reader.readId(6);
264
386
  if (reader.hasTag(7, TAG_ID))
265
- reader.readId(7);
387
+ line.moveId = reader.readId(7);
266
388
  if (reader.hasTag(8, TAG_BYTE4))
267
389
  line.colorRgba = reader.readInt(8);
268
390
  return line;
@@ -367,10 +489,12 @@ function readBlockBody(reader, blockType, version) {
367
489
  visible,
368
490
  };
369
491
  if (reader.bytesRemaining() > 0 && reader.hasTag(7, TAG_LENGTH4)) {
370
- node.anchorId = reader.readLww(7, () => reader.readId(2));
371
- node.anchorType = reader.readLww(8, () => reader.readByte(2));
372
- node.anchorThreshold = reader.readLww(9, () => reader.readFloat(2));
373
- node.anchorOriginX = reader.readLww(10, () => reader.readFloat(2));
492
+ node.anchor = {
493
+ id: reader.readLww(7, () => reader.readId(2)),
494
+ type: reader.readLww(8, () => reader.readByte(2)),
495
+ threshold: reader.readLww(9, () => reader.readFloat(2)),
496
+ originX: reader.readLww(10, () => reader.readFloat(2)),
497
+ };
374
498
  }
375
499
  return node;
376
500
  }
@@ -416,15 +540,19 @@ function readBlockBody(reader, blockType, version) {
416
540
  }
417
541
  return { type: "authorIds", authors };
418
542
  }
419
- case 0x0a:
420
- return {
543
+ case 0x0a: {
544
+ const info = {
421
545
  type: "pageInfo",
422
546
  loadsCount: reader.readInt(1),
423
547
  mergesCount: reader.readInt(2),
424
548
  textCharsCount: reader.readInt(3),
425
549
  textLinesCount: reader.readInt(4),
426
- typeFolioUseCount: reader.hasTag(5, TAG_BYTE4) ? reader.readInt(5) : 0,
427
550
  };
551
+ if (reader.hasTag(5, TAG_BYTE4)) {
552
+ info.typeFolioUseCount = reader.readInt(5);
553
+ }
554
+ return info;
555
+ }
428
556
  case 0x0d: {
429
557
  const info = {
430
558
  type: "sceneInfo",
@@ -445,29 +573,22 @@ function readBlockBody(reader, blockType, version) {
445
573
  throw new Error(`unknown v6 block type 0x${blockType.toString(16)}`);
446
574
  }
447
575
  }
576
+ /**
577
+ * the byte order of a little-endian (bytes_le) uuid
578
+ *
579
+ * The permutation is its own inverse, so one table serves both directions.
580
+ */
581
+ const UUID_LE_ORDER = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15];
582
+ function swapUuidBytes(bytes) {
583
+ const out = new Uint8Array(16);
584
+ for (let index = 0; index < 16; index++) {
585
+ out[index] = bytes[UUID_LE_ORDER[index]] ?? 0;
586
+ }
587
+ return out;
588
+ }
448
589
  function uuidToString(bytes) {
449
- // the uuid is stored little-endian (bytes_le); reverse the standard fields
450
- const b = [...bytes];
451
- const le = [
452
- b[3],
453
- b[2],
454
- b[1],
455
- b[0],
456
- b[5],
457
- b[4],
458
- b[7],
459
- b[6],
460
- b[8],
461
- b[9],
462
- b[10],
463
- b[11],
464
- b[12],
465
- b[13],
466
- b[14],
467
- b[15],
468
- ];
469
- const hex = le.map((byte) => (byte ?? 0).toString(16).padStart(2, "0"));
470
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
590
+ const hex = swapUuidBytes(bytes).toHex();
591
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
471
592
  }
472
593
  /** parse the raw block list of a version 6 `.rm` file */
473
594
  function parseV6Blocks(data) {
@@ -481,16 +602,36 @@ function parseV6Blocks(data) {
481
602
  if (reader.bytesRemaining() < 8)
482
603
  break;
483
604
  const length = reader.u32();
484
- reader.u8(); // unknown, always 0
605
+ const reserved = reader.u8();
485
606
  const minVersion = reader.u8();
486
607
  const currentVersion = reader.u8();
487
608
  const blockType = reader.u8();
488
609
  const blockStart = reader.offset;
610
+ if (length > reader.bytesRemaining()) {
611
+ // the block overruns the file; keep what's left verbatim and stop
612
+ blocks.push({
613
+ type: "unknown",
614
+ blockType,
615
+ data: reader.bytes(reader.bytesRemaining()),
616
+ declaredLength: length,
617
+ reserved,
618
+ minVersion,
619
+ currentVersion,
620
+ extraData: new Uint8Array(),
621
+ });
622
+ break;
623
+ }
489
624
  const blockEnd = blockStart + length;
490
625
  let block;
491
626
  try {
492
627
  const [body, extraData] = reader.bounded(blockEnd, () => readBlockBody(reader, blockType, currentVersion));
493
- block = { ...body, minVersion, currentVersion, extraData };
628
+ block = {
629
+ ...body,
630
+ reserved,
631
+ minVersion,
632
+ currentVersion,
633
+ extraData,
634
+ };
494
635
  }
495
636
  catch {
496
637
  // couldn't parse this block; keep its raw bytes so the file still
@@ -500,6 +641,7 @@ function parseV6Blocks(data) {
500
641
  type: "unknown",
501
642
  blockType,
502
643
  data: reader.bytes(length),
644
+ reserved,
503
645
  minVersion,
504
646
  currentVersion,
505
647
  extraData: new Uint8Array(),
@@ -722,6 +864,243 @@ export class RmScene {
722
864
  return this.#text;
723
865
  }
724
866
  }
867
+ function writeItemEnvelope(writer, parentId, item, itemType, writeValue) {
868
+ writer.writeId(1, parentId);
869
+ writer.writeId(2, item.itemId);
870
+ writer.writeId(3, item.leftId);
871
+ writer.writeId(4, item.rightId);
872
+ writer.writeInt(5, item.deletedLength);
873
+ const { value } = item;
874
+ if (value !== undefined) {
875
+ writer.subblock(6, () => {
876
+ writer.u8(itemType);
877
+ writeValue(value);
878
+ });
879
+ }
880
+ }
881
+ function writeLineValue(writer, line, version) {
882
+ writer.writeInt(1, line.tool);
883
+ writer.writeInt(2, line.color);
884
+ writer.writeDouble(3, line.thicknessScale);
885
+ writer.writeFloat(4, line.startingLength);
886
+ writer.subblock(5, () => {
887
+ for (const point of line.points) {
888
+ writer.f32(point.x);
889
+ writer.f32(point.y);
890
+ if (version === 1) {
891
+ writer.f32(point.speed / 4);
892
+ writer.f32((point.direction * 2 * Math.PI) / 255);
893
+ writer.f32(point.width / 4);
894
+ writer.f32(point.pressure / 255);
895
+ }
896
+ else {
897
+ writer.u16(point.speed);
898
+ writer.u16(point.width);
899
+ writer.u8(point.direction);
900
+ writer.u8(point.pressure);
901
+ }
902
+ }
903
+ });
904
+ if (line.timestampId !== undefined)
905
+ writer.writeId(6, line.timestampId);
906
+ if (line.moveId !== undefined)
907
+ writer.writeId(7, line.moveId);
908
+ if (line.colorRgba !== undefined)
909
+ writer.writeInt(8, line.colorRgba);
910
+ }
911
+ function writeGlyphValue(writer, glyph) {
912
+ if (glyph.start !== undefined)
913
+ writer.writeInt(2, glyph.start);
914
+ writer.writeInt(3, glyph.length);
915
+ writer.writeInt(4, glyph.color);
916
+ writer.writeString(5, glyph.text);
917
+ writer.subblock(6, () => {
918
+ writer.varuint(glyph.rectangles.length);
919
+ for (const rect of glyph.rectangles) {
920
+ writer.f64(rect.x);
921
+ writer.f64(rect.y);
922
+ writer.f64(rect.w);
923
+ writer.f64(rect.h);
924
+ }
925
+ });
926
+ if (glyph.colorRgba !== undefined)
927
+ writer.writeInt(10, glyph.colorRgba);
928
+ }
929
+ function writeText(writer, text) {
930
+ writer.subblock(2, () => {
931
+ writer.subblock(1, () => {
932
+ writer.subblock(1, () => {
933
+ writer.varuint(text.items.length);
934
+ for (const item of text.items) {
935
+ writer.subblock(0, () => {
936
+ writer.writeId(2, item.itemId);
937
+ writer.writeId(3, item.leftId);
938
+ writer.writeId(4, item.rightId);
939
+ writer.writeInt(5, item.deletedLength);
940
+ const { value } = item;
941
+ if (value !== "" && value !== undefined) {
942
+ writer.subblock(6, () => {
943
+ if (typeof value === "number") {
944
+ writer.varuint(0);
945
+ writer.u8(1);
946
+ writer.writeInt(2, value);
947
+ }
948
+ else {
949
+ const encoded = new TextEncoder().encode(value);
950
+ writer.varuint(encoded.length);
951
+ writer.u8(1);
952
+ writer.bytes(encoded);
953
+ }
954
+ });
955
+ }
956
+ });
957
+ }
958
+ });
959
+ });
960
+ writer.subblock(2, () => {
961
+ writer.subblock(1, () => {
962
+ writer.varuint(text.styles.size);
963
+ for (const [key, style] of text.styles) {
964
+ const [authorId, counter] = key.split(":");
965
+ writer.crdtId({
966
+ authorId: Number(authorId),
967
+ counter: Number(counter),
968
+ });
969
+ writer.writeId(1, style.timestamp);
970
+ writer.subblock(2, () => {
971
+ writer.u8(17);
972
+ writer.u8(style.value);
973
+ });
974
+ }
975
+ });
976
+ });
977
+ });
978
+ writer.subblock(3, () => {
979
+ writer.f64(text.posX);
980
+ writer.f64(text.posY);
981
+ });
982
+ writer.writeFloat(4, text.width);
983
+ }
984
+ function uuidToBytes(uuid) {
985
+ return swapUuidBytes(Uint8Array.fromHex(uuid.replaceAll("-", "")));
986
+ }
987
+ const BLOCK_TYPES = {
988
+ migrationInfo: 0x00,
989
+ sceneTree: 0x01,
990
+ treeNode: 0x02,
991
+ sceneGlyphItem: 0x03,
992
+ sceneGroupItem: 0x04,
993
+ sceneLineItem: 0x05,
994
+ sceneTextItem: 0x06,
995
+ rootText: 0x07,
996
+ sceneTombstone: 0x08,
997
+ authorIds: 0x09,
998
+ pageInfo: 0x0a,
999
+ sceneInfo: 0x0d,
1000
+ };
1001
+ function writeBlockBody(writer, block) {
1002
+ switch (block.type) {
1003
+ case "migrationInfo":
1004
+ writer.writeId(1, block.migrationId);
1005
+ writer.writeBool(2, block.isDevice);
1006
+ break;
1007
+ case "sceneTree":
1008
+ writer.writeId(1, block.treeId);
1009
+ writer.writeId(2, block.nodeId);
1010
+ writer.writeBool(3, block.isUpdate);
1011
+ writer.subblock(4, () => writer.writeId(1, block.parentId));
1012
+ break;
1013
+ case "treeNode":
1014
+ writer.writeId(1, block.nodeId);
1015
+ writer.writeLww(2, block.label, (value) => writer.writeString(2, value));
1016
+ writer.writeLww(3, block.visible, (value) => writer.writeBool(2, value));
1017
+ if (block.anchor !== undefined) {
1018
+ const { id, type, threshold, originX } = block.anchor;
1019
+ writer.writeLww(7, id, (value) => writer.writeId(2, value));
1020
+ writer.writeLww(8, type, (value) => writer.writeByte(2, value));
1021
+ writer.writeLww(9, threshold, (value) => writer.writeFloat(2, value));
1022
+ writer.writeLww(10, originX, (value) => writer.writeFloat(2, value));
1023
+ }
1024
+ break;
1025
+ case "sceneGlyphItem":
1026
+ writeItemEnvelope(writer, block.parentId, block.item, 0x01, (glyph) => writeGlyphValue(writer, glyph));
1027
+ break;
1028
+ case "sceneGroupItem":
1029
+ writeItemEnvelope(writer, block.parentId, block.item, 0x02, (id) => writer.writeId(2, id));
1030
+ break;
1031
+ case "sceneLineItem":
1032
+ writeItemEnvelope(writer, block.parentId, block.item, 0x03, (line) => writeLineValue(writer, line, block.currentVersion));
1033
+ break;
1034
+ case "sceneTextItem":
1035
+ case "sceneTombstone":
1036
+ writeItemEnvelope(writer, block.parentId, block.item, 0x00, () => undefined);
1037
+ break;
1038
+ case "rootText":
1039
+ writer.writeId(1, block.blockId);
1040
+ writeText(writer, block.text);
1041
+ break;
1042
+ case "authorIds":
1043
+ writer.varuint(block.authors.size);
1044
+ for (const [authorId, uuid] of block.authors) {
1045
+ writer.subblock(0, () => {
1046
+ const bytes = uuidToBytes(uuid);
1047
+ writer.varuint(bytes.length);
1048
+ writer.bytes(bytes);
1049
+ writer.u16(authorId);
1050
+ });
1051
+ }
1052
+ break;
1053
+ case "pageInfo":
1054
+ writer.writeInt(1, block.loadsCount);
1055
+ writer.writeInt(2, block.mergesCount);
1056
+ writer.writeInt(3, block.textCharsCount);
1057
+ writer.writeInt(4, block.textLinesCount);
1058
+ if (block.typeFolioUseCount !== undefined) {
1059
+ writer.writeInt(5, block.typeFolioUseCount);
1060
+ }
1061
+ break;
1062
+ case "sceneInfo":
1063
+ writer.writeLww(1, block.currentLayer, (value) => writer.writeId(2, value));
1064
+ if (block.backgroundVisible !== undefined) {
1065
+ writer.writeLww(2, block.backgroundVisible, (value) => writer.writeBool(2, value));
1066
+ }
1067
+ if (block.rootDocumentVisible !== undefined) {
1068
+ writer.writeLww(3, block.rootDocumentVisible, (value) => writer.writeBool(2, value));
1069
+ }
1070
+ if (block.paperSize !== undefined) {
1071
+ writer.subblock(5, () => {
1072
+ writer.u32(block.paperSize[0]);
1073
+ writer.u32(block.paperSize[1]);
1074
+ });
1075
+ }
1076
+ break;
1077
+ case "unknown":
1078
+ writer.bytes(block.data);
1079
+ break;
1080
+ }
1081
+ }
1082
+ /** serialize a parsed scene back to version 6 `.rm` bytes */
1083
+ export function serializeRmScene(scene) {
1084
+ const writer = new Writer();
1085
+ const header = V6_HEADER.padEnd(HEADER_LENGTH, " ");
1086
+ writer.bytes(new TextEncoder().encode(header));
1087
+ for (const block of scene.blocks) {
1088
+ const lengthAt = writer.length;
1089
+ writer.u32(0);
1090
+ writer.u8(block.reserved);
1091
+ writer.u8(block.minVersion);
1092
+ writer.u8(block.currentVersion);
1093
+ writer.u8(block.type === "unknown" ? block.blockType : BLOCK_TYPES[block.type]);
1094
+ const bodyStart = writer.length;
1095
+ writeBlockBody(writer, block);
1096
+ if (block.type !== "unknown")
1097
+ writer.bytes(block.extraData);
1098
+ writer.patchU32(lengthAt, block.type === "unknown" && block.declaredLength !== undefined
1099
+ ? block.declaredLength
1100
+ : writer.length - bodyStart);
1101
+ }
1102
+ return writer.finish();
1103
+ }
725
1104
  /** parse a version 6 `.rm` file into a resolvable scene */
726
1105
  export function parseRmScene(data) {
727
1106
  return new RmScene(parseV6Blocks(data));