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}