From 22c17e44308071f04f9a41985477e45e308cb3eb Mon Sep 17 00:00:00 2001 From: Brandon Philips's Clanker Date: Tue, 28 Jul 2026 13:53:39 -0700 Subject: [PATCH] Fix ILink.OBJPAGE and LINKTYPE doc comments to match observed data Both were verified wrong against real device-created notes (see #32): - OBJPAGE ("0-indexed page number this link appears on") doesn't match a link's actual source page under any indexing convention. In this repo's own nomad-3.26.40-link-tag-3p.note fixture, 3 links on the same source page have 3 different OBJPAGE values (1, 2, 0). - LINKTYPE ("1 = internal note link") doesn't reliably distinguish real internal links either -- 2 of that same fixture's 3 links have LINKTYPE: '0', and everything in SupernoteX.links is already a resolved link regardless of this value. What's actually reliable: the SupernoteX.links Record key's first 4 characters, read as an integer, are the link's 1-indexed source page. Documents that in _parseLinks()'s comment, and adds a regression test against the existing fixture pinning both the key-prefix convention and OBJPAGE's unreliability, so a future "fix" to OBJPAGE's doc comment can't silently drop the fact that it disagrees with the key prefix on this repo's own test data. --- src/format.ts | 17 +++++++++++++++-- src/parsing.ts | 14 +++++++++++++- tests/main.test.ts | 19 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/format.ts b/src/format.ts index 6340552..7534815 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,6 +1,11 @@ /** Supernote internal link (created via the Supernote link feature). */ export interface ILink { - /** Link type. 1 = internal note link. */ + /** Link type. Meaning unconfirmed: doesn't reliably distinguish real + * internal-note links from other kinds, e.g. real device-created notes + * have had genuine links with `LINKTYPE: '0'`. Every entry present in + * `SupernoteX.links` has already been resolved as a real link regardless + * of this value, so it isn't a "is this a real link" filter. See + * https://github.com/philips/supernote-typescript/issues/32. */ LINKTYPE: string; LINKINOUT: string; /** Address of the link bitmap. */ @@ -16,7 +21,15 @@ export interface ILink { LINKFILE: string; LINKFILEID: string; PAGEID: string; - /** 0-indexed page number this link appears on. */ + /** NOT reliably the page this link is drawn on, despite the name -- + * verified against a real device-created note where it didn't match the + * link's actual source page under any indexing convention. Best guess is + * something closer to the source page's own template/display label, + * which can drift from that page's current position in `pages` (e.g. + * after reordering), but that's unconfirmed. To find which page a link + * actually appears on, use the first 4 characters of its key in the + * `SupernoteX.links` Record (see `_parseLinks` in parsing.ts) instead. + * See https://github.com/philips/supernote-typescript/issues/32. */ OBJPAGE: string; /** Decoded filename (without path or .note extension) ready for use as [[link]]. */ text: string; diff --git a/src/parsing.ts b/src/parsing.ts index 2c73d0d..bf266bb 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -654,7 +654,19 @@ export class SupernoteX implements ISupernote { return this.titles; } - /** Parse links from Supernote file's buffer contents. */ + /** Parse links from Supernote file's buffer contents. + * The returned Record's keys (taken as-is from the file's own `LINKO_*` + * footer keys) aren't just unique identifiers: their first 4 characters, + * read as an integer, are the 1-indexed page the link is actually drawn + * on -- verified against real device-created notes, and the only + * reliable way to find a link's source page (`ILink.OBJPAGE` isn't; see + * its doc comment). The remaining characters are the link's own + * `LINKRECT` `y,x,h,w` digits, zero-padded to 4 each and concatenated -- + * cosmetic, just enough to keep keys with links on the same page + * distinct. E.g. key `"00021253011801320743"` for a link on page 2 with + * `LINKRECT: "118,1253,743,132"` (`x,y,w,h`): `0002` + `1253` (y) + + * `0118` (x) + `0132` (h) + `0743` (w). + * See https://github.com/philips/supernote-typescript/issues/32. */ _parseLinks(buffer: Uint8Array): Record { this.links = {}; Object.entries(this.footer.LINKO).forEach(([key, value]) => { diff --git a/tests/main.test.ts b/tests/main.test.ts index 7141178..65be369 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -62,6 +62,25 @@ describe("links", () => { expect(crossFileNoPage).toBeDefined() expect(crossFileNoPage!.text).toBe("nomad-3.26.40-blank-2p") }) + + test("the links Record key's first 4 characters are the 1-indexed source page, not OBJPAGE", async () => { + let sn = new SupernoteX(await readFileToUint8Array("nomad-3.26.40-link-tag-3p.note")) + // All 3 links in this fixture are physically drawn on the same page + // (source page array-index 1, i.e. page 2), so every key shares that + // page's 1-indexed prefix, "0002" -- see _parseLinks()'s doc comment. + const keys = Object.keys(sn.links) + expect(keys.length).toBeGreaterThan(0) + for (const key of keys) { + expect(key.slice(0, 4)).toBe("0002") + } + // OBJPAGE is NOT a reliable stand-in for the key prefix: these 3 links + // share one source page yet have 3 different OBJPAGE values, which is + // exactly what makes it unreliable (see ILink.OBJPAGE's doc comment and + // https://github.com/philips/supernote-typescript/issues/32). + const allLinks = Object.values(sn.links).flat() + const objPages = new Set(allLinks.map(l => l.OBJPAGE)) + expect(objPages.size).toBeGreaterThan(1) + }) }) describe("digest_image", () => {