Skip to content

Conversation

@jordanh
Copy link

@jordanh jordanh commented Sep 9, 2025

  • I have read CONTRIBUTING.md.
  • I have created a discussion that was approved by a maintainer (for new features).

Custom Link Formatting Architecture for Glamour

Overview

Adds custom link formatting capabilities to the Glamour markdown rendering library. An alternative to #204 and implements #361. The design enables users to provide custom formatting functions for links while maintaining complete backward compatibility.

Design

Existing Link Rendering

Extension Points Identified

  1. LinkElement.Render() method - main customization point
  2. Options struct - can hold formatter configuration
  3. TermRendererOption pattern - for user configuration
  4. Element creation in NewElement() - where formatter gets applied

Architecture Design

Core Data Structures

LinkData

// LinkData contains all parsed link information available to formatters
type LinkData struct {
    // Basic link properties
    URL         string                 // The destination URL
    Text        string                 // The link text (extracted from children)
    Title       string                 // Optional title attribute
    BaseURL     string                 // Base URL for relative link resolution
    
    // Formatting context
    IsAutoLink  bool                   // Whether this is an autolink
    IsInTable   bool                   // Whether link appears in a table
    Children    []ElementRenderer      // Original child elements for advanced rendering
    
    // Style context
    LinkStyle   StylePrimitive         // Style for the URL portion
    TextStyle   StylePrimitive         // Style for the text portion
}

LinkFormatter Interface

// LinkFormatter defines how links should be rendered
type LinkFormatter interface {
    FormatLink(data LinkData, ctx RenderContext) (string, error)
}

// LinkFormatterFunc allows functions to implement LinkFormatter
type LinkFormatterFunc func(LinkData, RenderContext) (string, error)
func (f LinkFormatterFunc) FormatLink(data LinkData, ctx RenderContext) (string, error)

Configuration System

Extended Options

type Options struct {
    // Existing fields (unchanged)
    BaseURL          string
    WordWrap         int
    TableWrap        *bool
    InlineTableLinks bool
    PreserveNewLines bool
    ColorProfile     termenv.Profile
    Styles           StyleConfig
    ChromaFormatter  string
    
    // New: Custom link formatter (nil = default behavior)
    LinkFormatter    LinkFormatter
}

TermRendererOptions

// WithLinkFormatter sets a custom link formatter
func WithLinkFormatter(formatter LinkFormatter) TermRendererOption

// Convenience methods
func WithTextOnlyLinks() TermRendererOption      // Show only clickable text
func WithURLOnlyLinks() TermRendererOption       // Show only URLs  
func WithHyperlinks() TermRendererOption         // Enable OSC 8 hyperlinks
func WithSmartHyperlinks() TermRendererOption    // OSC 8 with fallback

Adds New Built-in Formatters

Default Formatter

var DefaultFormatter = LinkFormatterFunc(func(data LinkData, ctx RenderContext) (string, error) {
    // Exactly replicate current behavior: "text url"
})

Text-Only Formatter

var TextOnlyFormatter = LinkFormatterFunc(func(data LinkData, ctx RenderContext) (string, error) {
    if supportsHyperlinks(ctx) {
        return formatHyperlink(styledText, data.URL), nil
    }
    return applyStyle(data.Text, data.TextStyle, ctx), nil
})

URL-Only Formatter

var URLOnlyFormatter = LinkFormatterFunc(func(data LinkData, ctx RenderContext) (string, error) {
    return applyStyle(data.URL, data.LinkStyle, ctx), nil
})

Hyperlink Formatter (OSC 8)

var HyperlinkFormatter = LinkFormatterFunc(func(data LinkData, ctx RenderContext) (string, error) {
    styledText := applyStyle(data.Text, data.TextStyle, ctx)
    return formatHyperlink(styledText, data.URL), nil
})

3. Modern Terminal Support

OSC 8 Hyperlink Implementation

const (
    HyperlinkStart = "\x1b]8;;"     
    HyperlinkMid   = "\x1b\\"       
    HyperlinkEnd   = "\x1b]8;;\x1b\\"
)

func formatHyperlink(text, url string) string {
    return fmt.Sprintf("%s%s%s%s%s", HyperlinkStart, url, HyperlinkMid, text, HyperlinkEnd)
}

Terminal Detection

func supportsHyperlinks(ctx RenderContext) bool {
    term := os.Getenv("TERM")
    termProgram := os.Getenv("TERM_PROGRAM")
    
    supportingTerminals := map[string]bool{
        "iTerm.app":          true,  // iTerm2
        "vscode":             true,  // VS Code terminal
        "Windows Terminal":   true,  // Windows Terminal
        "WezTerm":           true,   // WezTerm
    }
    
    return supportingTerminals[termProgram] || 
           strings.Contains(term, "256color") || 
           strings.Contains(term, "truecolor")
}

Usage Examples

Basic Usage (No Changes Required)

// Existing code works unchanged
r, _ := glamour.NewTermRenderer(glamour.WithStandardStyle("dark"))
out, _ := r.Render(markdown)

Built-in Formatters

// Text-only links (clickable in smart terminals)
r, _ := glamour.NewTermRenderer(
    glamour.WithStandardStyle("dark"),
    glamour.WithTextOnlyLinks(),
)

// Hyperlinks with fallback
r, _ := glamour.NewTermRenderer(
    glamour.WithStandardStyle("dark"),
    glamour.WithSmartHyperlinks(),
)

Custom Formatters

// Markdown-style formatting
markdownFormatter := glamour.LinkFormatterFunc(func(data glamour.LinkData, ctx glamour.RenderContext) (string, error) {
    return fmt.Sprintf("[%s](%s)", data.Text, data.URL), nil
})

r, _ := glamour.NewTermRenderer(
    glamour.WithStandardStyle("dark"),
    glamour.WithLinkFormatter(markdownFormatter),
)

// Conditional formatting based on context
smartFormatter := glamour.LinkFormatterFunc(func(data glamour.LinkData, ctx glamour.RenderContext) (string, error) {
    if data.IsInTable {
        return data.Text, nil  // Tables: text only
    }
    if supportsHyperlinks(ctx) {
        return formatHyperlink(data.Text, data.URL), nil  // Modern terminals: hyperlinks
    }
    return fmt.Sprintf("%s (%s)", data.Text, data.URL), nil  // Fallback: text (url)
})

Backward Compatibility

  1. Zero Breaking Changes: All existing code continues to work unchanged
  2. Performance: No overhead for existing users (fast path when LinkFormatter is nil)
  3. Output: Default behavior produces identical output to current implementation
  4. API: All existing TermRendererOption functions work unchanged
  5. Configuration: All existing style configurations remain valid
  6. Testing: All existing golden test files produce identical results

@jordanh
Copy link
Author

jordanh commented Sep 9, 2025

I also have a branch with changes for glow that implements custom link formatting for glow, should this PR be merged.

Thank you!

@alxn
Copy link

alxn commented Oct 25, 2025

OSC 8 Hyperlink Implementation

oh this is nice.. I want this over in Glow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants