link.go

  1package ansi
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"hash/fnv"
  7	"io"
  8	"net/url"
  9
 10	"github.com/charmbracelet/x/ansi"
 11)
 12
 13// A LinkElement is used to render hyperlinks.
 14type LinkElement struct {
 15	BaseURL  string
 16	URL      string
 17	Children []ElementRenderer
 18	SkipText bool
 19	SkipHref bool
 20
 21	hyperlink, resetHyperlink string
 22	validURL                  bool
 23}
 24
 25// Render renders a LinkElement.
 26func (e *LinkElement) Render(w io.Writer, ctx RenderContext) error {
 27	// Make OSC 8 hyperlink token.
 28	e.hyperlink, e.resetHyperlink, e.validURL = makeHyperlink(e.URL)
 29
 30	if !e.SkipText {
 31		if err := e.renderTextPart(w, ctx); err != nil {
 32			return err
 33		}
 34	}
 35	if !e.SkipHref {
 36		if err := e.renderHrefPart(w, ctx); err != nil {
 37			return err
 38		}
 39	}
 40	return nil
 41}
 42
 43func (e *LinkElement) renderTextPart(w io.Writer, ctx RenderContext) error {
 44	for _, child := range e.Children {
 45		if r, ok := child.(StyleOverriderElementRenderer); ok { //nolint:nestif
 46			var b bytes.Buffer
 47			st := ctx.options.Styles.LinkText
 48			if err := r.StyleOverrideRender(&b, ctx, st); err != nil {
 49				return fmt.Errorf("glamour: error rendering with style: %w", err)
 50			}
 51
 52			token := e.hyperlink + b.String() + e.resetHyperlink
 53			if _, err := io.WriteString(w, token); err != nil {
 54				return fmt.Errorf("glamour: error writing hyperlink: %w", err)
 55			}
 56		} else {
 57			var b bytes.Buffer
 58			if err := child.Render(&b, ctx); err != nil {
 59				return fmt.Errorf("glamour: error rendering: %w", err)
 60			}
 61			token := e.hyperlink + b.String() + e.resetHyperlink
 62			el := &BaseElement{
 63				Token: token,
 64				Style: ctx.options.Styles.LinkText,
 65			}
 66			if err := el.Render(w, ctx); err != nil {
 67				return fmt.Errorf("glamour: error rendering: %w", err)
 68			}
 69		}
 70	}
 71	return nil
 72}
 73
 74func (e *LinkElement) renderHrefPart(w io.Writer, ctx RenderContext) error {
 75	prefix := ""
 76	if !e.SkipText {
 77		prefix = " "
 78	}
 79
 80	if e.validURL {
 81		token := e.hyperlink + resolveRelativeURL(e.BaseURL, e.URL) + e.resetHyperlink
 82		el := &BaseElement{
 83			Token:  token,
 84			Prefix: prefix,
 85			Style:  ctx.options.Styles.Link,
 86		}
 87		if err := el.Render(w, ctx); err != nil {
 88			return err
 89		}
 90	}
 91	return nil
 92}
 93
 94// makeHyperlink takes a URL and returns an OSC 8 hyperlink token.
 95func makeHyperlink(link string) (string, string, bool) {
 96	// Make OSC 8 hyperlink token.
 97	var hyperlink, resetHyperlink string
 98
 99	u, err := url.Parse(link)
100	validURL := err == nil && "#"+u.Fragment != link // if the URL only consists of an anchor, ignore it
101	if validURL {
102		h := fnv.New32a()
103		if _, err := io.WriteString(h, link); err != nil {
104			return "", "", false
105		}
106		urlID := fmt.Sprintf("id=%d", h.Sum32())
107		hyperlink = ansi.SetHyperlink(link, urlID)
108		resetHyperlink = ansi.ResetHyperlink()
109	}
110
111	return hyperlink, resetHyperlink, validURL
112}