Skip to content

EPUB Annotations Meta Issues #3726

Description

@panaC

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.

Key Concepts

EPUB annotations are stored as regular Thorium notes. The canonical model is
INoteState in
src/common/redux/states/renderer/note.ts.

Important fields:

Field Role
uuid Canonical note id, generated by readerActions.note.addUpdate when absent.
group "annotation" for annotations, "bookmark" for bookmarks.
locatorExtended EPUB locator and selection anchor used for navigation and highlight rendering.
textualValue User comment/note body. Markdown is accepted in the UI.
color Annotation color as RGB.
drawType Highlight style: solid background, underline, strikethrough, outline.
tags Currently effectively one user-visible tag in most UI paths.
creator Creator metadata stored from the global creator profile.
created / modified Epoch timestamps in milliseconds.
readiumAnnotation.export.selector Generated Readium Web Annotation selectors used during export.
readiumAnnotation.import.target Imported Readium target retained until it is converted into locatorExtended.

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:

Runtime area Responsibility Main files
Reader renderer Selection capture, annotation UI state, highlight mounting, reader panel, export. src/renderer/reader/**, src/renderer/common/redux/sagas/readiumAnnotation/export.ts
Main process File picker for import, validation, conflict analysis, SQLite persistence. src/main/redux/sagas/note.ts, src/main/db/sqlite/note.ts
Shared/common Note model, Readium annotation model, converters, source href normalization. src/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"]
Loading

User Stories And Code Paths

1. Create An Annotation From Selected EPUB Text

User story:

As a reader, I can select text in an EPUB and create an annotation with a
comment, color, style, and tag.

Primary code path:

  1. The navigator publishes the current reading location and selection through
    readerActions.setLocator.
  2. setLocator() in
    src/renderer/reader/redux/sagas/note.ts
    stores the latest selected MiniLocatorExtended in the module-global
    __selectionInfoGlobal.
  3. The header annotation button calls triggerAnnotation() in
    src/renderer/reader/components/ReaderHeader.tsx.
  4. For EPUB, triggerPdfAnnotation(..., triggerEpubAnnotation) delegates to
    readerLocalActionAnnotations.trigger.
  5. annotationButtonTrigger() in the note saga validates:
    • TTS is stopped.
    • Media overlays are stopped.
    • A selection exists in __selectionInfoGlobal.locatorExtended.selectionInfo.
  6. If annotation visibility is currently hidden, the saga forces
    annotation_defaultDrawView back to "annotation" and refreshes highlights
    for the current href(s).
  7. newLocatorEditAndSaveTheNote() decides whether to open the edit popover.
  8. If the editor opens, readerLocalActionAnnotations.enableMode stores
    annotation edit state in state.annotation.
  9. ReaderHeader renders the shared
    AnnotationEdit
    popover.
  10. Saving dispatches readerLocalActionAnnotations.createNote.
  11. createAnnotation() builds an INoteState and dispatches
    readerActions.note.addUpdate.

Created note shape:

{
    color,
    textualValue: comment,
    index: noteTotalCount + 1,
    locatorExtended: clone(locatorExtended),
    drawType: EDrawType[drawType] || EDrawType.solid_background,
    tags,
    creator: clone(creator),
    created: Date.now(),
    group: "annotation",
}

Important behavior:

  • uuid is added by readerActions.note.addUpdate.build() if missing.
  • locatorExtended is the primary EPUB anchor.
  • readiumAnnotation.export.selector is not generated synchronously during
    creation. It is computed in the background after the note is stored.

2. Create A Quick Annotation

User story:

As a reader, I can create an annotation immediately using my default color and
style, without opening the editor.

Code path:

  • The persistent config flag is
    readerConfig.annotation_popoverNotOpenOnNoteTaking.
  • The default value is false in
    src/common/redux/states/reader.ts.
  • The annotation panel option toggles it in
    ReaderMenu.tsx.
  • newLocatorEditAndSaveTheNote() reads the flag from reader config.
  • When the flag is true, it calls createAnnotation() directly with:
    • default color from annotation_defaultColor;
    • default draw type from annotation_defaultDrawType;
    • empty comment;
    • empty tag list.

Keyboard path:

  • AnnotationsCreateQuick is defined in
    src/common/keyboard.ts.
  • 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.

Startup path:

  1. readerStart() in
    src/renderer/reader/redux/sagas/note.ts
    waits for both:
    • readerActions.setLocator;
    • winCommonActions.initSuccess.
  2. It skips Divina, audio, and PDF readers.
  3. It reads the current notes from state.reader.note.
  4. It converts each note to IHighlightHandlerState.
  5. It dispatches readerLocalActionHighlights.handler.pop for all note ids.
  6. It dispatches readerLocalActionHighlights.handler.push with all converted
    highlight definitions.

Update path:

  1. noteAddUpdate() receives every readerActions.note.addUpdate.
  2. If the note is new or relevant visual fields changed, it pops any existing
    highlight and pushes a new highlight definition.
  3. Visual change detection includes:
    • no previous note;
    • previous note exists but no current highlight item;
    • color changed;
    • popup text changed;
    • draw type changed.

Mounter path:

  • highlight/handler.ts
    calls mountHighlight(href, handlerState) for the active href and optional
    second webview href.
  • highlight/mounter.ts
    calls Readium navigator highlightsCreate().
  • Mounted navigator highlight references are stored in
    state.reader.highlight.mounter.

5. Navigate From The Annotation Panel To Content

User story:

As a reader, I can click an annotation card and return to the annotated EPUB
location.

Code path:

  • The annotation panel is
    AnnotationList.tsx.
  • Each card is
    AnnotationCard.tsx.
  • AnnotationCard calls getAnnotationPanelNavigation(annotation).
  • For EPUB annotations, the helper returns:
{
    type: "epub",
    locator: annotation.locatorExtended.locator,
}
  • The card calls goToLocator(annotationPanelNavigation.locator, closeNavPanel).
  • Reader.tsx routes that locator through the existing navigator link handling.

Important behavior:

  • A normal click closes the panel in full modal mode.
  • Shift+Alt keeps the panel open.
  • Docked reader menu mode generally stays open.

6. Edit An Annotation

User story:

As a reader, I can change an annotation comment, color, style, and tag.

Code path:

  1. AnnotationCard exposes an edit button when
    canEditAnnotationInPanel(annotation) returns true.
  2. That helper currently allows all group === "annotation" notes.
  3. The card renders AnnotationEdit in-place.
  4. Saving calls buildAnnotationPanelSaveNote() from
    pdfAnnotationPanel.ts.
  5. Despite its location under pdf/, this helper is shared by the annotation
    panel for both EPUB and PDF notes.
  6. The helper creates a replacement note preserving:
    • uuid;
    • created;
    • index;
    • group;
    • cloned locatorExtended for EPUB;
    • cloned pdfAnnotation for PDF;
    • cloned creator.
  7. 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.

Single delete path:

  • AnnotationCard dispatches readerActions.note.remove.build(annotation).
  • noteRemove() in the reader saga dispatches
    readerLocalActionHighlights.handler.pop.
  • highlight/handler.ts unmounts navigator highlights for the current href(s).
  • The main process note saga deletes the SQLite row.

Bulk delete path:

  • AnnotationList computes deletableAnnotationListFiltered.
  • Deletable notes are currently all group === "annotation" notes.
  • The UI dispatches readerActions.note.remove for each note.

8. Filter, Sort, And Page The Annotation List

User story:

As a reader with many annotations, I can filter and sort the list to find a
relevant note.

Filter options in AnnotationList:

  • Tag.
  • Color.
  • Draw type.
  • Creator.

Sort options:

  • Progression.
  • Last created.
  • Last modified.

Pagination:

  • The panel uses MAX_MATCHES_PER_PAGE = 5 from ReaderMenu.tsx.
  • It maintains local page state and focuses the paginator after page changes.

Progression sort:

  • EPUB progression is computed by computeProgression() in ReaderMenu.tsx.
  • It uses the spine item index and locator.locations.progression.
  • Shared comparison goes through compareAnnotationPanelProgression() so the
    same panel can sort PDF annotations by page/geometry when needed.

9. Toggle Annotation Visibility And Margin Marks

User story:

As a reader, I can choose whether annotations appear inline, as margin marks,
or are hidden.

Config:

  • annotation_defaultDrawView: "annotation" | "margin" | "hide".
  • Default is "annotation".

Panel controls:

  • marginAnnotationsOnChange() toggles "annotation" <-> "margin".
  • hideAnnotationOnChange() toggles current mode <-> "hide".
  • Both dispatch readerActions.setConfig.
  • Both dispatch readerLocalActionLocatorHrefChanged so highlights are
    unmounted/remounted for the current href(s).

Rendering effects:

  • mountHighlight() calls highlightsDrawMargin(["annotation", "bookmark"])
    when mode is "margin".
  • In "annotation" mode it calls highlightsDrawMargin(["bookmark"]), so
    annotation highlights render normally while bookmarks keep margin handling.
  • In "hide" mode, annotation/bookmark highlight definitions are filtered out
    before navigator highlight creation.

TTS/media overlay interaction:

  • Reader.tsx hides annotations before TTS or media overlays play.
  • It stores the previous draw view in component state.
  • It restores the previous view after stop.

10. Export Readium .annotation

User story:

As a reader, I can export my EPUB annotations in the Readium Annotation Set
JSON format.

Entry points:

  • Reader annotation panel export button in AnnotationList.
  • Bookmark panel export path in BookmarkList.
  • Library publication menu export path in
    CatalogMenu.tsx.

Code path:

  1. UI calls getSaga().run(exportAnnotationSet, notes, publicationView, title, "annotation").
  2. exportAnnotationSet() in
    src/renderer/common/redux/sagas/readiumAnnotation/export.ts
    reads the locale and calls convertAnnotationStateArrayToReadiumAnnotationSet().
  3. convertAnnotationStateToReadiumAnnotation() maps each note into a Web
    Annotation item:
    • uuid -> urn:uuid:{uuid};
    • created / modified -> ISO strings;
    • textualValue -> body.value;
    • RGB color -> Readium note color name;
    • first tag -> body.tag;
    • draw type -> body.highlight;
    • creator -> creator;
    • locatorExtended.locator.href -> target.source;
    • headings/page metadata -> target.meta;
    • readiumAnnotation.export.selector -> target.selector;
    • bookmark draw type -> motivation: "bookmarking";
    • annotation draw types -> motivation: "highlighting".
  4. The JSON is serialized with sorted keys.
  5. A browser download is started with MIME type mimeTypes.annotation.

Important behavior:

  • PDF notes are skipped explicitly during Readium export.
  • Export assumes selectors already exist or can be empty. Selector generation is
    asynchronous and may have happened in a prior note update.

11. Export HTML

User story:

As a reader, I can export annotations as a readable HTML document.

Code path:

  1. Same exportAnnotationSet() saga, but fileType = "html".
  2. It first builds the same IReadiumAnnotationSet.
  3. convertReadiumAnnotationSetToHtml() renders the annotation set with
    Mustache.
  4. The default template is
    src/common/readium/annotation/htmlTemplate.ts.
  5. Annotation body Markdown is rendered through marked.
  6. Rendered HTML body values are sanitized with DOMPurify.
  7. 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.

Entry points:

  • Reader annotation panel import button.
  • Bookmark panel import button.
  • Library publication menu import button.

Main process path:

  1. UI dispatches annotationActions.importAnnotationSet(publicationIdentifier, winId).
  2. Renderer sync middleware forwards the action to the main process.
  3. importAnnotationSet() in
    src/main/redux/sagas/note.ts
    opens an Electron file picker constrained to .annotation.
  4. It reads the file and parses JSON.
  5. It validates with isIReadiumAnnotationSet().
  6. It loads the target publication and deserializes its Readium publication
    JSON.
  7. It verifies every incoming target.source belongs to a spine item.
  8. resolveReadiumAnnotationSourceHref() normalizes exact, decoded, relative,
    query/hash, and unique suffix matches.
  9. Each incoming annotation is converted to an INoteState.
  10. The imported Readium target is stored under
    readiumAnnotation.import.target.
  11. The importer groups parsed notes into:
    • no conflict;
    • conflict where incoming is newer;
    • conflict where incoming is older.
  12. It opens ImportAnnotationsDialog.
  13. User chooses abort, import all, or import without conflicts.
  14. Accepted notes are pushed through readerActions.note.addUpdate.

Imported note shape:

{
    uuid,
    index: -1,
    textualValue: incomingAnnotation.body?.value,
    color,
    drawType,
    tags: [fileName],
    modified,
    created,
    creator,
    group: motivation === "bookmarking" ? "bookmark" : "annotation",
    readiumAnnotation: {
        import: { target: incomingAnnotation.target },
    },
}

Renderer resolution path:

  1. The imported note initially has no locatorExtended.
  2. noteAddUpdate() schedules noteUpdateLocatorExtendedFromImportSelector().
  3. That function checks for readiumAnnotation.import.target.
  4. It loads the target source document from the resource cache.
  5. It calls convertSelectorTargetToLocatorExtended().
  6. The converter attempts to match selectors in this order:
    • text quote selector;
    • text position selector;
    • CSS selector;
    • CFI selector or CFI fragment selector.
  7. Matching ranges are normalized and converted into Thorium range info.
  8. The saga dispatches readerActions.note.addUpdate with the generated
    locatorExtended.
  9. 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:

  1. Import every schema-valid annotation into either a resolved or unresolved
    state instead of silently skipping selector-missing annotations.
  2. Store an explicit import status, for example:
    • resolved;
    • source-not-in-publication;
    • unsupported-selector;
    • selector-not-found;
    • ambiguous-match.
  3. Keep the original readiumAnnotation.import.target for future retry.
  4. Auto-resolve only high-confidence matches:
    • exact/unique source href;
    • one matching DOM range;
    • matching quote and prefix/suffix where available.
  5. When source href remediation fails, optionally search the whole spine by
    TextQuoteSelector and use ProgressionSelector as a disambiguation hint.
  6. Show an import report before final acceptance, including unresolved counts
    and reasons.
  7. 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:

  1. Every readerActions.note.addUpdate schedules background work after a short
    delay.
  2. noteUpdateExportSelectorFromLocatorExtended() checks:
    • reader is locked/ready;
    • note has locatorExtended;
    • note does not already have readiumAnnotation.export.selector as an array.
  3. It gets the current publication and source href.
  4. It loads the cached XML document for the href.
  5. It calls readiumAnnotationSelectorFromNote().

Generated selectors:

  • CSS selector refined by text position.
  • Text position selector.
  • Text quote selector, except for LCP publications.
  • Progression selector.
  • EPUB CFI fragment selector with OPF spine path.
  • CFI selector without the OPF prefix.

The note is then updated with:

readiumAnnotation: {
    ...note.readiumAnnotation,
    export: { selector },
}

Persistence And Synchronization

Renderer state:

  • state.reader.note is an array reducer keyed by uuid.
  • readerActions.note.addUpdate replaces notes with matching uuid.
  • readerActions.note.remove removes notes by uuid.

Main persistence:

  • Notes are stored in SQLite table notes.
  • Schema fields:
    • pub_id;
    • note_id;
    • note_json;
    • timestamps.
  • Insert uses INSERT OR IGNORE.
  • Update uses UPDATE notes SET note_json=? WHERE note_id=?.
  • Delete uses DELETE FROM notes WHERE note_id=?.
  • Publication deletion removes all notes for that publication.

Synchronization:

  • Reader sync middleware includes readerActions.note.addUpdate and
    readerActions.note.remove.
  • Import actions are synchronized from reader/library renderer to main.
  • Main receives note actions and persists them.

Feature Matrix

Feature EPUB support Main code
Create from selection Yes note.ts, ReaderHeader.tsx, AnnotationEdit.tsx
Quick create Yes note.ts, Reader.tsx, ReaderMenu.tsx
Serial/advanced mode Yes ReaderMenu.tsx, note.ts
Inline highlight rendering Yes note.ts, highlight/handler.ts, highlight/mounter.ts
Margin marks Yes ReaderMenu.tsx, highlight/mounter.ts
Hide annotations Yes ReaderMenu.tsx, highlight/mounter.ts, Reader.tsx
Panel list Yes AnnotationList.tsx, AnnotationCard.tsx
Filter Yes AnnotationList.tsx
Sort Yes AnnotationList.tsx, ReaderMenu.tsx
Navigate to annotation Yes AnnotationCard.tsx, Reader.tsx
Edit annotation Yes AnnotationCard.tsx, AnnotationEdit.tsx, pdfAnnotationPanel.ts
Delete annotation Yes AnnotationCard.tsx, AnnotationList.tsx, note.ts
Import .annotation Yes main/redux/sagas/note.ts, ImportAnnotationsDialog.tsx
Export .annotation Yes readiumAnnotation/export.ts, converter.ts
Export HTML Yes readiumAnnotation/export.ts, htmlTemplate.ts
Readium selector generation Yes readiumAnnotation/selector.ts
Import conflict modal Yes main/redux/sagas/note.ts, ImportAnnotationsDialog.tsx

Important Design Notes

locatorExtended Is The Runtime Anchor

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

  1. 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.
  2. 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.
  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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.

  11. 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.

  12. 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

  1. 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.
  2. Add import conflict persistence tests covering:

    • importAll with existing older note;
    • importAll with existing newer note;
    • importNoConflict;
    • SQLite row content after import.
  3. Preserve or intentionally map incoming body.tag. See open question number 5.

  4. Decide whether buildAnnotationPanelSaveNote() should preserve
    readiumAnnotation.import and/or readiumAnnotation.export for EPUB notes.

  5. Replace __selectionInfoGlobal and the serial annotator window global with
    explicit state/actions, or document why they must remain local mutable state.

  6. Rename or wrap bookmarkTotalCount/noteTotalCount to reflect that it is
    used by annotations as well as bookmarks.

  7. Add an import report with counts for:

    • imported;
    • skipped because unsupported selector;
    • skipped because publication mismatch;
    • conflict newer;
    • conflict older;
    • already present.
  8. Implement an unresolved-annotation remediation workflow for imports:

    • track implementation and decisions in
      edrlab/thorium-reader#3508;
    • preserve schema-valid annotations even when they cannot be anchored yet;
    • store an explicit import/remediation status and reason;
    • keep the original readiumAnnotation.import.target for retry;
    • distinguish source mismatch, unsupported selector, selector-not-found,
      ambiguous match, conflict, and already-imported cases;
    • 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.
  9. 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.

  10. 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.

  1. Document the intended persistence semantics of annotation_defaultDrawView.

  2. 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

  1. Should Readium selector export be synchronous enough to guarantee non-empty
    selectors before download, or should export compute missing selectors on
    demand?

  2. Should an annotation with no selectors be exportable at all?

  3. What is the export policy for unresolved imported annotations?

  4. Should imported conflict notes update existing SQLite rows when the user
    chooses importAll?

  5. Should imported annotations preserve the original tag, append the source file
    name, or expose a user choice?

  6. Should serial/advanced annotation mode be persisted per reader, per session,
    or remain intentionally transient?

  7. Should hide/margin display mode be shared between bookmarks and annotations,
    or split into separate bookmark and annotation visibility preferences?

  8. Should annotation comments support full Markdown, or a smaller sanitized
    subset with explicit UX instructions?

  9. 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.

  10. Should imported unresolved annotations be visible in the panel before
    locatorExtended has been generated? If yes, how should failed resolution
    be communicated?

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions