You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document describes the EPUB annotation system in Thorium Reader. It
focuses on application annotations for EPUB/WEBPUB content, not the newer
PDF annotation project. PDF is mentioned only where the shared annotation panel
or branching logic affects the EPUB path.
Scope
Covered:
EPUB annotation creation from a text selection.
Quick annotation and serial/advanced annotation modes.
Rendering highlights in the Readium navigator webviews.
Reader annotation panel display, filtering, sorting, editing, deletion, and navigation.
Readium Web Annotation export to .annotation.
HTML export of annotation sets.
Readium Web Annotation import from .annotation.
Persistence and renderer/main process synchronization.
Not covered in depth:
Bookmarks, except where they share the note/highlight infrastructure.
PDF annotations, except shared panel helper references.
Search highlights and TTS/media overlay highlights, except where they interact with annotation visibility.
For EPUB, triggerPdfAnnotation(..., triggerEpubAnnotation) delegates to readerLocalActionAnnotations.trigger.
annotationButtonTrigger() in the note saga validates:
TTS is stopped.
Media overlays are stopped.
A selection exists in __selectionInfoGlobal.locatorExtended.selectionInfo.
If annotation visibility is currently hidden, the saga forces annotation_defaultDrawView back to "annotation" and refreshes highlights
for the current href(s).
newLocatorEditAndSaveTheNote() decides whether to open the edit popover.
If the editor opens, readerLocalActionAnnotations.enableMode stores
annotation edit state in state.annotation.
Reader.tsx temporarily forces annotation_popoverNotOpenOnNoteTaking = true
for EPUB when using the quick annotation shortcut, then restores the previous
config value after triggering creation.
3. Use Serial/Advanced Annotation Mode
User story:
As a reader doing intensive markup, I can enable a mode where each new text
selection starts annotation creation automatically.
Code path:
The UI checkbox is labeled reader.annotations.advancedMode in AnnotationList.tsx.
ReaderMenu.tsx stores the mode in local React state named serialAnnotator.
An effect writes the value to window.__annotation_noteAutomaticallyCreatedOnNoteTakingAKASerialAnnotator.
The note saga setLocator() checks that window global each time a new
selection arrives.
If enabled, setLocator() calls newLocatorEditAndSaveTheNote() immediately.
Important behavior:
This mode is local UI state, not persisted reader config.
For EPUB, it uses a window global rather than a Redux action or selector.
For PDF, the same checkbox dispatches to the PDF event bus. That branch is
outside this document.
4. Render Existing EPUB Annotations
User story:
As a reader, I see my existing annotations when opening or navigating through
an EPUB.
Despite its location under pdf/, this helper is shared by the annotation
panel for both EPUB and PDF notes.
The helper creates a replacement note preserving:
uuid;
created;
index;
group;
cloned locatorExtended for EPUB;
cloned pdfAnnotation for PDF;
cloned creator.
The card dispatches readerActions.note.addUpdate(pubId, newNote, previousNote).
Important behavior:
readiumAnnotation is not preserved by buildAnnotationPanelSaveNote().
For EPUB, this means export selectors are expected to be regenerated from locatorExtended after the edited note is saved.
AnnotationEdit updates default annotation color/style config when the user
saves a changed color or draw type.
The comment text is trimmed before saving.
The panel renders comments as GitHub-flavored Markdown and sanitizes HTML with
DOMPurify.
7. Delete An Annotation
User story:
As a reader, I can delete one annotation or all annotations matching my current
filter.
Annotation body Markdown is rendered through marked.
Rendered HTML body values are sanitized with DOMPurify.
The file is downloaded as .html.
Customization:
The HTML template can be overridden by state.noteExport.
The export saga reads overrideHTMLTemplate and htmlContent before
rendering.
12. Import Readium .annotation
User story:
As a reader, I can import a Readium Annotation Set generated by Thorium or a
compatible tool, review conflicts, and add the annotations to my publication.
That function checks for readiumAnnotation.import.target.
It loads the target source document from the resource cache.
It calls convertSelectorTargetToLocatorExtended().
The converter attempts to match selectors in this order:
text quote selector;
text position selector;
CSS selector;
CFI selector or CFI fragment selector.
Matching ranges are normalized and converted into Thorium range info.
The saga dispatches readerActions.note.addUpdate with the generated locatorExtended.
Normal highlight rendering then applies.
13. Import Remediation To The EPUB Document
User story:
As a system, Thorium should make a best effort to re-anchor imported Readium
annotations to the current EPUB document, even when the external annotation
file uses slightly different resource hrefs or selector strategies.
There are two remediation phases today.
Phase 1: publication resource remediation.
This happens in the main process before the import confirmation dialog.
The incoming target.source is normalized to a Thorium spine href with resolveReadiumAnnotationSourceHref().
The resolver handles:
exact spine href matches;
backslash-to-slash normalization;
relative path normalization;
fragment removal;
percent-encoded vs decoded path differences;
unique suffix matches, for example when an external tool exports a longer
container or URL path.
If any incoming annotation source cannot be resolved to exactly one spine
item, the current importer rejects the whole annotation set with message.annotations.noBelongTo.
This phase mutates only the in-memory imported annotation object before it is
converted into an INoteState. It does not rewrite the .annotation file.
Phase 2: selector-to-locatorExtended remediation.
This happens later in the reader renderer after the user accepts the import.
Imported notes are first stored with readiumAnnotation.import.target and no locatorExtended.
noteUpdateLocatorExtendedFromImportSelector() loads the cached XML document
for target.source.
convertSelectorTargetToLocatorExtended() tries to recover a DOM range using
every selector type it understands:
TextQuoteSelector;
TextPositionSelector;
CssSelector, including refinements through makeRefinable;
CfiSelector;
EPUB CFI FragmentSelector.
Matched DOM ranges are normalized and converted to Thorium IRangeInfo.
An annotation stores the result as selectionInfo; a bookmark stores it as caretInfo.
The final internal anchor is a MiniLocatorExtended, which is what Thorium
uses for navigation and highlight rendering.
Current discard/failure behavior:
If a source href does not map to the current publication spine, the whole
import is rejected.
If an annotation has none of the supported selector types, that annotation is
skipped before the confirmation dialog.
If selector matching fails inside the document, no locatorExtended is
generated. The note can remain imported but unresolved, with the original readiumAnnotation.import.target still present.
Unresolved notes are not rendered as highlights and cannot navigate to EPUB
content.
There is no user-facing remediation report that distinguishes unresolved
source, unsupported selector, failed selector match, conflict, and already
imported cases.
Important limitation:
The remediation target is Thorium note state, not the EPUB package and not the
Readium annotation file. The EPUB content document is never modified. The .annotation file is read, normalized in memory, and converted into Thorium
notes.
Suggested remediation model:
Import every schema-valid annotation into either a resolved or unresolved
state instead of silently skipping selector-missing annotations.
Store an explicit import status, for example:
resolved;
source-not-in-publication;
unsupported-selector;
selector-not-found;
ambiguous-match.
Keep the original readiumAnnotation.import.target for future retry.
Auto-resolve only high-confidence matches:
exact/unique source href;
one matching DOM range;
matching quote and prefix/suffix where available.
When source href remediation fails, optionally search the whole spine by TextQuoteSelector and use ProgressionSelector as a disambiguation hint.
Show an import report before final acceptance, including unresolved counts
and reasons.
Allow later retry after the publication cache is loaded or after improved
matching rules are added.
14. Generate Readium Export Selectors
User story:
As a system, Thorium should export annotations with robust selectors so other
Readium-compatible tools can re-anchor them.
Code path:
Every readerActions.note.addUpdate schedules background work after a short
delay.
EPUB rendering and navigation use locatorExtended, not the Readium Web
Annotation selector set. The selector set is primarily an exchange/export
representation.
Selector Generation Is Lazy
The system does not generate Readium selectors during selection capture. It
first saves the Thorium note, then derives selectors from locatorExtended and
the cached XML document. This keeps creation responsive but introduces failure
modes if the resource cache is not ready.
Import Resolves In Two Steps
Import first creates notes from annotation JSON, then later resolves readiumAnnotation.import.target into locatorExtended inside the reader
renderer. This split keeps the main process independent from renderer DOM
matching APIs.
Known Issues And Bugs
Empty export selectors can become permanently cached.
noteUpdateExportSelectorFromLocatorExtended() only checks whether readiumAnnotation.export.selector is an array. If selector generation runs
while xmlDom or range conversion is unavailable, the selector array can be
saved as []. A later export will then contain an annotation with no target
selectors, and future selector generation will be skipped because [] still
satisfies Array.isArray(...).
Thorium generates readiumAnnotation.export.selector asynchronously from locatorExtended. The risk is that generation can fail but still save selector: [], and later the code treats that empty array as "selectors already generated."
So the tests should verify:
missing xmlDom: if the EPUB resource cache is not ready, selector generation should not save export.selector = [] as a successful result.
collapsed range: if the selected DOM range has no text/area, generation should fail cleanly, not cache empty selectors.
invalid range info: if locatorExtended.selectionInfo.rangeInfo cannot be converted back into a DOM Range, the note should remain retryable.
empty selector arrays: [] should mean "not successfully generated" or the code should use a separate status flag.
Import conflict replacement may not persist existing-note updates.
The importer pushes accepted conflict notes with readerActions.note.addUpdate(publicationIdentifier, note) and no previousNote. The renderer array reducer will replace an existing note with
the same uuid, but the main persistence saga treats missing previousNote
as an insert and calls sqliteTableNoteInsert(), which uses INSERT OR IGNORE. If the row already exists, SQLite may ignore the incoming
conflict replacement instead of updating the persisted note. This needs a
targeted reproduction test.
Import detects conflicts by UUID and modified date. The UI can choose importAll or importNoConflict. The concern is that conflict replacement may update the renderer state but fail to update SQLite because the main saga inserts without previousNote, and SQLite uses INSERT OR IGNORE.
So the tests should verify:
importAll with existing older note: incoming newer annotation should replace the old note in SQLite.
importAll with existing newer note: decide/verify whether incoming older annotation replaces it or is preserved as-is.
importNoConflict: conflicting annotations should not overwrite existing notes.
SQLite row content after import: after the import action finishes, read the stored note_json and confirm it matches the expected final note, not just Redux state.
Imported tags are not preserved.
Import currently sets tags: [fileName] and has a TODO about whether to use
the incoming annotation tag. This means body.tag from the .annotation
file is discarded.
Edited EPUB notes drop readiumAnnotation.
buildAnnotationPanelSaveNote() preserves locatorExtended but not readiumAnnotation. This is usually recovered by background selector
generation, but if that generation fails, the edited note may lose export
selectors.
Annotation quick/serial state uses non-Redux mutable globals.
The latest selection is stored in module-global __selectionInfoGlobal.
Serial mode is stored on window.__annotation_noteAutomaticallyCreatedOnNoteTakingAKASerialAnnotator.
These are pragmatic, but they are hard to inspect, test, replay, or sync.
noteTotalCount / bookmarkTotalCount naming is misleading.
Annotation creation uses reader.noteTotalCount.state and dispatches readerActions.bookmarkTotalCount, even for annotations. The field appears
to count notes generally, not only bookmarks. This increases cognitive load
and makes future changes risky.
annotation_defaultDrawView === "hide" is reset during reader store
migration.
createStoreFromDi() replaces a preloaded "hide" value with the default
value. That may be intentional to avoid opening a reader with invisible
notes, but it conflicts with the existence of a hide option that otherwise
looks like a persistent reader setting.
Export can include annotations without locatorExtended.
convertAnnotationStateToReadiumAnnotation() logs when locatorExtended is
missing but still returns a Readium annotation with an empty source and
selector array unless the note is a PDF annotation. EPUB imports can briefly
be in that unresolved state.
LCP exports omit TextQuoteSelector.
The selector generator skips text quote selectors for LCP publications. That
may be required for privacy or rights reasons, but it reduces re-anchoring
robustness and should be documented as product/security policy.
Import silently skips annotations without supported selectors.
The importer ignores incoming annotations that do not have CSS, text quote,
text position, CFI, or CFI fragment selectors. The final toast may say there
is "nothing" to import, but the user does not get per-item diagnostics.
Import remediation is incomplete and does not preserve unresolved
annotations as first-class recoverable items.
The current import path only remediates target.source to a spine href and
then tries to convert supported selectors into locatorExtended. If source
remediation fails, the whole annotation set is rejected. If selector
matching fails after import, the note can remain unresolved and therefore
cannot render or navigate. There is no explicit unresolved annotation state,
no retry workflow, and no user-facing report that separates source mismatch,
unsupported selector, selector-not-found, ambiguous match, conflict, and
already-imported cases.
Import selector candidate selection silently uses the last valid converted
range.
convertSelectorTargetToLocatorExtended() converts every matched DOM range
into IRangeInfo and ISelectedTextInfo, then keeps assigning rangeInfo/textInfo for each valid tuple. The last tuple with a start
container CSS selector and raw text wins. There is no explicit selector
priority, no ambiguity status, and no user-visible distinction between
selector-not-found and selector-disagreement cases. This is part of the
remediation work tracked in edrlab/thorium-reader#3508.
TODO
Add unit tests for selector generation failure modes:
missing xmlDom;
collapsed range;
invalid range info;
ensuring empty selector arrays do not mark a note as successfully exported.
Add import conflict persistence tests covering:
importAll with existing older note;
importAll with existing newer note;
importNoConflict;
SQLite row content after import.
Preserve or intentionally map incoming body.tag. See open question number 5.
Decide whether buildAnnotationPanelSaveNote() should preserve readiumAnnotation.import and/or readiumAnnotation.export for EPUB notes.
Replace __selectionInfoGlobal and the serial annotator window global with
explicit state/actions, or document why they must remain local mutable state.
Rename or wrap bookmarkTotalCount/noteTotalCount to reflect that it is
used by annotations as well as bookmarks.
Add an import report with counts for:
imported;
skipped because unsupported selector;
skipped because publication mismatch;
conflict newer;
conflict older;
already present.
Implement an unresolved-annotation remediation workflow for imports:
allow retry after resource cache readiness or improved matching logic;
decide whether unresolved annotations are visible in the panel and how
they should be edited, deleted, exported, or retried.
Guard reader startup rendering against unresolved imported notes.
readerStart() maps all stored notes and dereferences note.locatorExtended.locator while building highlight handler state in src/renderer/reader/redux/sagas/note.ts.
If import remediation starts preserving unresolved notes without locatorExtended, startup highlight creation must filter or separately
handle those notes before dereferencing the locator.
Guard annotation panel sorting against unresolved imported notes.
AnnotationList uses left.locatorExtended!.locator and right.locatorExtended!.locator while sorting EPUB annotations by
progression in src/renderer/reader/components/ReaderMenu/AnnotationList.tsx.
If unresolved imported annotations are preserved without locatorExtended,
the panel needs a stable sort and display fallback instead of assuming every
EPUB annotation has a locator.
Document the intended persistence semantics of annotation_defaultDrawView.
Move shared panel helpers that apply to both EPUB and PDF out of the src/renderer/reader/pdf folder, or add a clear comment explaining why the
file is shared.
Open Questions
Should Readium selector export be synchronous enough to guarantee non-empty
selectors before download, or should export compute missing selectors on
demand?
Should an annotation with no selectors be exportable at all?
What is the export policy for unresolved imported annotations?
Should imported conflict notes update existing SQLite rows when the user
chooses importAll?
Should imported annotations preserve the original tag, append the source file
name, or expose a user choice?
Should serial/advanced annotation mode be persisted per reader, per session,
or remain intentionally transient?
Should hide/margin display mode be shared between bookmarks and annotations,
or split into separate bookmark and annotation visibility preferences?
Should annotation comments support full Markdown, or a smaller sanitized
subset with explicit UX instructions?
Should the panel support multiple tags per annotation? The model allows tags?: string[], but much of the UI treats only the first tag as meaningful.
Should imported unresolved annotations be visible in the panel before locatorExtended has been generated? If yes, how should failed resolution
be communicated?
This document describes the EPUB annotation system in Thorium Reader. It
focuses on application annotations for EPUB/WEBPUB content, not the newer
PDF annotation project. PDF is mentioned only where the shared annotation panel
or branching logic affects the EPUB path.
Scope
Covered:
.annotation..annotation.Not covered in depth:
Key Concepts
EPUB annotations are stored as regular Thorium notes. The canonical model is
INoteStateinsrc/common/redux/states/renderer/note.ts.Important fields:
uuidreaderActions.note.addUpdatewhen absent.group"annotation"for annotations,"bookmark"for bookmarks.locatorExtendedtextualValuecolordrawTypetagscreatorcreated/modifiedreadiumAnnotation.export.selectorreadiumAnnotation.import.targetlocatorExtended.The Readium annotation exchange format is modeled in
src/common/readium/annotation/annotationModel.type.ts.Conversion helpers live in
src/common/readium/annotation/converter.ts.Runtime Boundaries
The EPUB annotation path crosses three runtime areas:
src/renderer/reader/**,src/renderer/common/redux/sagas/readiumAnnotation/export.tssrc/main/redux/sagas/note.ts,src/main/db/sqlite/note.tssrc/common/redux/states/renderer/note.ts,src/common/readium/annotation/**The EPUB renderer path uses Readium navigator APIs directly from the reader
renderer runtime. It does not use the PDF event bus.
High-Level Flow
flowchart TD A["User selects EPUB text"] --> B["Navigator reports readerActions.setLocator"] B --> C["note saga caches latest locatorExtended"] C --> D["User triggers annotation"] D --> E{"Quick annotation?"} E -->|No| F["Header AnnotationEdit popover"] E -->|Yes| G["Create note immediately"] F --> H["readerLocalActionAnnotations.createNote"] H --> G G --> I["readerActions.note.addUpdate"] I --> J["Reader reducer stores INoteState"] I --> K["Main saga persists note JSON in SQLite"] I --> L["note saga pushes highlight handler state"] L --> M["highlight mounter calls navigator highlightsCreate"] I --> N["Background selector generation for Readium export"]User Stories And Code Paths
1. Create An Annotation From Selected EPUB Text
User story:
Primary code path:
readerActions.setLocator.setLocator()insrc/renderer/reader/redux/sagas/note.tsstores the latest selected
MiniLocatorExtendedin the module-global__selectionInfoGlobal.triggerAnnotation()insrc/renderer/reader/components/ReaderHeader.tsx.triggerPdfAnnotation(..., triggerEpubAnnotation)delegates toreaderLocalActionAnnotations.trigger.annotationButtonTrigger()in the note saga validates:__selectionInfoGlobal.locatorExtended.selectionInfo.annotation_defaultDrawViewback to"annotation"and refreshes highlightsfor the current href(s).
newLocatorEditAndSaveTheNote()decides whether to open the edit popover.readerLocalActionAnnotations.enableModestoresannotation edit state in
state.annotation.ReaderHeaderrenders the sharedAnnotationEditpopover.
readerLocalActionAnnotations.createNote.createAnnotation()builds anINoteStateand dispatchesreaderActions.note.addUpdate.Created note shape:
Important behavior:
uuidis added byreaderActions.note.addUpdate.build()if missing.locatorExtendedis the primary EPUB anchor.readiumAnnotation.export.selectoris not generated synchronously duringcreation. It is computed in the background after the note is stored.
2. Create A Quick Annotation
User story:
Code path:
readerConfig.annotation_popoverNotOpenOnNoteTaking.falseinsrc/common/redux/states/reader.ts.ReaderMenu.tsx.newLocatorEditAndSaveTheNote()reads the flag from reader config.createAnnotation()directly with:annotation_defaultColor;annotation_defaultDrawType;Keyboard path:
AnnotationsCreateQuickis defined insrc/common/keyboard.ts.Reader.tsxtemporarily forcesannotation_popoverNotOpenOnNoteTaking = truefor EPUB when using the quick annotation shortcut, then restores the previous
config value after triggering creation.
3. Use Serial/Advanced Annotation Mode
User story:
Code path:
reader.annotations.advancedModeinAnnotationList.tsx.ReaderMenu.tsxstores the mode in local React state namedserialAnnotator.window.__annotation_noteAutomaticallyCreatedOnNoteTakingAKASerialAnnotator.setLocator()checks that window global each time a newselection arrives.
setLocator()callsnewLocatorEditAndSaveTheNote()immediately.Important behavior:
outside this document.
4. Render Existing EPUB Annotations
User story:
Startup path:
readerStart()insrc/renderer/reader/redux/sagas/note.tswaits for both:
readerActions.setLocator;winCommonActions.initSuccess.state.reader.note.IHighlightHandlerState.readerLocalActionHighlights.handler.popfor all note ids.readerLocalActionHighlights.handler.pushwith all convertedhighlight definitions.
Update path:
noteAddUpdate()receives everyreaderActions.note.addUpdate.highlight and pushes a new highlight definition.
Mounter path:
highlight/handler.tscalls
mountHighlight(href, handlerState)for the active href and optionalsecond webview href.
highlight/mounter.tscalls Readium navigator
highlightsCreate().state.reader.highlight.mounter.5. Navigate From The Annotation Panel To Content
User story:
Code path:
AnnotationList.tsx.AnnotationCard.tsx.AnnotationCardcallsgetAnnotationPanelNavigation(annotation).goToLocator(annotationPanelNavigation.locator, closeNavPanel).Reader.tsxroutes that locator through the existing navigator link handling.Important behavior:
6. Edit An Annotation
User story:
Code path:
AnnotationCardexposes an edit button whencanEditAnnotationInPanel(annotation)returns true.group === "annotation"notes.AnnotationEditin-place.buildAnnotationPanelSaveNote()frompdfAnnotationPanel.ts.pdf/, this helper is shared by the annotationpanel for both EPUB and PDF notes.
uuid;created;index;group;locatorExtendedfor EPUB;pdfAnnotationfor PDF;creator.readerActions.note.addUpdate(pubId, newNote, previousNote).Important behavior:
readiumAnnotationis not preserved bybuildAnnotationPanelSaveNote().For EPUB, this means export selectors are expected to be regenerated from
locatorExtendedafter the edited note is saved.AnnotationEditupdates default annotation color/style config when the usersaves a changed color or draw type.
DOMPurify.
7. Delete An Annotation
User story:
Single delete path:
AnnotationCarddispatchesreaderActions.note.remove.build(annotation).noteRemove()in the reader saga dispatchesreaderLocalActionHighlights.handler.pop.highlight/handler.tsunmounts navigator highlights for the current href(s).Bulk delete path:
AnnotationListcomputesdeletableAnnotationListFiltered.group === "annotation"notes.readerActions.note.removefor each note.8. Filter, Sort, And Page The Annotation List
User story:
Filter options in
AnnotationList:Sort options:
Pagination:
MAX_MATCHES_PER_PAGE = 5fromReaderMenu.tsx.Progression sort:
computeProgression()inReaderMenu.tsx.locator.locations.progression.compareAnnotationPanelProgression()so thesame panel can sort PDF annotations by page/geometry when needed.
9. Toggle Annotation Visibility And Margin Marks
User story:
Config:
annotation_defaultDrawView: "annotation" | "margin" | "hide"."annotation".Panel controls:
marginAnnotationsOnChange()toggles"annotation"<->"margin".hideAnnotationOnChange()toggles current mode <->"hide".readerActions.setConfig.readerLocalActionLocatorHrefChangedso highlights areunmounted/remounted for the current href(s).
Rendering effects:
mountHighlight()callshighlightsDrawMargin(["annotation", "bookmark"])when mode is
"margin"."annotation"mode it callshighlightsDrawMargin(["bookmark"]), soannotation highlights render normally while bookmarks keep margin handling.
"hide"mode, annotation/bookmark highlight definitions are filtered outbefore navigator highlight creation.
TTS/media overlay interaction:
Reader.tsxhides annotations before TTS or media overlays play.10. Export Readium
.annotationUser story:
Entry points:
AnnotationList.BookmarkList.CatalogMenu.tsx.Code path:
getSaga().run(exportAnnotationSet, notes, publicationView, title, "annotation").exportAnnotationSet()insrc/renderer/common/redux/sagas/readiumAnnotation/export.tsreads the locale and calls
convertAnnotationStateArrayToReadiumAnnotationSet().convertAnnotationStateToReadiumAnnotation()maps each note into a WebAnnotation item:
uuid->urn:uuid:{uuid};created/modified-> ISO strings;textualValue->body.value;body.tag;body.highlight;creator;locatorExtended.locator.href->target.source;target.meta;readiumAnnotation.export.selector->target.selector;motivation: "bookmarking";motivation: "highlighting".mimeTypes.annotation.Important behavior:
asynchronous and may have happened in a prior note update.
11. Export HTML
User story:
Code path:
exportAnnotationSet()saga, butfileType = "html".IReadiumAnnotationSet.convertReadiumAnnotationSetToHtml()renders the annotation set withMustache.
src/common/readium/annotation/htmlTemplate.ts.marked..html.Customization:
state.noteExport.overrideHTMLTemplateandhtmlContentbeforerendering.
12. Import Readium
.annotationUser story:
Entry points:
Main process path:
annotationActions.importAnnotationSet(publicationIdentifier, winId).importAnnotationSet()insrc/main/redux/sagas/note.tsopens an Electron file picker constrained to
.annotation.isIReadiumAnnotationSet().JSON.
target.sourcebelongs to a spine item.resolveReadiumAnnotationSourceHref()normalizes exact, decoded, relative,query/hash, and unique suffix matches.
INoteState.readiumAnnotation.import.target.ImportAnnotationsDialog.readerActions.note.addUpdate.Imported note shape:
Renderer resolution path:
locatorExtended.noteAddUpdate()schedulesnoteUpdateLocatorExtendedFromImportSelector().readiumAnnotation.import.target.convertSelectorTargetToLocatorExtended().readerActions.note.addUpdatewith the generatedlocatorExtended.13. Import Remediation To The EPUB Document
User story:
There are two remediation phases today.
Phase 1: publication resource remediation.
target.sourceis normalized to a Thorium spine href withresolveReadiumAnnotationSourceHref().container or URL path.
item, the current importer rejects the whole annotation set with
message.annotations.noBelongTo.converted into an
INoteState. It does not rewrite the.annotationfile.Phase 2: selector-to-
locatorExtendedremediation.readiumAnnotation.import.targetand nolocatorExtended.noteUpdateLocatorExtendedFromImportSelector()loads the cached XML documentfor
target.source.convertSelectorTargetToLocatorExtended()tries to recover a DOM range usingevery selector type it understands:
TextQuoteSelector;TextPositionSelector;CssSelector, including refinements throughmakeRefinable;CfiSelector;FragmentSelector.IRangeInfo.selectionInfo; a bookmark stores it ascaretInfo.MiniLocatorExtended, which is what Thoriumuses for navigation and highlight rendering.
Current discard/failure behavior:
import is rejected.
skipped before the confirmation dialog.
locatorExtendedisgenerated. The note can remain imported but unresolved, with the original
readiumAnnotation.import.targetstill present.content.
source, unsupported selector, failed selector match, conflict, and already
imported cases.
Important limitation:
The remediation target is Thorium note state, not the EPUB package and not the
Readium annotation file. The EPUB content document is never modified. The
.annotationfile is read, normalized in memory, and converted into Thoriumnotes.
Suggested remediation model:
state instead of silently skipping selector-missing annotations.
resolved;source-not-in-publication;unsupported-selector;selector-not-found;ambiguous-match.readiumAnnotation.import.targetfor future retry.TextQuoteSelectorand useProgressionSelectoras a disambiguation hint.and reasons.
matching rules are added.
14. Generate Readium Export Selectors
User story:
Code path:
readerActions.note.addUpdateschedules background work after a shortdelay.
noteUpdateExportSelectorFromLocatorExtended()checks:locatorExtended;readiumAnnotation.export.selectoras an array.readiumAnnotationSelectorFromNote().Generated selectors:
The note is then updated with:
Persistence And Synchronization
Renderer state:
state.reader.noteis an array reducer keyed byuuid.readerActions.note.addUpdatereplaces notes with matchinguuid.readerActions.note.removeremoves notes byuuid.Main persistence:
notes.pub_id;note_id;note_json;INSERT OR IGNORE.UPDATE notes SET note_json=? WHERE note_id=?.DELETE FROM notes WHERE note_id=?.Synchronization:
readerActions.note.addUpdateandreaderActions.note.remove.Feature Matrix
note.ts,ReaderHeader.tsx,AnnotationEdit.tsxnote.ts,Reader.tsx,ReaderMenu.tsxReaderMenu.tsx,note.tsnote.ts,highlight/handler.ts,highlight/mounter.tsReaderMenu.tsx,highlight/mounter.tsReaderMenu.tsx,highlight/mounter.ts,Reader.tsxAnnotationList.tsx,AnnotationCard.tsxAnnotationList.tsxAnnotationList.tsx,ReaderMenu.tsxAnnotationCard.tsx,Reader.tsxAnnotationCard.tsx,AnnotationEdit.tsx,pdfAnnotationPanel.tsAnnotationCard.tsx,AnnotationList.tsx,note.ts.annotationmain/redux/sagas/note.ts,ImportAnnotationsDialog.tsx.annotationreadiumAnnotation/export.ts,converter.tsreadiumAnnotation/export.ts,htmlTemplate.tsreadiumAnnotation/selector.tsmain/redux/sagas/note.ts,ImportAnnotationsDialog.tsxImportant Design Notes
locatorExtendedIs The Runtime AnchorEPUB rendering and navigation use
locatorExtended, not the Readium WebAnnotation selector set. The selector set is primarily an exchange/export
representation.
Selector Generation Is Lazy
The system does not generate Readium selectors during selection capture. It
first saves the Thorium note, then derives selectors from
locatorExtendedandthe cached XML document. This keeps creation responsive but introduces failure
modes if the resource cache is not ready.
Import Resolves In Two Steps
Import first creates notes from annotation JSON, then later resolves
readiumAnnotation.import.targetintolocatorExtendedinside the readerrenderer. This split keeps the main process independent from renderer DOM
matching APIs.
Known Issues And Bugs
Empty export selectors can become permanently cached.
noteUpdateExportSelectorFromLocatorExtended()only checks whetherreadiumAnnotation.export.selectoris an array. If selector generation runswhile
xmlDomor range conversion is unavailable, the selector array can besaved as
[]. A later export will then contain an annotation with no targetselectors, and future selector generation will be skipped because
[]stillsatisfies
Array.isArray(...).Thorium generates
readiumAnnotation.export.selectorasynchronously from locatorExtended. The risk is that generation can fail but still saveselector: [], and later the code treats that empty array as "selectors already generated."So the tests should verify:
xmlDom: if the EPUB resource cache is not ready, selector generation should not saveexport.selector = []as a successful result.locatorExtended.selectionInfo.rangeInfocannot be converted back into a DOM Range, the note should remain retryable.Import conflict replacement may not persist existing-note updates.
The importer pushes accepted conflict notes with
readerActions.note.addUpdate(publicationIdentifier, note)and nopreviousNote. The renderer array reducer will replace an existing note withthe same
uuid, but the main persistence saga treats missingpreviousNoteas an insert and calls
sqliteTableNoteInsert(), which usesINSERT OR IGNORE. If the row already exists, SQLite may ignore the incomingconflict replacement instead of updating the persisted note. This needs a
targeted reproduction test.
Import detects conflicts by UUID and modified date. The UI can choose importAll or importNoConflict. The concern is that conflict replacement may update the renderer state but fail to update SQLite because the main saga inserts without previousNote, and SQLite uses INSERT OR IGNORE.
So the tests should verify:
Imported tags are not preserved.
Import currently sets
tags: [fileName]and has a TODO about whether to usethe incoming annotation tag. This means
body.tagfrom the.annotationfile is discarded.
Edited EPUB notes drop
readiumAnnotation.buildAnnotationPanelSaveNote()preserveslocatorExtendedbut notreadiumAnnotation. This is usually recovered by background selectorgeneration, but if that generation fails, the edited note may lose export
selectors.
Annotation quick/serial state uses non-Redux mutable globals.
The latest selection is stored in module-global
__selectionInfoGlobal.Serial mode is stored on
window.__annotation_noteAutomaticallyCreatedOnNoteTakingAKASerialAnnotator.These are pragmatic, but they are hard to inspect, test, replay, or sync.
noteTotalCount/bookmarkTotalCountnaming is misleading.Annotation creation uses
reader.noteTotalCount.stateand dispatchesreaderActions.bookmarkTotalCount, even for annotations. The field appearsto count notes generally, not only bookmarks. This increases cognitive load
and makes future changes risky.
annotation_defaultDrawView === "hide"is reset during reader storemigration.
createStoreFromDi()replaces a preloaded"hide"value with the defaultvalue. That may be intentional to avoid opening a reader with invisible
notes, but it conflicts with the existence of a hide option that otherwise
looks like a persistent reader setting.
Export can include annotations without
locatorExtended.convertAnnotationStateToReadiumAnnotation()logs whenlocatorExtendedismissing but still returns a Readium annotation with an empty source and
selector array unless the note is a PDF annotation. EPUB imports can briefly
be in that unresolved state.
LCP exports omit
TextQuoteSelector.The selector generator skips text quote selectors for LCP publications. That
may be required for privacy or rights reasons, but it reduces re-anchoring
robustness and should be documented as product/security policy.
Import silently skips annotations without supported selectors.
The importer ignores incoming annotations that do not have CSS, text quote,
text position, CFI, or CFI fragment selectors. The final toast may say there
is "nothing" to import, but the user does not get per-item diagnostics.
Import remediation is incomplete and does not preserve unresolved
annotations as first-class recoverable items.
The current import path only remediates
target.sourceto a spine href andthen tries to convert supported selectors into
locatorExtended. If sourceremediation fails, the whole annotation set is rejected. If selector
matching fails after import, the note can remain unresolved and therefore
cannot render or navigate. There is no explicit unresolved annotation state,
no retry workflow, and no user-facing report that separates source mismatch,
unsupported selector, selector-not-found, ambiguous match, conflict, and
already-imported cases.
Import selector candidate selection silently uses the last valid converted
range.
convertSelectorTargetToLocatorExtended()converts every matched DOM rangeinto
IRangeInfoandISelectedTextInfo, then keeps assigningrangeInfo/textInfofor each valid tuple. The last tuple with a startcontainer CSS selector and raw text wins. There is no explicit selector
priority, no ambiguity status, and no user-visible distinction between
selector-not-found and selector-disagreement cases. This is part of the
remediation work tracked in
edrlab/thorium-reader#3508.
TODO
Add unit tests for selector generation failure modes:
xmlDom;Add import conflict persistence tests covering:
importAllwith existing older note;importAllwith existing newer note;importNoConflict;Preserve or intentionally map incoming
body.tag. See open question number 5.Decide whether
buildAnnotationPanelSaveNote()should preservereadiumAnnotation.importand/orreadiumAnnotation.exportfor EPUB notes.Replace
__selectionInfoGlobaland the serial annotator window global withexplicit state/actions, or document why they must remain local mutable state.
Rename or wrap
bookmarkTotalCount/noteTotalCountto reflect that it isused by annotations as well as bookmarks.
Add an import report with counts for:
Implement an unresolved-annotation remediation workflow for imports:
edrlab/thorium-reader#3508;
readiumAnnotation.import.targetfor retry;ambiguous match, conflict, and already-imported cases;
they should be edited, deleted, exported, or retried.
Guard reader startup rendering against unresolved imported notes.
readerStart()maps all stored notes and dereferencesnote.locatorExtended.locatorwhile building highlight handler state insrc/renderer/reader/redux/sagas/note.ts.If import remediation starts preserving unresolved notes without
locatorExtended, startup highlight creation must filter or separatelyhandle those notes before dereferencing the locator.
Guard annotation panel sorting against unresolved imported notes.
AnnotationListusesleft.locatorExtended!.locatorandright.locatorExtended!.locatorwhile sorting EPUB annotations byprogression in
src/renderer/reader/components/ReaderMenu/AnnotationList.tsx.If unresolved imported annotations are preserved without
locatorExtended,the panel needs a stable sort and display fallback instead of assuming every
EPUB annotation has a locator.
Document the intended persistence semantics of
annotation_defaultDrawView.Move shared panel helpers that apply to both EPUB and PDF out of the
src/renderer/reader/pdffolder, or add a clear comment explaining why thefile is shared.
Open Questions
Should Readium selector export be synchronous enough to guarantee non-empty
selectors before download, or should export compute missing selectors on
demand?
Should an annotation with no selectors be exportable at all?
What is the export policy for unresolved imported annotations?
Should imported conflict notes update existing SQLite rows when the user
chooses
importAll?Should imported annotations preserve the original tag, append the source file
name, or expose a user choice?
Should serial/advanced annotation mode be persisted per reader, per session,
or remain intentionally transient?
Should hide/margin display mode be shared between bookmarks and annotations,
or split into separate bookmark and annotation visibility preferences?
Should annotation comments support full Markdown, or a smaller sanitized
subset with explicit UX instructions?
Should the panel support multiple tags per annotation? The model allows
tags?: string[], but much of the UI treats only the first tag as meaningful.Should imported unresolved annotations be visible in the panel before
locatorExtendedhas been generated? If yes, how should failed resolutionbe communicated?