feat(koreader): one conversion route for every feature; bookmarks get web CFIs
Release / build-and-push (push) Successful in 2m14s

Progress, highlights, notes, and bookmarks entered position conversion
through three different doors: progress converted inline with an
uncached converter, annotations through the facade, bookmarks not at
all (the raw xpointer was stored verbatim, cfi_position stayed empty,
and the web drawer's goToBookmark silently no-ops on cfi-less entries).

Unify on the facade (ConvertToCanonical/ConvertFromCanonical):

- annotationEpub context resolved once per push: media item + EPUB path
  shared by every annotation instead of re-fetched per entry
- progress forward: the inline block becomes one facade call;
  non-reflowable formats pass through unchanged, and the cached
  converter stops re-parsing the book on every sync
- progress reverse: convertCFIToXPointer delegates to reverseConvertCFI,
  keeping the stored percentage in play for the fallback ladder
- bookmarks (bulk progress and /sync-bookmarks): pos0 resolves
  structural-only — bookmark text is a display label, never book text,
  so no context is supplied; webUsableCFI stores the result only for
  structural/exact epubcfi landings, discarding href/percentage results
  rather than storing dead drawer links. Also records percentage_location
  and origin_source on the legacy endpoint.
- percentages thread through: highlights/notes/bookmarks pass the device
  percentage or the derived section percentage instead of a hardcoded 0,
  so the last-resort fallback lands near the true position instead of
  the document start
- extendCFIByLength end-derivation now also fires on structural starts
  (it had silently stopped matching when the structural rung began
  landing starts with precision 'structural' rather than 'exact')

Tests: the drop-cap xpointer through the facade with empty context (the
bookmark scenario) must land structurally, not doc-start; webUsableCFI
table covers the store/discard gate.
This commit is contained in:
2026-09-09 09:04:23 -04:00
parent 5a6c361c11
commit e40530824e
3 changed files with 216 additions and 108 deletions
+21
View File
@@ -666,3 +666,24 @@ func TestReverseIgnoresSingleCharContext(t *testing.T) {
t.Errorf("single-char reverse context must fall back to percentage, got %s", reverse.Precision)
}
}
// The bookmark route supplies no context (bookmark text is a display
// label, never book text), so the facade must still resolve the drop-cap
// xpointer structurally instead of collapsing to the document start.
func TestConvertToCanonical_DropCapEmptyContext(t *testing.T) {
epubPath := writeDropCapEPUB(t)
xp := "/body/DocFragment[2]/body/p[3]/span[1]/text().0"
loc := ConvertToCanonical(LocatorSourceKOReader, xp, 0.52, "", string(FormatGroupReflowable), epubPath, "")
t.Logf("facade drop-cap (no context) → %s (%s)", loc.CFI, loc.Precision)
if loc.Precision != "structural" && loc.Precision != "exact" {
t.Fatalf("expected structural/exact precision, got %s (%s)", loc.Precision, loc.CFI)
}
if !strings.HasPrefix(loc.CFI, "epubcfi(") {
t.Fatalf("expected standard epubcfi, got %s", loc.CFI)
}
if strings.HasSuffix(loc.CFI, "/4/2/1:0)") {
t.Errorf("empty-context conversion collapsed to doc start: %s", loc.CFI)
}
}