Skip to content

Commit 3156365

Browse files
authored
fix: allow type:TemplateType metadata (#15)
* fix: allow type:TemplateType metadata
1 parent efa36ec commit 3156365

File tree

3 files changed

+220
-39
lines changed

3 files changed

+220
-39
lines changed

bun.lockb

0 Bytes
Binary file not shown.

src/index.spec.ts

Lines changed: 114 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Metadata,
77
register,
88
remarkable,
9+
TemplateContent,
910
} from ".";
1011
import {
1112
bytesResponse,
@@ -71,16 +72,26 @@ describe("remarkable", () => {
7172

7273
test("#listItems()", async () => {
7374
const docId = "document";
75+
const templateId = "template";
7476
const entryHash = repHash("1");
7577
const metaHash = repHash("2");
7678
const contentHash = repHash("3");
79+
const templateEntryHash = repHash("4");
80+
const templateMetaHash = repHash("5");
81+
const templateContentHash = repHash("6");
7782
const rootEntries = `3
7883
${entryHash}:80000000:${docId}:4:3
84+
${templateEntryHash}:80000000:${templateId}:4:3
7985
`;
8086
const docEntries = `3
8187
${contentHash}:0:${docId}.content:0:1
8288
${metaHash}:0:${docId}.metadata:0:1
8389
fake_hash:0:${docId}.epub:0:1
90+
`;
91+
const templateEntries = `3
92+
${templateContentHash}:0:${templateId}.content:0:1
93+
${templateMetaHash}:0:${templateId}.metadata:0:1
94+
fake_template_hash:0:${docId}.template:0:1
8495
`;
8596
const content: DocumentContent = {
8697
coverPageNumber: 0,
@@ -104,6 +115,36 @@ fake_hash:0:${docId}.epub:0:1
104115
type: "DocumentType",
105116
visibleName: "doc name",
106117
};
118+
const templateMetadata: Metadata = {
119+
createdTime: "",
120+
lastModified: "",
121+
new: false,
122+
parent: "",
123+
pinned: false,
124+
source: "mock",
125+
type: "TemplateType",
126+
visibleName: "Template",
127+
};
128+
const templateContent: TemplateContent = {
129+
author: "",
130+
categories: ["a", "b"],
131+
formatVersion: 1,
132+
iconData: "",
133+
labels: [],
134+
name: "Template",
135+
orientation: "portrait",
136+
supportedScreens: ["rm2", "rmPP"],
137+
templateVersion: "0.0.1",
138+
constants: [{ a: 1 }],
139+
items: [
140+
{
141+
type: "group",
142+
id: "a",
143+
boundingBox: { x: 0, y: 0, width: 1, height: 1 },
144+
children: [],
145+
},
146+
],
147+
};
107148
const expected: Entry = {
108149
id: docId,
109150
hash: entryHash,
@@ -127,8 +168,11 @@ fake_hash:0:${docId}.epub:0:1
127168
}),
128169
textResponse(rootEntries),
129170
textResponse(docEntries),
171+
textResponse(templateEntries),
130172
jsonResponse(metadata),
131173
jsonResponse(content),
174+
jsonResponse(templateMetadata),
175+
jsonResponse(templateContent),
132176
),
133177
);
134178

@@ -166,40 +210,82 @@ hash2:80000000:other:0:2
166210
});
167211
});
168212

169-
test("#getContent()", async () => {
170-
const realHash = repHash("1");
171-
const file = `3
213+
describe("#getContent()", () => {
214+
test("DocumentType", async () => {
215+
const realHash = repHash("1");
216+
const file = `3
172217
${realHash}:0:doc.content:0:1
173218
hash:0:doc.metadata:0:1
174219
hash:0:doc.epub:0:1
175220
hash:0:doc.pdf:0:1
176221
`;
177-
const content: Content = {
178-
fileType: "pdf",
179-
coverPageNumber: -1,
180-
documentMetadata: {},
181-
extraMetadata: {},
182-
fontName: "",
183-
formatVersion: 0,
184-
lineHeight: -1,
185-
margins: 125,
186-
orientation: "portrait",
187-
pageCount: 1,
188-
sizeInBytes: "1",
189-
textAlignment: "left",
190-
textScale: 1,
191-
};
192-
globalThis.fetch = mock(
193-
createMockFetch(
194-
emptyResponse(),
195-
textResponse(file),
196-
jsonResponse(content),
197-
),
198-
);
222+
const content: Content = {
223+
fileType: "pdf",
224+
coverPageNumber: -1,
225+
documentMetadata: {},
226+
extraMetadata: {},
227+
fontName: "",
228+
formatVersion: 0,
229+
lineHeight: -1,
230+
margins: 125,
231+
orientation: "portrait",
232+
pageCount: 1,
233+
sizeInBytes: "1",
234+
textAlignment: "left",
235+
textScale: 1,
236+
};
237+
globalThis.fetch = mock(
238+
createMockFetch(
239+
emptyResponse(),
240+
textResponse(file),
241+
jsonResponse(content),
242+
),
243+
);
199244

200-
const api = await remarkable("");
201-
const cont = await api.getContent(repHash("0"));
202-
expect(cont).toEqual(content);
245+
const api = await remarkable("");
246+
const cont = await api.getContent(repHash("0"));
247+
expect(cont).toEqual(content);
248+
});
249+
250+
test("TemplateType", async () => {
251+
const realHash = repHash("1");
252+
const file = `3
253+
${realHash}:0:tpl.content:0:1
254+
hash:0:tpl.metadata:0:1
255+
hash:0:tpl.template:0:1
256+
`;
257+
const content: TemplateContent = {
258+
author: "",
259+
categories: ["a", "b"],
260+
formatVersion: 1,
261+
iconData: "",
262+
labels: [],
263+
name: "Template",
264+
orientation: "portrait",
265+
supportedScreens: ["rm2", "rmPP"],
266+
templateVersion: "0.0.1",
267+
constants: [{ a: 1 }],
268+
items: [
269+
{
270+
type: "group",
271+
id: "a",
272+
boundingBox: { x: 0, y: 0, width: 1, height: 1 },
273+
children: [],
274+
},
275+
],
276+
};
277+
globalThis.fetch = mock(
278+
createMockFetch(
279+
emptyResponse(),
280+
textResponse(file),
281+
jsonResponse(content),
282+
),
283+
);
284+
285+
const api = await remarkable("");
286+
const cont = await api.getContent(repHash("0"));
287+
expect(cont).toEqual(content);
288+
});
203289
});
204290

205291
test("#getMetadata()", async () => {

src/index.ts

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import JSZip from "jszip";
5757
import {
5858
boolean,
5959
elements,
60+
empty,
6061
enumeration,
6162
float64,
6263
int32,
@@ -225,8 +226,20 @@ export interface DocumentType extends EntryCommon {
225226
lastOpened: string;
226227
}
227228

229+
/** a template, such as from methods.remarkable.com */
230+
export interface TemplateType extends EntryCommon {
231+
/** the key to identify this as a template */
232+
type: "TemplateType";
233+
/** the timestamp of when the template was added/created */
234+
createdTime?: string;
235+
/** where this template was installed from */
236+
source?: string;
237+
/** indicates if this is a newly-installed template */
238+
new?: boolean;
239+
}
240+
228241
/** a remarkable entry for cloud items */
229-
export type Entry = CollectionEntry | DocumentType;
242+
export type Entry = CollectionEntry | DocumentType | TemplateType;
230243

231244
/** an simple entry without any extra information */
232245
export interface SimpleEntry {
@@ -673,8 +686,58 @@ export interface DocumentContent {
673686
viewBackgroundFilter?: BackgroundFilter;
674687
}
675688

689+
/**
690+
* content metadata, stored with the "content" extension
691+
*
692+
* This largely contains description of how to render the document, rather than
693+
* metadata about it.
694+
*/
695+
export interface TemplateContent {
696+
/** the template name */
697+
name: string;
698+
/** the template's author */
699+
author: string;
700+
/** Base64-encoded SVG icon image */
701+
iconData: string;
702+
/** category names this template belongs to (eg: "Planning", "Productivity") */
703+
categories: string[];
704+
/** labels associated with this template (eg: "Project management") */
705+
labels: string[];
706+
/** the orientation of this template */
707+
orientation: "portrait" | "landscape";
708+
/** semantic version for this template */
709+
templateVersion: string;
710+
/** template configuration format version (currently just `1`) */
711+
formatVersion: number;
712+
/**
713+
* which screens the template supports:
714+
*
715+
* - `rm2`: reMarkable 2
716+
* - `rmPP`: reMarkable Paper Pro
717+
*/
718+
supportedScreens: ("rm2" | "rmPP")[];
719+
/** constant values used by the commands in `items` */
720+
constants?: { [name: string]: number }[];
721+
/** the template definition, an SVG-like DSL in JSON */
722+
items: object[];
723+
}
724+
725+
const templateContent = properties({
726+
name: string(),
727+
author: string(),
728+
iconData: string(),
729+
categories: elements(string()),
730+
labels: elements(string()),
731+
orientation: enumeration("portrait", "landscape"),
732+
templateVersion: string(),
733+
formatVersion: uint8(),
734+
supportedScreens: elements(enumeration("rm2", "rmPP")),
735+
constants: elements(values(int32())),
736+
items: elements(empty() as CompiledSchema<object, unknown>),
737+
}) satisfies CompiledSchema<TemplateContent, unknown>;
738+
676739
/** content metadata for any item */
677-
export type Content = CollectionContent | DocumentContent;
740+
export type Content = CollectionContent | DocumentContent | TemplateContent;
678741

679742
const documentContent = properties(
680743
{
@@ -774,7 +837,15 @@ export interface Metadata {
774837
* DocumentType is a document, an epub, pdf, or notebook, CollectionType is a
775838
* folder.
776839
*/
777-
type: "DocumentType" | "CollectionType";
840+
type: "DocumentType" | "CollectionType" | "TemplateType";
841+
/** whether this is this a newly-installed template */
842+
new?: boolean;
843+
/**
844+
* the provider from which this item was obtained/installed
845+
*
846+
* Example: a template from "com.remarkable.methods".
847+
*/
848+
source?: string;
778849
/** [speculative] metadata version, always 0 */
779850
version?: number;
780851
/** the visible name of the item, what it's called on the reMarkable */
@@ -786,7 +857,7 @@ const metadata = properties(
786857
lastModified: string(),
787858
parent: string(),
788859
pinned: boolean(),
789-
type: enumeration("DocumentType", "CollectionType"),
860+
type: enumeration("DocumentType", "CollectionType", "TemplateType"),
790861
visibleName: string(),
791862
},
792863
{
@@ -959,7 +1030,7 @@ export interface RawRemarkableApi {
9591030
* these are hashed differently than files.
9601031
9611032
* @param hash - the hash to get entries for
962-
* @returns the entries
1033+
* @returns the entries
9631034
*/
9641035
getEntries(hash: string): Promise<RawEntry[]>;
9651036

@@ -1563,6 +1634,8 @@ class RawRemarkable implements RawRemarkableApi {
15631634
// the full error for the richer content.
15641635
if (collectionContent.guard(loaded)) {
15651636
return loaded;
1637+
} else if (templateContent.guard(loaded)) {
1638+
return loaded;
15661639
} else if (documentContent.guardAssert(loaded)) {
15671640
return loaded;
15681641
} else {
@@ -1824,12 +1897,34 @@ class Remarkable implements RemarkableApi {
18241897
throw new Error(`couldn't find content for hash ${hash}`);
18251898
}
18261899

1827-
const [{ visibleName, lastModified, pinned, parent, lastOpened }, content] =
1828-
await Promise.all([
1829-
this.raw.getMetadata(metaEnt.hash),
1830-
this.raw.getContent(contentEnt.hash),
1831-
]);
1832-
if (content.fileType === undefined) {
1900+
const [
1901+
{
1902+
visibleName,
1903+
lastModified,
1904+
pinned,
1905+
parent,
1906+
lastOpened,
1907+
new: isNew,
1908+
source,
1909+
},
1910+
content,
1911+
] = await Promise.all([
1912+
this.raw.getMetadata(metaEnt.hash),
1913+
this.raw.getContent(contentEnt.hash),
1914+
]);
1915+
if ("templateVersion" in content) {
1916+
return {
1917+
id,
1918+
hash,
1919+
visibleName,
1920+
lastModified,
1921+
new: isNew,
1922+
pinned,
1923+
source,
1924+
parent,
1925+
type: "TemplateType",
1926+
};
1927+
} else if (content.fileType === undefined) {
18331928
return {
18341929
id,
18351930
hash,

0 commit comments

Comments
 (0)