Skip to content

Missing text selection anchors for readium locators #231

Description

@ivan-ivanov-eden

Summary

When a user selects text in the web EPUB reader, the text_selected pipeline does not produce locators optimal for highlight/annotation persistence and cross-device sync.

Today the injectable sends only the selected string and viewport coordinates. The navigator then builds a minimal selection.locator with href, type, and text.highlight only. Consumers that store Readium-style annotation locators (matching kotlin-toolkit / Readium LCP annotation shape) end up with sparse data: no text.before / text.after, no locations.position / locations.totalProgression, and no character offset (target).

Motivation

Annotation locators are used to:

  1. Re-anchor highlights after layout or font changes (TextQuoteSelector via rangeFromLocator in injectables — needs before / highlight / after)
  2. Sync annotations between web and native readers (kotlin-toolkit already emits full locators)
  3. Disambiguate duplicate phrases on the same page (target + text context)

Examples

onPointUp(event: PointerEvent) {
const selection = this.wnd.getSelection();
if (!!selection && selection.toString()?.length > 0) {
const domRectList = selection.getRangeAt(0)?.getClientRects();
// Sanity check to avoid sending empty selections
if (!domRectList || domRectList.length === 0) {
return;
}
const rect = domRectList[0];
const textSelection = {
text: selection.toString(),
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
targetFrameSrc: this.wnd?.location?.href
}
this.comms.send("text_selected", textSelection as BasicTextSelection);

selection.locator = new Locator({ href, type: link!.type || "application/xhtml+xml", text: new LocatorText({ highlight: selection.text }) });

The code that generates the selected text event needs expanding with something like:

const range = selection.getRangeAt(0);
const root = document.body;

const beforeRange = range.cloneRange();
beforeRange.selectNodeContents(root);
beforeRange.setEnd(range.startContainer, range.startOffset);
const afterRange = range.cloneRange();
afterRange.selectNodeContents(root);
afterRange.setStart(range.endContainer, range.endOffset);

const CONTEXT_CHARS = 160;
const before = beforeRange.toString().replace(/\s+/g, " ").trim().slice(-CONTEXT_CHARS);
const after = afterRange.toString().replace(/\s+/g, " ").trim().slice(0, CONTEXT_CHARS);

import { TextRange } from "../vendor/hypothesis/anchoring/text-range.ts";
const target = TextRange.fromRange(range).relativeTo(root).start.offset;

Also update: types/src/modules/Peripherals.d.ts — add optional before, after, target on BasicTextSelection.

Current (EPUB):

selection.locator = new Locator({
  href,
  type: link!.type || "application/xhtml+xml",
  text: new LocatorText({ highlight: selection.text }),
});

After the changes:

  1. Build full LocatorText from injectable payload:

    new LocatorText({
      before: selection.before,
      highlight: selection.text,
      after: selection.after,
    })
  2. Merge this.currentLocation.locations (position, progression, totalProgression) into the new locator.

  3. Attach @readium/sharedLocator.target

  • kotlin-toolkit and several annotation stores use locator.target as a character offset in the resource. The web Locator class does not define or serialize this field today.
  • Consider adding optional target?: number to Locator (serialize/deserialize) so annotation consumers do not rely on Object.assign extensions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions