Skip to content

Export Supernote keywords as tags and links as Wikilinks - #122

Closed
philips-clanker wants to merge 1 commit into
bump-supernote-typescript-linksfrom
keywords-links-export
Closed

Export Supernote keywords as tags and links as Wikilinks#122
philips-clanker wants to merge 1 commit into
bump-supernote-typescript-linksfrom
keywords-links-export

Conversation

@philips-clanker

Copy link
Copy Markdown
Collaborator

Summary

Split out of #81 (by @Floper), stacked on #121 (submodule bump) since it needs ILink/sn.links and richer sn.keywords from supernote-typescript.

  • Keywords → tags: starred keywords are emitted as Obsidian tags (e.g. #My_Keyword) on the correct page, rather than as a flat list at the top of the document. Page is derived from the KEYWORD footer key's prefix (more reliable than KEYWORDPAGE, which can be '0'/invalid - see the parsing library's own test for this). Deduplicated against tags already present in that page's OCR text.
  • Links → Wikilinks: Supernote internal links are emitted as Wikilinks on the correct page, ordered top-to-bottom by sorting LINKO keys. The library already resolves same-document page anchors (ILink.text gets #Page N appended); this adds cross-file anchor resolution - loads the target .note file from the vault and looks up its PAGEID, with a per-call cache to avoid re-parsing the same target note more than once per export.
  • Both gated behind a new "Convert supernote keywords to tags and links to wikilinks" setting, on by default.

Ported onto the current main.ts/settings.ts shape, and built on the now-current supernote-typescript link/keyword API (#24) rather than #81's own version of that work, which never merged.

Test plan

  • npm run build - clean
  • npx eslint src/main.ts src/settings.ts - 0 errors
  • npx vitest run - 22 passed
  • Manual test in Obsidian against a .note file with keyword stars and internal links (e.g. the submodule's tests/input/nomad-3.26.40-link-tag-3p.note fixture), verifying tag placement/dedup and same-file/cross-file Wikilink anchors

Surfaces two supernote-typescript features (sn.keywords, sn.links -
see the submodule bump this stacks on) as Obsidian-native constructs
in exported markdown:

- Starred keywords become Obsidian tags (e.g. #My_Keyword) on the
  correct page, derived from the KEYWORD footer key's page prefix
  (more reliable than KEYWORDPAGE, which can be '0'/invalid) - rather
  than as a flat list at the top of the document. Deduplicated against
  tags already present in the page's OCR text.
- Supernote internal links become Wikilinks on the correct page,
  ordered top-to-bottom by sorting LINKO keys (the key encodes the Y
  coordinate after the page prefix). The library already resolves
  same-document page anchors (ILink.text gets "#Page N" appended); this
  adds cross-file anchor resolution by loading the target .note file
  from the vault and looking up its PAGEID, with a per-call cache to
  avoid re-parsing the same target multiple times.

Both gated behind a new "Convert supernote keywords to tags and links
to wikilinks" setting, on by default.

Split out of #81 (by @Floper), ported onto the current settings.ts
declarative API and current main.ts VaultWriter shape, and built on
the now-current supernote-typescript link/keyword parsing API rather
than #81's own (never-merged) version of it.
philips-clanker added a commit that referenced this pull request Jul 27, 2026
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".
pull Bot pushed a commit to ben-vargas/supernote-obsidian-plugin that referenced this pull request Jul 28, 2026
Closes philips#127. Buckets each page's ILink entries (from the link parsing
merged in philips#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 philips#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).
@philips philips closed this Jul 29, 2026
pull Bot pushed a commit to ben-vargas/supernote-obsidian-plugin that referenced this pull request Jul 29, 2026
…gins

Extends PageTextProcessorContext (registerPageTextProcessor's hook, see
ocr-plugins.md) with per-page fields an external companion plugin has
had no way to see until now: starred keywords, internal links, PAGEID,
orientation, and recognition status. Previously the hook only exposed
a flat OCR-text string and rasterized image bytes, which was enough to
reproduce the OCR-text-pattern half of philips#123 but not philips#122's genuine
keyword-star/link handling - see the discussion on philips#145.

sn.keywords/sn.links are grouped per page using the same
first-4-characters-of-the-Record-key convention supernote-typescript's
_parseLinks documents (more reliable than IKeyword.KEYWORDPAGE, which
can be '0'/invalid). Verified against a real device-created fixture
(supernote-typescript/tests/input/nomad-3.26.40-link-tag-3p.note).

Titles were considered too (same "device-native structured signal"
category as keywords/links) but dropped from this pass: ITitle has no
decoded text, only a bounding box + bitmap, so getting title text needs
correlating that box against page.recognitionElements' word boxes -
real new logic, not just wiring through an existing field.

Bumps the supernote-typescript submodule pointer to a branch (not yet
merged) that exports RecognitionStatuses from its index, needed to
type the new recognitionStatus field as that enum rather than a bare
string - see supernote-typescript#38.
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.

2 participants