rmapi-js 12.0.3 → 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 +169 -36
- package/dist/raw.js +214 -74
- 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 +1 -1
package/dist/raw.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
// core-js installs these only when the runtime lacks them, so node 26+ keeps
|
|
2
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";
|
|
3
6
|
import "core-js/modules/es.uint8-array.from-hex.js";
|
|
4
7
|
import "core-js/modules/es.uint8-array.to-base64.js";
|
|
5
8
|
import "core-js/modules/es.uint8-array.to-hex.js";
|
|
6
9
|
import CRC32C from "crc-32/crc32c.js";
|
|
7
10
|
import { z } from "zod";
|
|
8
11
|
import { ValidationError } from "./error.js";
|
|
9
|
-
import { HEADER_LENGTH, parseV5, serializeRm, VERSION_PREFIX, } from "./rm5.js";
|
|
10
|
-
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";
|
|
11
14
|
import { concatArrays } from "./utils.js";
|
|
12
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";
|
|
13
22
|
/**
|
|
14
23
|
* parse the bytes of a reMarkable `.rm` page file
|
|
15
24
|
*
|
|
@@ -39,6 +48,19 @@ export function parseRm(data) {
|
|
|
39
48
|
throw new Error(`unsupported .lines version '${versionChar}'`);
|
|
40
49
|
}
|
|
41
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
|
+
}
|
|
42
64
|
const tag = z
|
|
43
65
|
.object({
|
|
44
66
|
name: z.string(),
|
|
@@ -204,6 +226,33 @@ const content = z.union([
|
|
|
204
226
|
documentContent,
|
|
205
227
|
legacyDocumentContent,
|
|
206
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();
|
|
207
256
|
const metadata = z
|
|
208
257
|
.object({
|
|
209
258
|
lastModified: z.string().optional(),
|
|
@@ -311,9 +360,10 @@ function parseRawEntryLine(line) {
|
|
|
311
360
|
* - `<docid>.content` - a json file roughly describing document properties (see {@link DocumentContent | `DocumentContent`})
|
|
312
361
|
* - `<docid>.metadata` - metadata about the document (see {@link Metadata | `Metadata`})
|
|
313
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`})
|
|
314
364
|
* - `<docid>/<pageid>.rm` - [speculative] raw remarkable vectors, text, etc
|
|
315
|
-
* - `<docid>/<pageid>-metadata.json` -
|
|
316
|
-
* - `<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`})
|
|
317
367
|
*
|
|
318
368
|
* Some items will have both a `.pdf` and `.epub` file, likely due to preparing
|
|
319
369
|
* for export. Collections only have `.content` and `.metadata` files, with
|
|
@@ -322,9 +372,12 @@ function parseRawEntryLine(line) {
|
|
|
322
372
|
* ## Caching
|
|
323
373
|
*
|
|
324
374
|
* Since everything is tied to the hash of it's contents, we can agressively
|
|
325
|
-
* cache results.
|
|
326
|
-
*
|
|
327
|
-
*
|
|
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.
|
|
328
381
|
*
|
|
329
382
|
* By default, this only persists as long as the api instance is alive. However,
|
|
330
383
|
* for performance reasons, you should call {@link dumpCache | `dumpCache`} to
|
|
@@ -341,20 +394,21 @@ export class RawRemarkable {
|
|
|
341
394
|
/**
|
|
342
395
|
* a cache of all hashes we know exist
|
|
343
396
|
*
|
|
344
|
-
* The backend is a readonly file system of hashes to content
|
|
345
|
-
* been read or written successfully
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
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.
|
|
349
402
|
*/
|
|
350
403
|
#cache;
|
|
351
|
-
|
|
404
|
+
#maxCachedBytes;
|
|
405
|
+
constructor(authedFetch, cache, rawHost, uploadHost, maxCachedBytes) {
|
|
352
406
|
this.#authedFetch = authedFetch;
|
|
353
407
|
this.#cache = cache;
|
|
354
408
|
this.#rawHost = rawHost;
|
|
355
409
|
this.#uploadHost = uploadHost;
|
|
410
|
+
this.#maxCachedBytes = maxCachedBytes;
|
|
356
411
|
}
|
|
357
|
-
/** make an authorized request to remarkable */
|
|
358
412
|
/**
|
|
359
413
|
* gets the root hash and the current generation
|
|
360
414
|
*
|
|
@@ -400,16 +454,12 @@ export class RawRemarkable {
|
|
|
400
454
|
async getHash({ id: fileName, hash }) {
|
|
401
455
|
const cached = this.#cache.get(hash);
|
|
402
456
|
if (cached != null) {
|
|
403
|
-
|
|
404
|
-
return enc.encode(cached);
|
|
457
|
+
return cached.slice();
|
|
405
458
|
}
|
|
406
459
|
else {
|
|
460
|
+
// NOTE two simultaneous requests will fetch twice
|
|
407
461
|
const res = await this.#getHash(fileName, hash);
|
|
408
|
-
|
|
409
|
-
const cacheVal = this.#cache.get(hash);
|
|
410
|
-
if (cacheVal === undefined) {
|
|
411
|
-
this.#cache.set(hash, null);
|
|
412
|
-
}
|
|
462
|
+
this.#cache.set(hash, res.byteLength <= this.#maxCachedBytes ? res.slice() : null);
|
|
413
463
|
return res;
|
|
414
464
|
}
|
|
415
465
|
}
|
|
@@ -422,19 +472,8 @@ export class RawRemarkable {
|
|
|
422
472
|
* @param ref - a reference to the stored file (see {@link getHash})
|
|
423
473
|
* @returns the text
|
|
424
474
|
*/
|
|
425
|
-
async getText(
|
|
426
|
-
|
|
427
|
-
if (cached != null) {
|
|
428
|
-
return cached;
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
// NOTE two simultaneous requests will fetch twice
|
|
432
|
-
const raw = await this.#getHash(fileName, hash);
|
|
433
|
-
const dec = new TextDecoder();
|
|
434
|
-
const res = dec.decode(raw);
|
|
435
|
-
this.#cache.set(hash, res);
|
|
436
|
-
return res;
|
|
437
|
-
}
|
|
475
|
+
async getText(ref) {
|
|
476
|
+
return new TextDecoder().decode(await this.getHash(ref));
|
|
438
477
|
}
|
|
439
478
|
/**
|
|
440
479
|
* get the entries associated with a list hash
|
|
@@ -442,12 +481,12 @@ export class RawRemarkable {
|
|
|
442
481
|
* A list hash is the root hash, or any hash with the type 80000000. NOTE
|
|
443
482
|
* these are hashed differently than files.
|
|
444
483
|
*
|
|
445
|
-
* @param ref - a reference whose `id` is
|
|
446
|
-
*
|
|
484
|
+
* @param ref - a reference whose `id` is the bare document id, or `"root"`
|
|
485
|
+
* for the root index
|
|
447
486
|
* @returns the entries
|
|
448
487
|
*/
|
|
449
|
-
async getEntries(
|
|
450
|
-
const rawFile = await this.getText(
|
|
488
|
+
async getEntries({ id, hash }) {
|
|
489
|
+
const rawFile = await this.getText({ id: `${id}.docSchema`, hash });
|
|
451
490
|
const [version, ...rest] = rawFile.slice(0, -1).split("\n");
|
|
452
491
|
if (version === "3") {
|
|
453
492
|
return { entries: rest.map(parseRawEntryLine) };
|
|
@@ -513,11 +552,44 @@ export class RawRemarkable {
|
|
|
513
552
|
const bytes = await this.getHash(ref);
|
|
514
553
|
return parseRm(bytes);
|
|
515
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
|
+
}
|
|
516
589
|
/**
|
|
517
590
|
* the same as {@link putFile | `putFile`} but rendering an `RmPage` to `.rm`
|
|
518
591
|
* bytes
|
|
519
592
|
*
|
|
520
|
-
* Only version 3 and 5 pages can be rendered; version 6 pages are read-only.
|
|
521
593
|
*/
|
|
522
594
|
async putRm(fileName, page) {
|
|
523
595
|
if (!fileName.endsWith(".rm")) {
|
|
@@ -583,67 +655,117 @@ export class RawRemarkable {
|
|
|
583
655
|
"x-goog-hash": `crc32c=${crcHash}`,
|
|
584
656
|
},
|
|
585
657
|
});
|
|
586
|
-
|
|
587
|
-
const cacheVal = this.#cache.get(hash);
|
|
588
|
-
if (cacheVal === undefined) {
|
|
589
|
-
this.#cache.set(hash, null);
|
|
590
|
-
}
|
|
658
|
+
this.#cache.set(hash, bytes.byteLength <= this.#maxCachedBytes ? bytes.slice() : null);
|
|
591
659
|
}
|
|
592
660
|
}
|
|
593
661
|
/**
|
|
594
|
-
* put a raw onto the server
|
|
662
|
+
* put a raw file onto the server
|
|
595
663
|
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
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`}.
|
|
599
667
|
*
|
|
600
668
|
* NOTE: This won't update the state of the reMarkable until this entry is
|
|
601
669
|
* incorporated into the root hash.
|
|
602
670
|
*
|
|
603
671
|
* @param fileName - the file name to upload (e.g. `<id>.pdf`)
|
|
604
672
|
* @param bytes - the bytes to upload
|
|
605
|
-
* @returns the new entry
|
|
673
|
+
* @returns the new entry, pending its upload
|
|
606
674
|
*/
|
|
607
675
|
async putFile(fileName, bytes) {
|
|
608
676
|
const hash = await digest(bytes);
|
|
609
|
-
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 {
|
|
610
682
|
id: fileName,
|
|
611
683
|
hash,
|
|
612
684
|
type: 0,
|
|
613
685
|
subfiles: 0,
|
|
614
686
|
size: bytes.length,
|
|
687
|
+
async [Symbol.asyncDispose]() {
|
|
688
|
+
await upload;
|
|
689
|
+
},
|
|
615
690
|
};
|
|
616
|
-
return [res, this.#putFile(fileName, hash, bytes)];
|
|
617
691
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const [ent, upload] = await this.putFile(fileName, bytes);
|
|
623
|
-
return [
|
|
624
|
-
ent,
|
|
625
|
-
upload.then(() => {
|
|
626
|
-
// on success, write to cache
|
|
627
|
-
this.#cache.set(ent.hash, text);
|
|
628
|
-
}),
|
|
629
|
-
];
|
|
630
|
-
}
|
|
631
|
-
/** 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 */
|
|
632
696
|
async putContent(fileName, content) {
|
|
633
697
|
if (!fileName.endsWith(".content")) {
|
|
634
698
|
throw new Error(`fileName ${fileName} did not end with '.content'`);
|
|
635
699
|
}
|
|
636
700
|
else {
|
|
637
|
-
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`);
|
|
638
716
|
}
|
|
639
717
|
}
|
|
640
|
-
/**
|
|
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 */
|
|
641
744
|
async putMetadata(fileName, metadata) {
|
|
642
745
|
if (!fileName.endsWith(".metadata")) {
|
|
643
746
|
throw new Error(`fileName ${fileName} did not end with '.metadata'`);
|
|
644
747
|
}
|
|
645
748
|
else {
|
|
646
|
-
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 }));
|
|
647
769
|
}
|
|
648
770
|
}
|
|
649
771
|
/**
|
|
@@ -663,11 +785,9 @@ export class RawRemarkable {
|
|
|
663
785
|
* @param id - the id of the list to upload - this should be the item id if
|
|
664
786
|
* uploading an item list, or "root" if uploading a new root list. Note the
|
|
665
787
|
* asymmetry with {@link getEntries | `getEntries`}: `getEntries` takes the
|
|
666
|
-
* full `"<id>.docSchema"` file name, whereas `putEntries` takes the bare id
|
|
667
|
-
* and appends `.docSchema` (and special-cases `"root"`) itself.
|
|
668
788
|
* @param entries - the entries to upload
|
|
669
789
|
*
|
|
670
|
-
* @returns the new list entry
|
|
790
|
+
* @returns the new list entry, pending its upload
|
|
671
791
|
*/
|
|
672
792
|
async putEntries(id, entries, schemaVersion) {
|
|
673
793
|
if (id === "root" && schemaVersion === 3) {
|
|
@@ -704,14 +824,18 @@ export class RawRemarkable {
|
|
|
704
824
|
else {
|
|
705
825
|
throw new Error(`unsupported schema version ${schemaVersion}`);
|
|
706
826
|
}
|
|
707
|
-
const
|
|
827
|
+
const upload = this.#putFile(`${id}.docSchema`, hash, entryBuff);
|
|
828
|
+
upload.catch(() => { });
|
|
829
|
+
return {
|
|
708
830
|
id,
|
|
709
831
|
hash,
|
|
710
832
|
type: schemaVersion > 3 ? 0 : 80000000,
|
|
711
833
|
subfiles: sorted.length,
|
|
712
834
|
size,
|
|
835
|
+
async [Symbol.asyncDispose]() {
|
|
836
|
+
await upload;
|
|
837
|
+
},
|
|
713
838
|
};
|
|
714
|
-
return [res, this.#putFile(`${id}.docSchema`, hash, entryBuff)];
|
|
715
839
|
}
|
|
716
840
|
/**
|
|
717
841
|
* upload a file to the reMarkable cloud using the simple api
|
|
@@ -749,7 +873,23 @@ export class RawRemarkable {
|
|
|
749
873
|
* @returns a serialized version of the cache to pass to a new api instance
|
|
750
874
|
*/
|
|
751
875
|
dumpCache() {
|
|
752
|
-
|
|
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 });
|
|
753
893
|
}
|
|
754
894
|
/** completely clear the cache */
|
|
755
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 {};
|