okami.mom/glamour

git clone https://git.nkp.pet/okami.mom/glamour

Ayman BagabasMerge pull request #408 from charmbracelet/v2-expcf874d7

main
2.7 KiB112 linesraw
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}