fix(sync): anchor device highlight ranges to selection text
KOReader highlights converted to wrong or unresolvable web CFIs whenever a selection crossed text-node boundaries. Three compounding defects: ParseCREXPointer discarded the text()[N] node index and applied .offset as a global block offset; the end anchor converted with an empty context (blind structural landing, zero verification); and the block-containment check accepted wrong-node landings because a selection's text always occurs somewhere in its own block. pos0 and pos1 were converted independently, yielding mismatched start/end base paths and backwards ranges - highlights stored but never painted, or painted at the wrong location, in the web reader. Add ConvertHighlightRange: both endpoints are anchored by locating the selection text in the flattened document text (whitespace-normalized, crossing block boundaries), with the device locators demoted to disambiguation hints. A single document match yields a same-spine, ordered start/end pair by construction. Falls back to the legacy per-endpoint path when the text cannot be located, so behavior is never worse than before. Also fix the shared flatten stream: zero-width format characters (ZWSP/ZWNJ/BOM) and whitespace-only inline spans (<span> </span>) now act as word separators instead of fusing adjacent words, which prevented text matching in real books. Verified against 28 real device highlights from production: converter output now matches the text-search oracle on all of them (previously 8/28 correct, 18 with mismatched base paths, 2 backwards).
This commit is contained in:
1 parent
cbdddc095e
commit
bb3aef517c
4 files changed
+227
-5
No files matched your search
@@ -626,7 +626,12 @@ func flattenDocumentForSearch(body *html.Node) ([]rune, []docRunePos) {
|
||||
lastBlock = block
|
||||
off := 0
|
||||
for _, r := range n.Data {
|
||||
if unicode.IsSpace(r) {
|
||||
// Zero-width format characters are line-break opportunities,
|
||||
// not word characters: devices render them as breaks and
|
||||
// capture plain spaces, so they must not fuse words in the
|
||||
// search stream. ZWJ (U+200D) is exempt — it glues emoji
|
||||
// sequences.
|
||||
if unicode.IsSpace(r) || r == '\u200b' || r == '\u200c' || r == '\ufeff' {
|
||||
pendingSpace = true
|
||||
} else {
|
||||
if pendingSpace && len(runes) > 0 {
|
||||
@@ -644,9 +649,12 @@ func flattenDocumentForSearch(body *html.Node) ([]rune, []docRunePos) {
|
||||
var walk func(n *html.Node)
|
||||
walk = func(n *html.Node) {
|
||||
if n.Type == html.TextNode {
|
||||
if strings.TrimSpace(n.Data) != "" {
|
||||
appendNode(n)
|
||||
}
|
||||
// Whitespace-only nodes still participate: a space carried in
|
||||
// its own inline span (<span class="sp"> </span>) is a real
|
||||
// word separator, and skipping it fuses adjacent words in the
|
||||
// search stream. appendNode turns such nodes into pending
|
||||
// space without emitting anything.
|
||||
appendNode(n)
|
||||
return
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
|
||||
Reference in new issue
Block a user