Skip to content

Render clickable Supernote internal links in SupernoteView - #128

Merged
philips merged 4 commits into
mainfrom
clickable-links-supernoteview
Jul 28, 2026
Merged

Render clickable Supernote internal links in SupernoteView#128
philips merged 4 commits into
mainfrom
clickable-links-supernoteview

Conversation

@philips-clanker

@philips-clanker philips-clanker commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #127.

Summary

Renders each page's Supernote internal links (ILink/sn.links, parsed since #121's submodule bump) as a clickable overlay in SupernoteView:

  • Overlay rendering: LINKRECT regions are positioned/scaled using the exact same mechanism (drawPageImage()) that already keeps the pdf.js text layer aligned through zoom — a new .supernote-links-layer sits alongside the existing .textLayer, sized identically, carried along by the same CSS transform during a zoom drag.
  • Same-file links jump to the target page in place via the existing goToPage(), resolving PAGEID against the currently-loaded file's own pages.
  • Cross-file links resolve their target by basename against the vault, mirroring VaultWriter.resolvePageAnchor()'s export-time link resolution from Export Supernote keywords as tags and links as Wikilinks #122 (so a link resolves to the same note whether you're viewing it live or exporting it), then open it via a new leaf using the existing #page=N ephemeral-state anchor — the same mechanism a regular [[note#page=N]] link already uses — rather than inventing a new cross-view API.
  • If the target note isn't in the vault, shows a Notice rather than failing silently or (Obsidian's default openLinkText behavior) creating an empty note.

parseLinkRect/bucketLinksByPage live in a new Obsidian-free module (src/linkOverlay.ts) so they're unit-testable — main.ts extends several Obsidian base classes and has never had test coverage as a result. Includes a test that loads the submodule's own real link-tag fixture (nomad-3.26.40-link-tag-3p.note) and checks LINKRECT decodes to values within [0, pageWidth] x [0, pageHeight], concretely verifying the coordinate-space assumption the overlay math depends on instead of trusting it from a doc comment.

This is the first test in the repo that reads a real file from disk, which needed some plumbing: vitest.config.ts now aliases supernote-typescript to the submodule's compiled lib/ (mirroring tsconfig.json/esbuild.config.mjs), and a small src/node-shims.d.ts gives fs/path/import.meta.dirname narrow ambient types without pulling in all of @types/node — doing that project-wide collides with the DOM lib's Window.setTimeout typing that deviceFetch.ts/main.ts already rely on (window.setTimeout's return type goes ambiguous between DOM's number and Node's NodeJS.Timeout once both are in scope).

Scope decisions

  • SupernoteEmbed (the simpler, separate embed renderer) is out of scope — Clickable links in SupernoteView? #127 asks specifically about SupernoteView.
  • No modifier-click (Ctrl/Cmd) to open cross-file links in a new pane — no existing precedent for Keymap.isModEvent in this codebase; v1 always reuses the current-most-recent leaf.
  • Basename-based cross-file resolution is ambiguous if two vault notes share a basename in different folders — same known limitation Export Supernote keywords as tags and links as Wikilinks #122 already accepted for the export path.

Test plan

  • npx vitest run — 32 passed (including the 10 new linkOverlay.test.ts cases)
  • npx tsc --noEmit --skipLibCheck — clean
  • npx eslint src/main.ts src/linkOverlay.ts src/linkOverlay.test.ts src/node-shims.d.ts — clean
  • npm run build — clean
  • Manual test in Obsidian against real device link data — not possible in this environment (no Supernote hardware); the fixture-based test is the closest available verification. Would appreciate a real-device smoke test before merge.

Closes #127. Buckets each page's ILink entries (from the link parsing
merged in #121) and overlays a clickable, invisibly-positioned region
for each LINKRECT, scaled/repositioned by the same mechanism that
already keeps the pdf.js text layer aligned through zoom.

Click handling resolves same-file links via PAGEID against the
currently loaded pages, and cross-file links by basename against the
vault (mirroring VaultWriter.resolvePageAnchor's export-time
resolution from PR #122), reusing the existing #page=N ephemeral-state
anchor for the cross-file page jump rather than inventing a new one.

parseLinkRect/bucketLinksByPage live in a new obsidian-free module
(src/linkOverlay.ts) so they're unit-testable, including a real-fixture
test that verifies LINKRECT's coordinate space against actual parsed
data rather than trusting it from a doc comment. Also fixes the
vitest/eslint/tsc plumbing needed for a test to read a real .note file
from disk (the first test in this repo to need any Node builtins).
Testing against a real note with two internal links showed no links
rendering at all. Root cause was two bugs stacked:

- The LINKTYPE === '1' filter dropped both real links (their LINKTYPE
  was '0') — the doc comment claiming '1 = internal note link' doesn't
  hold up against real data.
- bucketLinksByPage used ILink.OBJPAGE for the source page, which
  looked reasonable but is wrong: verified by drawing each link's
  LINKRECT on every rendered page and measuring ink coverage under it,
  OBJPAGE (4 and 1) didn't match either link's actual page (array
  index 0 and 3) under any indexing convention. It tracks something
  else -- looks like the page's own template label, which had drifted
  from its current array position.

The original sn.links Record key's first-4-characters convention
(1-indexed page, taken from PR #122) was actually right; reverting to
it. It looked broken against the submodule's own link-tag fixture
only because all three of its links happen to share one page and one
key prefix -- confirmed by re-checking that fixture's exact expected
bucketing rather than just "some valid page index".
@philips-clanker

Copy link
Copy Markdown
Collaborator Author

Pushed a fix for a real bug found while testing against an actual note with two internal links: nothing rendered at all.

Root cause was two stacked mistakes in `bucketLinksByPage`:

  • Filtering to `LINKTYPE === '1'` dropped both real links in the test note (their `LINKTYPE` was `'0'`) — the doc comment claiming `1 = internal note link` doesn't hold up against real data.
  • Using `ILink.OBJPAGE` for the source page looked right but isn't — verified by drawing each link's `LINKRECT` onto every rendered page of the test note and measuring ink coverage under it. `OBJPAGE` (4 and 1) didn't match either link's actual page (array index 0 and 3) under any indexing convention; it tracks something else, closer to the page's own template label than its current array position.

Reverted to the original `sn.links` key-prefix convention (1-indexed page in the key's first 4 characters, from PR #122) — that turned out to be correct all along. It only looked broken against the submodule's own link-tag test fixture because all three of that fixture's links happen to share one page and one key prefix, which I mistook for evidence the key was meaningless. Re-verified with an exact expected-bucketing assertion against that fixture, not just "some valid page index" like before.

linkOverlay.test.ts is the first test to import supernote-typescript
at runtime; the test job never checked out or built the submodule
(only lint/build/test-submodule did), so it failed on CI with
"Cannot find package 'supernote-typescript'" even though it passed
locally. Mirrors the lint job's existing submodule checkout + build
steps.
Writes up what building the clickable link overlay found: LINKTYPE
and OBJPAGE don't behave the way their doc comments claim, verified
against real notes by rendering pages and measuring where each
LINKRECT's ink actually falls. Cross-references the upstream issue
(philips/supernote-typescript#32) filed with the same findings.
@philips
philips merged commit caecf9c into main Jul 28, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clickable links in SupernoteView?

2 participants