Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion ansi/baseelement.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ func renderText(w io.Writer, p termenv.Profile, rules StylePrimitive, s string)
return
}

// If text contains ANSI sequences (like OSC 8 hyperlinks), write it directly
// without processing through termenv to avoid corruption
if containsANSISequences(s) {
_, _ = io.WriteString(w, s)
return
}

out := termenv.String(s)
if rules.Upper != nil && *rules.Upper {
out = termenv.String(cases.Upper(language.English).String(s))
Expand Down Expand Up @@ -124,7 +131,15 @@ func (e *BaseElement) doRender(w io.Writer, p termenv.Profile, st1, st2 StylePri
return err
}
}
renderText(w, p, st2, escapeReplacer.Replace(s))

// Don't apply escapeReplacer to text that contains ANSI sequences
// This preserves custom formatter output (like OSC 8 hyperlinks)
processedText := s
if !containsANSISequences(s) {
processedText = escapeReplacer.Replace(s)
}

renderText(w, p, st2, processedText)
return nil
}

Expand All @@ -149,3 +164,15 @@ var escapeReplacer = strings.NewReplacer(
"\\!", "!",
"\\|", "|",
)

// containsANSISequences checks if text contains ANSI escape sequences.
// This prevents processing of custom formatter output through escapeReplacer.
func containsANSISequences(text string) bool {
// Check for common ANSI sequence patterns:
// - CSI sequences: ESC[
// - OSC sequences: ESC]
// - Simple escapes: ESC followed by letter
return len(text) > 1 && (strings.Contains(text, "\x1b[") ||
strings.Contains(text, "\x1b]") ||
strings.Contains(text, "\x1b\\"))
}
27 changes: 17 additions & 10 deletions ansi/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,12 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {

return Element{
Renderer: &LinkElement{
BaseURL: ctx.options.BaseURL,
URL: string(n.Destination),
Children: children,
SkipHref: isFooterLinks,
BaseURL: ctx.options.BaseURL,
URL: string(n.Destination),
Children: children,
SkipHref: isFooterLinks,
Formatter: ctx.options.LinkFormatter,
IsInTable: isFooterLinks,
},
}
case ast.KindAutoLink:
Expand Down Expand Up @@ -286,15 +288,20 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
text := linkWithSuffix(tl, ctx.table.tableLinks)

renderer = &LinkElement{
Children: []ElementRenderer{&BaseElement{Token: text}},
URL: u,
SkipHref: true,
Children: []ElementRenderer{&BaseElement{Token: text}},
URL: u,
SkipHref: true,
Formatter: ctx.options.LinkFormatter, // Only set when explicitly provided
IsAutoLink: true,
IsInTable: isFooterLinks,
}
} else {
renderer = &LinkElement{
Children: children,
URL: u,
SkipText: n.AutoLinkType != ast.AutoLinkEmail,
Children: children,
URL: u,
SkipText: n.AutoLinkType != ast.AutoLinkEmail,
Formatter: ctx.options.LinkFormatter, // Only set when explicitly provided
IsAutoLink: true,
}
}
return Element{Renderer: renderer}
Expand Down
Loading