@kosko-app/enex-core
v0.3.0
Published
Kosko's ENEX reader, ENML-to-Tiptap converter, fp1 Evernote note fingerprint and the note schema they need. One copy, shared by Kosko and kosko-desktop-assist.
Readme
@kosko-app/enex-core
The part of Kosko that has to give the same answer everywhere it runs:
convertEnml(enml, { schema, window })turns one Evernote note body (ENML) into Tiptap JSON. Nothing is summarised or rewritten. Whatever cannot be mapped is kept verbatim in arawHtmlnode, and the result reports any text it could not keep.fingerprintNote(note)/assignFingerprints(notes)compute thefp1key that identifies an Evernote note across exports, since ENEX carries no note id. Since 0.3.0 a note's version is computed over canonical ENML (canonicalEnml), so the same unchanged note has one version whether it came from an ENEX export or Evernote's MCP server.parseEvernoteNoteLink(href)(0.3.0) returns the note GUID anevernote:///view/…link names, ornull.NOTE_SCHEMA_EXTENSIONS/noteSchema()are the note schema the converter targets. A Tiptap schema is an allowlist: anything it does not name is deleted. So the converter and the editor must share one.
Kosko's web import and the desktop tool in this repository both use this package, pinned to one exact version, so the same note always becomes the same Kosko note and the same key.
Use
import { JSDOM } from 'jsdom';
import { convertEnml, noteSchema, fingerprintNote } from '@kosko-app/enex-core';
const { window } = new JSDOM('');
const result = convertEnml(enmlString, { schema: noteSchema(), window });
// { ok: true, doc, media, report }, or { ok: false, reason } when the ENML declares an entity (refused)
const { identity, version } = await fingerprintNote({ created, title, content: enmlString, tags, resources });Without the editor schema, @kosko-app/enex-core/enex reads ENEX (readEnex), formats dates (formatEnexDate), keys
notes (fingerprintNote, assignFingerprints) and, since 0.2.1, holds the MIME sets (STORABLE_MIME,
NOTE_UPLOADABLE_MIME, CONVERTED_MIME), and since 0.3.0 canonicalEnml and parseEvernoteNoteLink. It
needs no Tiptap peer.
window is always an argument. In a browser, pass the page's own window; in Node, pass any DOM
implementation's. The package never touches a global DOM, makes no network call and reads no environment.
Requirements
- Node 22 or a current browser (it uses
crypto.subtle). - Tiptap 3.30.5 as peer dependencies, installed by you:
@tiptap/core,@tiptap/pm,@tiptap/starter-kitand the extensions listed inpackage.json. They are peers, not dependencies, because ProseMirror compares node types by identity, and a second copy ofprosemirror-modelbreaks the schema. Check withnpm ls prosemirror-model, which should list one version. - The package has no runtime dependencies of its own.
What it deliberately leaves out
It holds no node views, no editor behaviour and no HTML sanitiser. Kosko's editor adds those itself. The
rawHtml node stores third-party HTML as an attribute and never renders it; showing it safely is the host's job.
Development
npm install # npm 11 or later: npm 10.9 crashes resolving vitest's peers
npm test # vitest; the package's own tests plus a boundary scan of its importsReleasing
Releases are published by .github/workflows/publish-enex-core.yml through npm's trusted publishing: no npm token
exists, and each release carries a provenance attestation tying it to the commit that built it.
- Bump
versioninpackage.jsonand commit. - Push a tag
enex-core-v<version>, e.g.git tag enex-core-v0.1.1 && git push origin enex-core-v0.1.1. - Approve the run in GitHub (the
npm-publishenvironment waits for the owner). - In each consumer (Kosko, the desktop tool), pin the new version exact and run its full suite.
Licensed under Apache-2.0.
