diff --git a/internal/sync/cfi_converter.go b/internal/sync/cfi_converter.go index e85d0bc..cc37237 100644 --- a/internal/sync/cfi_converter.go +++ b/internal/sync/cfi_converter.go @@ -394,7 +394,69 @@ func findTextInNode(root *html.Node, searchText string) (*html.Node, int) { } walk(root) - return found, foundRuneOffset + if found != nil { + return found, foundRuneOffset + } + + return findTextAcrossInlineElements(root, re) +} + +func findTextAcrossInlineElements(root *html.Node, re *regexp.Regexp) (*html.Node, int) { + var blockElements []*html.Node + collectBlockElements(root, &blockElements) + + for _, block := range blockElements { + segments := collectInlineText(block) + if len(segments) == 0 { + continue + } + + var allRunes []rune + for _, seg := range segments { + allRunes = append(allRunes, seg.runes...) + } + + flattened := string(allRunes) + loc := re.FindStringIndex(flattened) + if loc == nil { + continue + } + + matchStart := utf8.RuneCountInString(flattened[:loc[0]]) + charPos := 0 + for _, seg := range segments { + if charPos+len(seg.runes) > matchStart { + localOffset := matchStart - charPos + return seg.node, localOffset + } + charPos += len(seg.runes) + } + } + + return nil, 0 +} + +func collectBlockElements(n *html.Node, result *[]*html.Node) { + if n.Type == html.ElementNode && !inlineFormattingElements[n.Data] { + hasText := false + var checkText func(*html.Node) + checkText = func(c *html.Node) { + if c.Type == html.TextNode && strings.TrimSpace(c.Data) != "" { + hasText = true + return + } + for child := c.FirstChild; child != nil; child = child.NextSibling { + checkText(child) + } + } + checkText(n) + if hasText { + *result = append(*result, n) + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + collectBlockElements(c, result) + } } func normalizeWhitespace(s string) string { @@ -909,6 +971,54 @@ var voidElements = map[string]bool{ "param": true, "source": true, "track": true, "wbr": true, } +var inlineFormattingElements = map[string]bool{ + "a": true, "abbr": true, "b": true, "bdo": true, "cite": true, + "code": true, "del": true, "dfn": true, "em": true, "i": true, + "ins": true, "kbd": true, "mark": true, "q": true, "samp": true, + "small": true, "span": true, "strong": true, "sub": true, "sup": true, + "u": true, "var": true, +} + +func isInlineFormatting(n *html.Node) bool { + return n != nil && n.Type == html.ElementNode && inlineFormattingElements[n.Data] +} + +type textSegment struct { + node *html.Node + runes []rune + offset int +} + +func collectInlineText(blockParent *html.Node) []textSegment { + var segments []textSegment + var walk func(*html.Node) + walk = func(n *html.Node) { + if n.Type == html.TextNode { + runes := []rune(n.Data) + segments = append(segments, textSegment{node: n, runes: runes}) + } else if isInlineFormatting(n) { + for c := n.FirstChild; c != nil; c = c.NextSibling { + walk(c) + } + } + } + for c := blockParent.FirstChild; c != nil; c = c.NextSibling { + walk(c) + } + return segments +} + +func findBlockParent(textNode *html.Node) *html.Node { + current := textNode.Parent + for current != nil { + if current.Type == html.ElementNode && !inlineFormattingElements[current.Data] { + return current + } + current = current.Parent + } + return nil +} + var selfClosingRe = regexp.MustCompile(`<([a-zA-Z][a-zA-Z0-9]*)([^>]*?)/\s*>`) func preprocessXHTML(input string) string {