Deadcode-verified removals (RTA reachability from main, cross-checked with -test and textual xrefs): Go server: - whole files: middleware/transaction.go, middleware/error_handler.go, middleware/password_validator.go (live registration machinery kept), handlers/sync.go, sync/offline.go - unrouted endpoints: auth UpdateUsername/UpdateEmail, timezone settings handler, OPDS RegisterOPDS, Kobo LibrarySync handler, ReaderHandler.ShowReader, JSON-data twins on collections/library - duplicate book-matching handlers (book_matching.go Handler twins, matching.go MatchingHandler twins), superseded helpers (router.parseUUID, handlers.ValidateDeviceToken, normalizePath, userExists, generateLockID) Templ/client: - never-routed pages: api_explorer.templ, queue.templ and their client modules api-explorer.ts, api-explorer-docs.ts, queue.ts - fully orphaned wrapper modules events.ts, dom.ts - broken clearSyncQueue (called a nonexistent endpoint) and the hidden, never-populated sync-queue section on the devices page - 166+ unused exports demoted to private (knip-verified); ~85 truly dead declarations removed via tsc noUnusedLocals fixpoint - stale 'interactive API Explorer' claims dropped from docs The docs drift-gate package (internal/docs), templ-bound modules (dashboard, stats, header, index, login, profile*, bookshelf, collections, toast-error), sqlc-generated code, and web/static are intentionally untouched.
270 lines
7.5 KiB
Go
270 lines
7.5 KiB
Go
package sync
|
|
|
|
import (
|
|
"log"
|
|
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
type KEPUBCFIConverter struct {
|
|
epubConverter *CFIConverter
|
|
kepubConverter *CFIConverter
|
|
}
|
|
|
|
type KEPUBConversionResult struct {
|
|
CFI string
|
|
ExtractedContext string
|
|
Percentage float64
|
|
Precision string
|
|
}
|
|
|
|
func NewKEPUBCFIConverter(epubPath, kepubPath string) *KEPUBCFIConverter {
|
|
return &KEPUBCFIConverter{
|
|
epubConverter: NewCFIConverter(epubPath),
|
|
kepubConverter: NewCFIConverter(kepubPath),
|
|
}
|
|
}
|
|
|
|
func (k *KEPUBCFIConverter) ConvertKEPUBCFIToStandard(kepubCFI string, percentage float64, contextText string) (*KEPUBConversionResult, error) {
|
|
if !IsStandardEPUBCFI(kepubCFI) {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
spineIndex, steps, err := parseEPUBCFI(kepubCFI)
|
|
if err != nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
kepubDoc, _, docErr := k.kepubConverter.getContentDoc(spineIndex + 1)
|
|
if docErr != nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
kepubTextNode, kepubOffset, resolveErr := resolveCFIToNode(kepubDoc, steps)
|
|
if resolveErr != nil {
|
|
return k.kepubToStandardByPercentage(spineIndex, percentage)
|
|
}
|
|
|
|
var searchText string
|
|
if contextText != "" {
|
|
searchText = normalizeWhitespace(contextText)
|
|
} else if kepubTextNode != nil && kepubTextNode.Type == html.TextNode {
|
|
searchText = extractSurroundingText(kepubTextNode, kepubOffset, 80)
|
|
}
|
|
|
|
if searchText != "" {
|
|
epubDoc, _, docErr := k.epubConverter.getContentDoc(spineIndex + 1)
|
|
if docErr == nil {
|
|
epubBody := findBody(epubDoc)
|
|
if epubBody != nil {
|
|
matchNode, matchOffset := findTextInNode(epubBody, searchText)
|
|
if matchNode != nil {
|
|
cfi, buildErr := buildCFI(spineIndex, matchNode, matchOffset)
|
|
if buildErr == nil && cfi != "" {
|
|
return &KEPUBConversionResult{
|
|
CFI: cfi,
|
|
ExtractedContext: searchText,
|
|
Percentage: percentage,
|
|
Precision: "exact",
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result, _ := k.kepubToStandardByPercentage(spineIndex, percentage)
|
|
result.ExtractedContext = searchText
|
|
return result, nil
|
|
}
|
|
|
|
func (k *KEPUBCFIConverter) ConvertStandardCFIToKEPUB(standardCFI string, percentage float64, contextText string) (*KEPUBConversionResult, error) {
|
|
if !IsStandardEPUBCFI(standardCFI) {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
spineIndex, steps, err := parseEPUBCFI(standardCFI)
|
|
if err != nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
epubDoc, _, docErr := k.epubConverter.getContentDoc(spineIndex + 1)
|
|
if docErr != nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
epubTextNode, epubOffset, resolveErr := resolveCFIToNode(epubDoc, steps)
|
|
if resolveErr != nil {
|
|
return k.standardToKEPUBByPercentage(spineIndex, percentage)
|
|
}
|
|
|
|
var searchText string
|
|
if contextText != "" {
|
|
searchText = normalizeWhitespace(contextText)
|
|
} else if epubTextNode != nil && epubTextNode.Type == html.TextNode {
|
|
searchText = extractSurroundingText(epubTextNode, epubOffset, 80)
|
|
}
|
|
|
|
if searchText != "" {
|
|
kepubDoc, _, docErr := k.kepubConverter.getContentDoc(spineIndex + 1)
|
|
if docErr == nil {
|
|
kepubBody := findBody(kepubDoc)
|
|
if kepubBody != nil {
|
|
matchNode, matchOffset := findTextInNode(kepubBody, searchText)
|
|
if matchNode != nil {
|
|
cfi, buildErr := buildCFI(spineIndex, matchNode, matchOffset)
|
|
if buildErr == nil && cfi != "" {
|
|
return &KEPUBConversionResult{
|
|
CFI: cfi,
|
|
ExtractedContext: searchText,
|
|
Percentage: percentage,
|
|
Precision: "exact",
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result, _ := k.standardToKEPUBByPercentage(spineIndex, percentage)
|
|
result.ExtractedContext = searchText
|
|
return result, nil
|
|
}
|
|
|
|
func (k *KEPUBCFIConverter) kepubToStandardByPercentage(spineIndex int, percentage float64) (*KEPUBConversionResult, error) {
|
|
if percentage <= 0 {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
epubDoc, _, docErr := k.epubConverter.getContentDoc(spineIndex + 1)
|
|
if docErr != nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
epubBody := findBody(epubDoc)
|
|
if epubBody == nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
totalChars := countTextChars(epubBody)
|
|
if totalChars <= 0 {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
targetOffset := int(float64(totalChars) * percentage)
|
|
targetNode, foundOffset := findNodeAtCharOffset(epubBody, targetOffset)
|
|
if targetNode == nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
charInNode := targetOffset - foundOffset
|
|
cfi, err := buildCFI(spineIndex, targetNode, charInNode)
|
|
if err != nil || cfi == "" {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
return &KEPUBConversionResult{
|
|
CFI: cfi,
|
|
Percentage: percentage,
|
|
Precision: "percentage",
|
|
}, nil
|
|
}
|
|
|
|
func (k *KEPUBCFIConverter) standardToKEPUBByPercentage(spineIndex int, percentage float64) (*KEPUBConversionResult, error) {
|
|
if percentage <= 0 {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
kepubDoc, _, docErr := k.kepubConverter.getContentDoc(spineIndex + 1)
|
|
if docErr != nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
kepubBody := findBody(kepubDoc)
|
|
if kepubBody == nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
totalChars := countTextChars(kepubBody)
|
|
if totalChars <= 0 {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
targetOffset := int(float64(totalChars) * percentage)
|
|
targetNode, foundOffset := findNodeAtCharOffset(kepubBody, targetOffset)
|
|
if targetNode == nil {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
charInNode := targetOffset - foundOffset
|
|
cfi, err := buildCFI(spineIndex, targetNode, charInNode)
|
|
if err != nil || cfi == "" {
|
|
return &KEPUBConversionResult{Percentage: percentage, Precision: "percentage"}, nil
|
|
}
|
|
|
|
return &KEPUBConversionResult{
|
|
CFI: cfi,
|
|
Percentage: percentage,
|
|
Precision: "percentage",
|
|
}, nil
|
|
}
|
|
|
|
func extractSurroundingText(textNode *html.Node, offset int, window int) string {
|
|
if textNode == nil || textNode.Type != html.TextNode {
|
|
return ""
|
|
}
|
|
|
|
block := findBlockParent(textNode)
|
|
if block == nil {
|
|
text := textNode.Data
|
|
runes := []rune(text)
|
|
start := offset - window
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
end := offset + window
|
|
if end > len(runes) {
|
|
end = len(runes)
|
|
}
|
|
if start >= end {
|
|
return normalizeWhitespace(text)
|
|
}
|
|
return normalizeWhitespace(string(runes[start:end]))
|
|
}
|
|
|
|
segments := collectInlineText(block)
|
|
|
|
globalOffset := 0
|
|
for _, seg := range segments {
|
|
if seg.node == textNode {
|
|
globalOffset += offset
|
|
break
|
|
}
|
|
globalOffset += len(seg.runes)
|
|
}
|
|
|
|
var allRunes []rune
|
|
for _, seg := range segments {
|
|
allRunes = append(allRunes, seg.runes...)
|
|
}
|
|
|
|
start := globalOffset - window
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
end := globalOffset + window
|
|
if end > len(allRunes) {
|
|
end = len(allRunes)
|
|
}
|
|
|
|
if start >= end {
|
|
return normalizeWhitespace(string(allRunes))
|
|
}
|
|
|
|
return normalizeWhitespace(string(allRunes[start:end]))
|
|
}
|
|
|
|
func init() {
|
|
_ = log.Printf
|
|
}
|