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/index.d.ts +182 -36
- package/dist/index.js +730 -236
- package/dist/lru.d.ts +4 -4
- package/dist/lru.js +4 -4
- package/dist/raw.d.ts +172 -36
- package/dist/raw.js +220 -75
- package/dist/rm5.d.ts +1 -1
- package/dist/rm6.d.ts +38 -13
- package/dist/rm6.js +418 -39
- package/dist/rmapi-js.esm.min.js +13 -9
- package/package.json +2 -1
package/dist/raw.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
// core-js installs these only when the runtime lacks them, so node 26+ keeps
|
|
2
|
+
// its native implementations untouched
|
|
3
|
+
import "core-js/modules/es.async-disposable-stack.constructor.js";
|
|
4
|
+
import "core-js/modules/es.symbol.async-dispose.js";
|
|
5
|
+
import "core-js/modules/es.uint8-array.from-base64.js";
|
|
6
|
+
import "core-js/modules/es.uint8-array.from-hex.js";
|
|
7
|
+
import "core-js/modules/es.uint8-array.to-base64.js";
|
|
8
|
+
import "core-js/modules/es.uint8-array.to-hex.js";
|
|
9
|
+
import CRC32C from "crc-32/crc32c.js";
|
|
2
10
|
import { z } from "zod";
|
|
3
11
|
import { ValidationError } from "./error.js";
|
|
4
|
-
import { HEADER_LENGTH, parseV5, serializeRm, VERSION_PREFIX, } from "./rm5.js";
|
|
5
|
-
import { parseRmScene } from "./rm6.js";
|
|
12
|
+
import { HEADER_LENGTH, parseV5, serializeRm as serializeV5, VERSION_PREFIX, } from "./rm5.js";
|
|
13
|
+
import { parseRmScene, serializeRmScene } from "./rm6.js";
|
|
6
14
|
import { concatArrays } from "./utils.js";
|
|
7
15
|
const hashReg = /^[0-9a-f]{64}$/;
|
|
16
|
+
/** the dump format version; absent means the original text-only format */
|
|
17
|
+
export const CACHE_VERSION = 2;
|
|
18
|
+
/** marks a dumped cache entry stored as utf-8 text */
|
|
19
|
+
export const TEXT_PREFIX = "t";
|
|
20
|
+
/** marks a dumped cache entry stored as base64 */
|
|
21
|
+
export const BYTES_PREFIX = "b";
|
|
8
22
|
/**
|
|
9
23
|
* parse the bytes of a reMarkable `.rm` page file
|
|
10
24
|
*
|
|
@@ -34,6 +48,19 @@ export function parseRm(data) {
|
|
|
34
48
|
throw new Error(`unsupported .lines version '${versionChar}'`);
|
|
35
49
|
}
|
|
36
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* serialize a parsed reMarkable `.rm` page back to bytes
|
|
53
|
+
*
|
|
54
|
+
* The inverse of {@link parseRm | `parseRm`}: versions 3 and 5 render from a
|
|
55
|
+
* flat {@link RmPageV5 | `RmPageV5`}, version 6 from an
|
|
56
|
+
* {@link RmScene | `RmScene`}.
|
|
57
|
+
*
|
|
58
|
+
* @param page - the page to render
|
|
59
|
+
* @returns the `.rm` file bytes
|
|
60
|
+
*/
|
|
61
|
+
export function serializeRm(page) {
|
|
62
|
+
return page.version === 6 ? serializeRmScene(page) : serializeV5(page);
|
|
63
|
+
}
|
|
37
64
|
const tag = z
|
|
38
65
|
.object({
|
|
39
66
|
name: z.string(),
|
|
@@ -199,6 +226,33 @@ const content = z.union([
|
|
|
199
226
|
documentContent,
|
|
200
227
|
legacyDocumentContent,
|
|
201
228
|
]);
|
|
229
|
+
const highlightsReg = /\.highlights\/[^/]+\.json$/;
|
|
230
|
+
const highlightsFile = z
|
|
231
|
+
.object({
|
|
232
|
+
highlights: z.array(z.array(z
|
|
233
|
+
.object({
|
|
234
|
+
text: z.string(),
|
|
235
|
+
color: z.number().int(),
|
|
236
|
+
start: z.number().int(),
|
|
237
|
+
length: z.number().int(),
|
|
238
|
+
rects: z.array(z
|
|
239
|
+
.object({
|
|
240
|
+
x: z.number(),
|
|
241
|
+
y: z.number(),
|
|
242
|
+
width: z.number(),
|
|
243
|
+
height: z.number(),
|
|
244
|
+
})
|
|
245
|
+
.passthrough()),
|
|
246
|
+
})
|
|
247
|
+
.passthrough())),
|
|
248
|
+
})
|
|
249
|
+
.passthrough();
|
|
250
|
+
const pageMetadataReg = /\/[^/]+-metadata\.json$/;
|
|
251
|
+
const pageMetadata = z
|
|
252
|
+
.object({
|
|
253
|
+
layers: z.array(z.object({ name: z.string() }).passthrough()),
|
|
254
|
+
})
|
|
255
|
+
.passthrough();
|
|
202
256
|
const metadata = z
|
|
203
257
|
.object({
|
|
204
258
|
lastModified: z.string().optional(),
|
|
@@ -306,9 +360,10 @@ function parseRawEntryLine(line) {
|
|
|
306
360
|
* - `<docid>.content` - a json file roughly describing document properties (see {@link DocumentContent | `DocumentContent`})
|
|
307
361
|
* - `<docid>.metadata` - metadata about the document (see {@link Metadata | `Metadata`})
|
|
308
362
|
* - `<docid>.pagedata` - a text file where each line is the template of that page
|
|
363
|
+
* - `<docid>.template` - a template attached to the item (see {@link TemplateContent | `TemplateContent`})
|
|
309
364
|
* - `<docid>/<pageid>.rm` - [speculative] raw remarkable vectors, text, etc
|
|
310
|
-
* - `<docid>/<pageid>-metadata.json` -
|
|
311
|
-
* - `<docid>.highlights/<pageid>.json` -
|
|
365
|
+
* - `<docid>/<pageid>-metadata.json` - page layer metadata (see {@link PageMetadata | `PageMetadata`})
|
|
366
|
+
* - `<docid>.highlights/<pageid>.json` - text highlights on the page (see {@link Highlight | `Highlight`})
|
|
312
367
|
*
|
|
313
368
|
* Some items will have both a `.pdf` and `.epub` file, likely due to preparing
|
|
314
369
|
* for export. Collections only have `.content` and `.metadata` files, with
|
|
@@ -317,9 +372,12 @@ function parseRawEntryLine(line) {
|
|
|
317
372
|
* ## Caching
|
|
318
373
|
*
|
|
319
374
|
* Since everything is tied to the hash of it's contents, we can agressively
|
|
320
|
-
* cache results.
|
|
321
|
-
*
|
|
322
|
-
*
|
|
375
|
+
* cache results. Anything up to `maxCachedBytes` is kept as the bytes that were
|
|
376
|
+
* transferred; anything larger records only that the hash exists, which is
|
|
377
|
+
* enough to skip writing it again.
|
|
378
|
+
*
|
|
379
|
+
* A dump is tied to the account it came from, since a known hash is taken as
|
|
380
|
+
* proof the server already holds that file. Don't share one between accounts.
|
|
323
381
|
*
|
|
324
382
|
* By default, this only persists as long as the api instance is alive. However,
|
|
325
383
|
* for performance reasons, you should call {@link dumpCache | `dumpCache`} to
|
|
@@ -336,20 +394,21 @@ export class RawRemarkable {
|
|
|
336
394
|
/**
|
|
337
395
|
* a cache of all hashes we know exist
|
|
338
396
|
*
|
|
339
|
-
* The backend is a readonly file system of hashes to content
|
|
340
|
-
* been read or written successfully
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
397
|
+
* The backend is a readonly file system of hashes to content, so once a hash
|
|
398
|
+
* has been read or written successfully we know it exists, and often what it
|
|
399
|
+
* holds. Contents are kept as the bytes that were transferred; anything over
|
|
400
|
+
* `maxCachedBytes` is stored as null, recording existence without the
|
|
401
|
+
* payload.
|
|
344
402
|
*/
|
|
345
403
|
#cache;
|
|
346
|
-
|
|
404
|
+
#maxCachedBytes;
|
|
405
|
+
constructor(authedFetch, cache, rawHost, uploadHost, maxCachedBytes) {
|
|
347
406
|
this.#authedFetch = authedFetch;
|
|
348
407
|
this.#cache = cache;
|
|
349
408
|
this.#rawHost = rawHost;
|
|
350
409
|
this.#uploadHost = uploadHost;
|
|
410
|
+
this.#maxCachedBytes = maxCachedBytes;
|
|
351
411
|
}
|
|
352
|
-
/** make an authorized request to remarkable */
|
|
353
412
|
/**
|
|
354
413
|
* gets the root hash and the current generation
|
|
355
414
|
*
|
|
@@ -395,16 +454,12 @@ export class RawRemarkable {
|
|
|
395
454
|
async getHash({ id: fileName, hash }) {
|
|
396
455
|
const cached = this.#cache.get(hash);
|
|
397
456
|
if (cached != null) {
|
|
398
|
-
|
|
399
|
-
return enc.encode(cached);
|
|
457
|
+
return cached.slice();
|
|
400
458
|
}
|
|
401
459
|
else {
|
|
460
|
+
// NOTE two simultaneous requests will fetch twice
|
|
402
461
|
const res = await this.#getHash(fileName, hash);
|
|
403
|
-
|
|
404
|
-
const cacheVal = this.#cache.get(hash);
|
|
405
|
-
if (cacheVal === undefined) {
|
|
406
|
-
this.#cache.set(hash, null);
|
|
407
|
-
}
|
|
462
|
+
this.#cache.set(hash, res.byteLength <= this.#maxCachedBytes ? res.slice() : null);
|
|
408
463
|
return res;
|
|
409
464
|
}
|
|
410
465
|
}
|
|
@@ -417,19 +472,8 @@ export class RawRemarkable {
|
|
|
417
472
|
* @param ref - a reference to the stored file (see {@link getHash})
|
|
418
473
|
* @returns the text
|
|
419
474
|
*/
|
|
420
|
-
async getText(
|
|
421
|
-
|
|
422
|
-
if (cached != null) {
|
|
423
|
-
return cached;
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
// NOTE two simultaneous requests will fetch twice
|
|
427
|
-
const raw = await this.#getHash(fileName, hash);
|
|
428
|
-
const dec = new TextDecoder();
|
|
429
|
-
const res = dec.decode(raw);
|
|
430
|
-
this.#cache.set(hash, res);
|
|
431
|
-
return res;
|
|
432
|
-
}
|
|
475
|
+
async getText(ref) {
|
|
476
|
+
return new TextDecoder().decode(await this.getHash(ref));
|
|
433
477
|
}
|
|
434
478
|
/**
|
|
435
479
|
* get the entries associated with a list hash
|
|
@@ -437,12 +481,12 @@ export class RawRemarkable {
|
|
|
437
481
|
* A list hash is the root hash, or any hash with the type 80000000. NOTE
|
|
438
482
|
* these are hashed differently than files.
|
|
439
483
|
*
|
|
440
|
-
* @param ref - a reference whose `id` is
|
|
441
|
-
*
|
|
484
|
+
* @param ref - a reference whose `id` is the bare document id, or `"root"`
|
|
485
|
+
* for the root index
|
|
442
486
|
* @returns the entries
|
|
443
487
|
*/
|
|
444
|
-
async getEntries(
|
|
445
|
-
const rawFile = await this.getText(
|
|
488
|
+
async getEntries({ id, hash }) {
|
|
489
|
+
const rawFile = await this.getText({ id: `${id}.docSchema`, hash });
|
|
446
490
|
const [version, ...rest] = rawFile.slice(0, -1).split("\n");
|
|
447
491
|
if (version === "3") {
|
|
448
492
|
return { entries: rest.map(parseRawEntryLine) };
|
|
@@ -508,11 +552,44 @@ export class RawRemarkable {
|
|
|
508
552
|
const bytes = await this.getHash(ref);
|
|
509
553
|
return parseRm(bytes);
|
|
510
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* get the parsed text highlights of a page
|
|
557
|
+
*
|
|
558
|
+
* @param ref - a reference to a `<docid>.highlights/<pageid>.json` file
|
|
559
|
+
* @returns the page's highlights; [speculative] each inner array groups the
|
|
560
|
+
* fragments of one highlighted passage
|
|
561
|
+
*/
|
|
562
|
+
async getHighlights(ref) {
|
|
563
|
+
const raw = await this.getText(ref);
|
|
564
|
+
const loaded = JSON.parse(raw);
|
|
565
|
+
return highlightsFile.parse(loaded).highlights;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* get a template stored as a `<docid>.template` sidecar file
|
|
569
|
+
*
|
|
570
|
+
* @param ref - a reference to a `<docid>.template` file
|
|
571
|
+
* @returns the template content
|
|
572
|
+
*/
|
|
573
|
+
async getTemplate(ref) {
|
|
574
|
+
const raw = await this.getText(ref);
|
|
575
|
+
const loaded = JSON.parse(raw);
|
|
576
|
+
return templateContent.parse(loaded);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* get the parsed layer metadata of a page
|
|
580
|
+
*
|
|
581
|
+
* @param ref - a reference to a `<docid>/<pageid>-metadata.json` file
|
|
582
|
+
* @returns the page metadata
|
|
583
|
+
*/
|
|
584
|
+
async getPageMetadata(ref) {
|
|
585
|
+
const raw = await this.getText(ref);
|
|
586
|
+
const loaded = JSON.parse(raw);
|
|
587
|
+
return pageMetadata.parse(loaded);
|
|
588
|
+
}
|
|
511
589
|
/**
|
|
512
590
|
* the same as {@link putFile | `putFile`} but rendering an `RmPage` to `.rm`
|
|
513
591
|
* bytes
|
|
514
592
|
*
|
|
515
|
-
* Only version 3 and 5 pages can be rendered; version 6 pages are read-only.
|
|
516
593
|
*/
|
|
517
594
|
async putRm(fileName, page) {
|
|
518
595
|
if (!fileName.endsWith(".rm")) {
|
|
@@ -578,67 +655,117 @@ export class RawRemarkable {
|
|
|
578
655
|
"x-goog-hash": `crc32c=${crcHash}`,
|
|
579
656
|
},
|
|
580
657
|
});
|
|
581
|
-
|
|
582
|
-
const cacheVal = this.#cache.get(hash);
|
|
583
|
-
if (cacheVal === undefined) {
|
|
584
|
-
this.#cache.set(hash, null);
|
|
585
|
-
}
|
|
658
|
+
this.#cache.set(hash, bytes.byteLength <= this.#maxCachedBytes ? bytes.slice() : null);
|
|
586
659
|
}
|
|
587
660
|
}
|
|
588
661
|
/**
|
|
589
|
-
* put a raw onto the server
|
|
662
|
+
* put a raw file onto the server
|
|
590
663
|
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
664
|
+
* The returned entry is usable immediately, while the upload is still in
|
|
665
|
+
* flight; disposing it waits for the upload to finish. See
|
|
666
|
+
* {@link PendingEntry | `PendingEntry`}.
|
|
594
667
|
*
|
|
595
668
|
* NOTE: This won't update the state of the reMarkable until this entry is
|
|
596
669
|
* incorporated into the root hash.
|
|
597
670
|
*
|
|
598
671
|
* @param fileName - the file name to upload (e.g. `<id>.pdf`)
|
|
599
672
|
* @param bytes - the bytes to upload
|
|
600
|
-
* @returns the new entry
|
|
673
|
+
* @returns the new entry, pending its upload
|
|
601
674
|
*/
|
|
602
675
|
async putFile(fileName, bytes) {
|
|
603
676
|
const hash = await digest(bytes);
|
|
604
|
-
const
|
|
677
|
+
const upload = this.#putFile(fileName, hash, bytes);
|
|
678
|
+
// an upload that fails before anyone disposes the entry would otherwise be
|
|
679
|
+
// an unhandled rejection; disposing re-awaits it and still throws
|
|
680
|
+
upload.catch(() => { });
|
|
681
|
+
return {
|
|
605
682
|
id: fileName,
|
|
606
683
|
hash,
|
|
607
684
|
type: 0,
|
|
608
685
|
subfiles: 0,
|
|
609
686
|
size: bytes.length,
|
|
687
|
+
async [Symbol.asyncDispose]() {
|
|
688
|
+
await upload;
|
|
689
|
+
},
|
|
610
690
|
};
|
|
611
|
-
return [res, this.#putFile(fileName, hash, bytes)];
|
|
612
691
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const [ent, upload] = await this.putFile(fileName, bytes);
|
|
618
|
-
return [
|
|
619
|
-
ent,
|
|
620
|
-
upload.then(() => {
|
|
621
|
-
// on success, write to cache
|
|
622
|
-
this.#cache.set(ent.hash, text);
|
|
623
|
-
}),
|
|
624
|
-
];
|
|
625
|
-
}
|
|
626
|
-
/** the same as {@link putText | `putText`} but with extra validation for Content */
|
|
692
|
+
async #putEncoded(fileName, text) {
|
|
693
|
+
return await this.putFile(fileName, new TextEncoder().encode(text));
|
|
694
|
+
}
|
|
695
|
+
/** the same as {@link putFile | `putFile`} but with extra validation for Content */
|
|
627
696
|
async putContent(fileName, content) {
|
|
628
697
|
if (!fileName.endsWith(".content")) {
|
|
629
698
|
throw new Error(`fileName ${fileName} did not end with '.content'`);
|
|
630
699
|
}
|
|
631
700
|
else {
|
|
632
|
-
return await this
|
|
701
|
+
return await this.#putEncoded(fileName, JSON.stringify(content));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* the same as {@link putFile | `putFile`} but for a `.pagedata` file
|
|
706
|
+
*
|
|
707
|
+
* @param fileName - the file to write, of the form `<docid>.pagedata`
|
|
708
|
+
* @param templates - one template name per page, in page order
|
|
709
|
+
*/
|
|
710
|
+
async putPagedata(fileName, templates) {
|
|
711
|
+
if (!fileName.endsWith(".pagedata")) {
|
|
712
|
+
throw new Error(`fileName ${fileName} did not end with '.pagedata'`);
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
return await this.#putEncoded(fileName, `${templates.join("\n")}\n`);
|
|
633
716
|
}
|
|
634
717
|
}
|
|
635
|
-
/**
|
|
718
|
+
/**
|
|
719
|
+
* the same as {@link putFile | `putFile`} but with extra validation for page
|
|
720
|
+
* layer metadata
|
|
721
|
+
*
|
|
722
|
+
* @param fileName - the file to write, of the form
|
|
723
|
+
* `<docid>/<pageid>-metadata.json`
|
|
724
|
+
* @param meta - the page's layer metadata
|
|
725
|
+
*/
|
|
726
|
+
async putPageMetadata(fileName, meta) {
|
|
727
|
+
if (!pageMetadataReg.test(fileName)) {
|
|
728
|
+
throw new ValidationError(fileName, pageMetadataReg, "fileName was not of the form '<docid>/<pageid>-metadata.json'");
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
return await this.#putEncoded(fileName, JSON.stringify(meta));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/** the same as {@link putFile | `putFile`} but with extra validation for a template sidecar */
|
|
735
|
+
async putTemplate(fileName, template) {
|
|
736
|
+
if (!fileName.endsWith(".template")) {
|
|
737
|
+
throw new Error(`fileName ${fileName} did not end with '.template'`);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
return await this.#putEncoded(fileName, JSON.stringify(template));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/** the same as {@link putFile | `putFile`} but with extra validation for Metadata */
|
|
636
744
|
async putMetadata(fileName, metadata) {
|
|
637
745
|
if (!fileName.endsWith(".metadata")) {
|
|
638
746
|
throw new Error(`fileName ${fileName} did not end with '.metadata'`);
|
|
639
747
|
}
|
|
640
748
|
else {
|
|
641
|
-
return await this
|
|
749
|
+
return await this.#putEncoded(fileName, JSON.stringify(metadata));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* the same as {@link putFile | `putFile`} but with extra validation for
|
|
754
|
+
* highlights
|
|
755
|
+
*
|
|
756
|
+
* Rewraps the array in the file's `highlights` envelope, which
|
|
757
|
+
* {@link getHighlights | `getHighlights`} strips.
|
|
758
|
+
*
|
|
759
|
+
* @param fileName - the file to write, of the form
|
|
760
|
+
* `<docid>.highlights/<pageid>.json`
|
|
761
|
+
* @param highlights - the page's highlights
|
|
762
|
+
*/
|
|
763
|
+
async putHighlights(fileName, highlights) {
|
|
764
|
+
if (!highlightsReg.test(fileName)) {
|
|
765
|
+
throw new ValidationError(fileName, highlightsReg, "fileName was not of the form '<docid>.highlights/<pageid>.json'");
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
return await this.#putEncoded(fileName, JSON.stringify({ highlights }));
|
|
642
769
|
}
|
|
643
770
|
}
|
|
644
771
|
/**
|
|
@@ -658,11 +785,9 @@ export class RawRemarkable {
|
|
|
658
785
|
* @param id - the id of the list to upload - this should be the item id if
|
|
659
786
|
* uploading an item list, or "root" if uploading a new root list. Note the
|
|
660
787
|
* asymmetry with {@link getEntries | `getEntries`}: `getEntries` takes the
|
|
661
|
-
* full `"<id>.docSchema"` file name, whereas `putEntries` takes the bare id
|
|
662
|
-
* and appends `.docSchema` (and special-cases `"root"`) itself.
|
|
663
788
|
* @param entries - the entries to upload
|
|
664
789
|
*
|
|
665
|
-
* @returns the new list entry
|
|
790
|
+
* @returns the new list entry, pending its upload
|
|
666
791
|
*/
|
|
667
792
|
async putEntries(id, entries, schemaVersion) {
|
|
668
793
|
if (id === "root" && schemaVersion === 3) {
|
|
@@ -699,14 +824,18 @@ export class RawRemarkable {
|
|
|
699
824
|
else {
|
|
700
825
|
throw new Error(`unsupported schema version ${schemaVersion}`);
|
|
701
826
|
}
|
|
702
|
-
const
|
|
827
|
+
const upload = this.#putFile(`${id}.docSchema`, hash, entryBuff);
|
|
828
|
+
upload.catch(() => { });
|
|
829
|
+
return {
|
|
703
830
|
id,
|
|
704
831
|
hash,
|
|
705
832
|
type: schemaVersion > 3 ? 0 : 80000000,
|
|
706
833
|
subfiles: sorted.length,
|
|
707
834
|
size,
|
|
835
|
+
async [Symbol.asyncDispose]() {
|
|
836
|
+
await upload;
|
|
837
|
+
},
|
|
708
838
|
};
|
|
709
|
-
return [res, this.#putFile(`${id}.docSchema`, hash, entryBuff)];
|
|
710
839
|
}
|
|
711
840
|
/**
|
|
712
841
|
* upload a file to the reMarkable cloud using the simple api
|
|
@@ -744,7 +873,23 @@ export class RawRemarkable {
|
|
|
744
873
|
* @returns a serialized version of the cache to pass to a new api instance
|
|
745
874
|
*/
|
|
746
875
|
dumpCache() {
|
|
747
|
-
|
|
876
|
+
const entries = {};
|
|
877
|
+
// utf-8 text is stored as itself; base64 would cost a third more for what
|
|
878
|
+
// is mostly json
|
|
879
|
+
const dec = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
880
|
+
for (const [hash, value] of this.#cache) {
|
|
881
|
+
if (value === null) {
|
|
882
|
+
entries[hash] = null;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
entries[hash] = `${TEXT_PREFIX}${dec.decode(value)}`;
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
entries[hash] = `${BYTES_PREFIX}${value.toBase64()}`;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return JSON.stringify({ version: CACHE_VERSION, entries });
|
|
748
893
|
}
|
|
749
894
|
/** completely clear the cache */
|
|
750
895
|
clearCache() {
|
package/dist/rm5.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface RmLayer {
|
|
|
62
62
|
* a parsed version 3 or 5 page
|
|
63
63
|
*
|
|
64
64
|
* Coordinates use a top-left origin: `x` in `[0, width]`, `y` in `[0, height]`,
|
|
65
|
-
* in device pixels.
|
|
65
|
+
* in device pixels.
|
|
66
66
|
*/
|
|
67
67
|
export interface RmPageV5 {
|
|
68
68
|
/** the file format version */
|
package/dist/rm6.d.ts
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.
|
|
9
|
-
*
|
|
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
|
*/
|
|
@@ -44,7 +45,7 @@ export interface RmV6Point {
|
|
|
44
45
|
y: number;
|
|
45
46
|
/** pen speed */
|
|
46
47
|
speed: number;
|
|
47
|
-
/** stroke width */
|
|
48
|
+
/** stroke width; integral for version 2 points, fractional for version 1 */
|
|
48
49
|
width: number;
|
|
49
50
|
/** pen direction, 0–255 mapping onto 0–2π */
|
|
50
51
|
direction: number;
|
|
@@ -63,6 +64,10 @@ export interface RmV6Line {
|
|
|
63
64
|
startingLength: number;
|
|
64
65
|
/** the sampled points */
|
|
65
66
|
points: RmV6Point[];
|
|
67
|
+
/** the stroke's timestamp id, if the file carried one */
|
|
68
|
+
timestampId?: CrdtId;
|
|
69
|
+
/** the stroke's move id, if the file carried one */
|
|
70
|
+
moveId?: CrdtId;
|
|
66
71
|
/**
|
|
67
72
|
* a packed little-endian uint32 color, only present for highlighter strokes
|
|
68
73
|
*
|
|
@@ -128,6 +133,12 @@ export interface RmV6Text {
|
|
|
128
133
|
}
|
|
129
134
|
/** fields carried by every block */
|
|
130
135
|
interface BlockCommon {
|
|
136
|
+
/**
|
|
137
|
+
* the header byte between the block length and the versions
|
|
138
|
+
*
|
|
139
|
+
* Zero in every well-formed file seen, but preserved rather than assumed.
|
|
140
|
+
*/
|
|
141
|
+
reserved: number;
|
|
131
142
|
/** the block's minimum reader version */
|
|
132
143
|
minVersion: number;
|
|
133
144
|
/** the block's current version (selects the point encoding for lines) */
|
|
@@ -167,14 +178,19 @@ export interface TreeNodeBlock extends BlockCommon {
|
|
|
167
178
|
label: LwwValue<string>;
|
|
168
179
|
/** whether the layer is visible */
|
|
169
180
|
visible: LwwValue<boolean>;
|
|
170
|
-
/** the
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
/** where the node is anchored, when it is */
|
|
182
|
+
anchor?: TreeNodeAnchor;
|
|
183
|
+
}
|
|
184
|
+
/** the four values a tree node carries when it's anchored, always together */
|
|
185
|
+
export interface TreeNodeAnchor {
|
|
186
|
+
/** the anchor node's crdt id */
|
|
187
|
+
id: LwwValue<CrdtId>;
|
|
188
|
+
/** the anchor type */
|
|
189
|
+
type: LwwValue<number>;
|
|
190
|
+
/** the anchor threshold */
|
|
191
|
+
threshold: LwwValue<number>;
|
|
192
|
+
/** the anchor's horizontal origin */
|
|
193
|
+
originX: LwwValue<number>;
|
|
178
194
|
}
|
|
179
195
|
/** the `0x05` scene line item block — a stroke */
|
|
180
196
|
export interface SceneLineItemBlock extends BlockCommon {
|
|
@@ -242,8 +258,8 @@ export interface PageInfoBlock extends BlockCommon {
|
|
|
242
258
|
textCharsCount: number;
|
|
243
259
|
/** the number of text lines on the page */
|
|
244
260
|
textLinesCount: number;
|
|
245
|
-
/** the type-folio use count */
|
|
246
|
-
typeFolioUseCount
|
|
261
|
+
/** the type-folio use count, if the block carried one */
|
|
262
|
+
typeFolioUseCount?: number;
|
|
247
263
|
}
|
|
248
264
|
/** the `0x09` author ids block — the author-id to uuid table */
|
|
249
265
|
export interface AuthorIdsBlock extends BlockCommon {
|
|
@@ -273,6 +289,13 @@ export interface UnknownBlock extends BlockCommon {
|
|
|
273
289
|
blockType: number;
|
|
274
290
|
/** the raw block body bytes */
|
|
275
291
|
data: Uint8Array;
|
|
292
|
+
/**
|
|
293
|
+
* the length the block header declared, when it overran the file
|
|
294
|
+
*
|
|
295
|
+
* Corrupt files exist in the wild; keeping the original figure lets them
|
|
296
|
+
* round-trip byte for byte instead of being silently rewritten.
|
|
297
|
+
*/
|
|
298
|
+
declaredLength?: number;
|
|
276
299
|
}
|
|
277
300
|
/** any parsed version 6 block */
|
|
278
301
|
export type RmBlock = MigrationInfoBlock | SceneTreeBlock | TreeNodeBlock | SceneLineItemBlock | SceneGlyphItemBlock | SceneGroupItemBlock | SceneTextItemBlock | SceneTombstoneItemBlock | RootTextBlock | PageInfoBlock | AuthorIdsBlock | SceneInfoBlock | UnknownBlock;
|
|
@@ -329,6 +352,8 @@ export declare class RmScene {
|
|
|
329
352
|
/** the page's document text, if any */
|
|
330
353
|
text(): RmV6Text | undefined;
|
|
331
354
|
}
|
|
355
|
+
/** serialize a parsed scene back to version 6 `.rm` bytes */
|
|
356
|
+
export declare function serializeRmScene(scene: RmScene): Uint8Array;
|
|
332
357
|
/** parse a version 6 `.rm` file into a resolvable scene */
|
|
333
358
|
export declare function parseRmScene(data: Uint8Array): RmScene;
|
|
334
359
|
export {};
|