1package ansi
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "strings"
8 "text/template"
9
10 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/x/ansi"
12 "golang.org/x/text/cases"
13 "golang.org/x/text/language"
14)
15
16// BaseElement renders a styled primitive element.
17type BaseElement struct {
18 Token string
19 Prefix string
20 Suffix string
21 Style StylePrimitive
22}
23
24func formatToken(format string, token string) (string, error) {
25 var b bytes.Buffer
26
27 v := make(map[string]interface{})
28 v["text"] = token
29
30 tmpl, err := template.New(format).Funcs(TemplateFuncMap).Parse(format)
31 if err != nil {
32 return "", fmt.Errorf("glamour: error parsing template: %w", err)
33 }
34
35 err = tmpl.Execute(&b, v)
36 return b.String(), err
37}
38
39func renderText(w io.Writer, rules StylePrimitive, s string) (int, error) { //nolint:unparam
40 if len(s) == 0 {
41 return 0, nil
42 }
43
44 // XXX: We're using [ansi.Style] instead of [lipgloss.Style] because
45 // Lip Gloss has a weird bug where it adds spaces when rendering joined
46 // strings. Needs further investigation.
47 style := ansi.Style{}
48 if rules.Upper != nil && *rules.Upper {
49 s = cases.Upper(language.English).String(s)
50 }
51 if rules.Lower != nil && *rules.Lower {
52 s = cases.Lower(language.English).String(s)
53 }
54 if rules.Title != nil && *rules.Title {
55 s = cases.Title(language.English).String(s)
56 }
57 if rules.Color != nil {
58 style = style.ForegroundColor(lipgloss.Color(*rules.Color))
59 }
60 if rules.BackgroundColor != nil {
61 style = style.BackgroundColor(lipgloss.Color(*rules.BackgroundColor))
62 }
63 if rules.Underline != nil && *rules.Underline {
64 style = style.Underline()
65 }
66 if rules.Bold != nil && *rules.Bold {
67 style = style.Bold()
68 }
69 if rules.Italic != nil && *rules.Italic {
70 style = style.Italic()
71 }
72 if rules.CrossedOut != nil && *rules.CrossedOut {
73 style = style.Strikethrough()
74 }
75 if rules.Inverse != nil && *rules.Inverse {
76 style = style.Reverse()
77 }
78 if rules.Blink != nil && *rules.Blink {
79 style = style.SlowBlink()
80 }
81
82 n, err := io.WriteString(w, style.Styled(s))
83 if err != nil {
84 return n, fmt.Errorf("glamour: error writing to writer: %w", err)
85 }
86
87 return n, nil
88}
89
90// StyleOverrideRender renders a BaseElement with an overridden style.
91func (e *BaseElement) StyleOverrideRender(w io.Writer, ctx RenderContext, style StylePrimitive) error {
92 bs := ctx.blockStack
93 st1 := cascadeStylePrimitives(bs.Current().Style.StylePrimitive, style)
94 st2 := cascadeStylePrimitives(bs.With(e.Style), style)
95
96 return e.doRender(w, st1, st2)
97}
98
99// Render renders a BaseElement.
100func (e *BaseElement) Render(w io.Writer, ctx RenderContext) error {
101 bs := ctx.blockStack
102 st1 := bs.Current().Style.StylePrimitive
103 st2 := bs.With(e.Style)
104 return e.doRender(w, st1, st2)
105}
106
107func (e *BaseElement) doRender(w io.Writer, st1, st2 StylePrimitive) error {
108 _, _ = renderText(w, st1, e.Prefix)
109 defer func() {
110 _, _ = renderText(w, st1, e.Suffix)
111 }()
112
113 // render unstyled prefix/suffix
114 _, _ = renderText(w, st1, st2.BlockPrefix)
115 defer func() {
116 _, _ = renderText(w, st1, st2.BlockSuffix)
117 }()
118
119 // render styled prefix/suffix
120 _, _ = renderText(w, st2, st2.Prefix)
121 defer func() {
122 _, _ = renderText(w, st2, st2.Suffix)
123 }()
124
125 s := e.Token
126 if len(st2.Format) > 0 {
127 var err error
128 s, err = formatToken(st2.Format, s)
129 if err != nil {
130 return err
131 }
132 }
133 _, _ = renderText(w, st2, escapeReplacer.Replace(s))
134 return nil
135}
136
137// https://www.markdownguide.org/basic-syntax/#characters-you-can-escape
138var escapeReplacer = strings.NewReplacer(
139 "\\\\", "\\",
140 "\\`", "`",
141 "\\*", "*",
142 "\\_", "_",
143 "\\{", "{",
144 "\\}", "}",
145 "\\[", "[",
146 "\\]", "]",
147 "\\<", "<",
148 "\\>", ">",
149 "\\(", "(",
150 "\\)", ")",
151 "\\#", "#",
152 "\\+", "+",
153 "\\-", "-",
154 "\\.", ".",
155 "\\!", "!",
156 "\\|", "|",
157)