diff --git a/internal/sync/kepub_cfi_converter.go b/internal/sync/kepub_cfi_converter.go new file mode 100644 index 0000000..29bc49b --- /dev/null +++ b/internal/sync/kepub_cfi_converter.go @@ -0,0 +1,283 @@ +package sync + +import ( + "fmt" + "log" + + "golang.org/x/net/html" +) + +type KEPUBCFIConverter struct { + epubConverter *CFIConverter + kepubConverter *CFIConverter +} + +type KEPUBConversionResult struct { + CFI 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, + Percentage: percentage, + Precision: "exact", + }, nil + } + } + } + } + } + + return k.kepubToStandardByPercentage(spineIndex, percentage) +} + +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, + Percentage: percentage, + Precision: "exact", + }, nil + } + } + } + } + } + + return k.standardToKEPUBByPercentage(spineIndex, percentage) +} + +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 "" + } + + 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])) +} + +func (k *KEPUBCFIConverter) ComputeKEPUBPercentage(kepubCFI string) (float64, error) { + if !IsStandardEPUBCFI(kepubCFI) { + return -1, fmt.Errorf("not a standard epubcfi") + } + + spineIndex, steps, err := parseEPUBCFI(kepubCFI) + if err != nil { + return -1, err + } + + kepubDoc, _, docErr := k.kepubConverter.getContentDoc(spineIndex + 1) + if docErr != nil { + return -1, docErr + } + + kepubBody := findBody(kepubDoc) + if kepubBody == nil { + return -1, fmt.Errorf("no body in kepub doc") + } + + textNode, textOffset, resolveErr := resolveCFIToNode(kepubBody, steps) + if resolveErr != nil { + return -1, resolveErr + } + + charOffset := countTextCharsBefore(textNode) + textOffset + + total := 0 + kepubSpine, spineErr := k.kepubConverter.loadSpine() + if spineErr != nil { + return -1, spineErr + } + + for i := range kepubSpine.items { + doc, _, dErr := k.kepubConverter.getContentDoc(i + 1) + if dErr != nil { + continue + } + b := findBody(doc) + if b != nil { + total += countTextChars(b) + } + } + + if total <= 0 { + return -1, fmt.Errorf("no text in kepub") + } + + return float64(charOffset) / float64(total), nil +} + +func init() { + _ = log.Printf +}