Export, import and W3C
Export
Section titled “Export”export() returns the document’s annotations as plain data, ready for JSON.stringify:
const data = penciled.export();// { version: 1, documentId: 'article-123', annotations: [...] }For example, a “Download my notes” button:
function download() { const blob = new Blob([JSON.stringify(penciled.export(), null, 2)], { type: 'application/json', }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `${penciled.documentId.replace(/\W+/g, '-')}-annotations.json`; link.click(); URL.revokeObjectURL(link.href);}Orphaned annotations are included, so nothing is lost.
Import
Section titled “Import”import() accepts what export() returns, or a bare array of annotations:
async function upload(file: File) { penciled.import(JSON.parse(await file.text()));}- By default, imported annotations are merged: new ones are added, and ones with the same
idas an existing annotation replace it. - With
{ mode: 'replace' }, every annotation that isn’t in the data is removed too.
penciled.import(data, { mode: 'replace' });Imported annotations are anchored against the current content, exactly like stored ones. Any that don’t fit become orphans instead of highlighting the wrong text, so importing data meant for a different page is harmless. Entries that aren’t valid annotations are skipped. Imports emit create and update events and are saved through the instance’s storage.
The documentId in the data is ignored: annotations are imported into the instance’s own document.
W3C Web Annotations
Section titled “W3C Web Annotations”The W3C Web Annotation Data Model is the standard format for exchanging annotations between tools. toW3C() and fromW3C() convert to and from it:
import { fromW3C, toW3C } from 'penciled';
const w3c = toW3C(penciled.getAll(), { source: 'https://example.com/article' });penciled.import(fromW3C(w3c));Each annotation becomes:
{ "@context": "http://www.w3.org/ns/anno.jsonld", "id": "urn:uuid:5f0c…", "type": "Annotation", "motivation": "commenting", "created": "2026-10-16T12:00:00.000Z", "modified": "2026-10-16T12:05:00.000Z", "body": { "type": "TextualBody", "value": "Worth rereading.", "purpose": "commenting", "format": "text/plain" }, "target": { "source": "https://example.com/article", "selector": [ { "type": "TextQuoteSelector", "exact": "A margin is an invitation", "prefix": "…", "suffix": "…" }, { "type": "TextPositionSelector", "start": 412, "end": 437 } ] }, "penciled": { "color": "yellow" }}motivationiscommentingwhen there is a note, andhighlightingotherwise. Without a note there is nobody.sourcedefaults to the current page’s URL.- The
penciledproperty keeps the color,editableandmeta, so a round trip loses nothing. Other tools ignore it. TextPositionSelectoroffsets are counted in penciled’s whitespace-collapsed text, so they may not match tools that count raw text. TheTextQuoteSelectoris what matters: penciled re-anchors from the quote and its context.
fromW3C() accepts one annotation or an array. Items without a TextQuoteSelector are skipped, and annotations without a penciled color get options.color, 'yellow' by default:
const annotations = fromW3C(annotationsFromAnotherTool, { color: 'blue' });penciled.import(annotations);Moving annotations between devices
Section titled “Moving annotations between devices”Combining these is enough for a simple sync without a backend of your own, for example through a file or a cloud drive:
// device Aconst json = JSON.stringify(penciled.export());
// device Bpenciled.import(JSON.parse(json)); // merge: keeps what's already hereFor continuous sync, use a storage adapter instead.