package sync import ( "fmt" "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 (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 }