1package glamour
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"io/ioutil"
8	"os"
9
10	"github.com/muesli/termenv"
11	"github.com/yuin/goldmark"
12	emoji "github.com/yuin/goldmark-emoji"
13	"github.com/yuin/goldmark/extension"
14	"github.com/yuin/goldmark/parser"
15	"github.com/yuin/goldmark/renderer"
16	"github.com/yuin/goldmark/util"
17
18	"github.com/charmbracelet/glamour/ansi"
19)
20
21// A TermRendererOption sets an option on a TermRenderer.
22type TermRendererOption func(*TermRenderer) error
23
24// TermRenderer can be used to render markdown content, posing a depth of
25// customization and styles to fit your needs.
26type TermRenderer struct {
27	md          goldmark.Markdown
28	ansiOptions ansi.Options
29	buf         bytes.Buffer
30	renderBuf   bytes.Buffer
31}
32
33// Render initializes a new TermRenderer and renders a markdown with a specific
34// style.
35func Render(in string, stylePath string) (string, error) {
36	b, err := RenderBytes([]byte(in), stylePath)
37	return string(b), err
38}
39
40// RenderWithEnvironmentConfig initializes a new TermRenderer and renders a
41// markdown with a specific style defined by the GLAMOUR_STYLE environment variable.
42func RenderWithEnvironmentConfig(in string) (string, error) {
43	b, err := RenderBytes([]byte(in), getEnvironmentStyle())
44	return string(b), err
45}
46
47// RenderBytes initializes a new TermRenderer and renders a markdown with a
48// specific style.
49func RenderBytes(in []byte, stylePath string) ([]byte, error) {
50	r, err := NewTermRenderer(
51		WithStylePath(stylePath),
52	)
53	if err != nil {
54		return nil, err
55	}
56	return r.RenderBytes(in)
57}
58
59// NewTermRenderer returns a new TermRenderer the given options.
60func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) {
61	tr := &TermRenderer{
62		md: goldmark.New(
63			goldmark.WithExtensions(
64				extension.GFM,
65				extension.DefinitionList,
66			),
67			goldmark.WithParserOptions(
68				parser.WithAutoHeadingID(),
69			),
70		),
71		ansiOptions: ansi.Options{
72			WordWrap:     80,
73			ColorProfile: termenv.TrueColor,
74		},
75	}
76	for _, o := range options {
77		if err := o(tr); err != nil {
78			return nil, err
79		}
80	}
81	ar := ansi.NewRenderer(tr.ansiOptions)
82	tr.md.SetRenderer(
83		renderer.NewRenderer(
84			renderer.WithNodeRenderers(
85				util.Prioritized(ar, 1000),
86			),
87		),
88	)
89	return tr, nil
90}
91
92// WithBaseURL sets a TermRenderer's base URL.
93func WithBaseURL(baseURL string) TermRendererOption {
94	return func(tr *TermRenderer) error {
95		tr.ansiOptions.BaseURL = baseURL
96		return nil
97	}
98}
99
100// WithColorProfile sets the TermRenderer's color profile
101// (TrueColor / ANSI256 / ANSI).
102func WithColorProfile(profile termenv.Profile) TermRendererOption {
103	return func(tr *TermRenderer) error {
104		tr.ansiOptions.ColorProfile = profile
105		return nil
106	}
107}
108
109// WithStandardStyle sets a TermRenderer's styles with a standard (builtin)
110// style.
111func WithStandardStyle(style string) TermRendererOption {
112	return func(tr *TermRenderer) error {
113		styles, err := getDefaultStyle(style)
114		if err != nil {
115			return err
116		}
117		tr.ansiOptions.Styles = *styles
118		return nil
119	}
120}
121
122// WithAutoStyle sets a TermRenderer's styles with either the standard dark
123// or light style, depending on the terminal's background color at run-time.
124func WithAutoStyle() TermRendererOption {
125	return WithStandardStyle("auto")
126}
127
128// WithEnvironmentConfig sets a TermRenderer's styles based on the
129// GLAMOUR_STYLE environment variable.
130func WithEnvironmentConfig() TermRendererOption {
131	return WithStylePath(getEnvironmentStyle())
132}
133
134// WithStylePath sets a TermRenderer's style from stylePath. stylePath is first
135// interpreted as a filename. If no such file exists, it is re-interpreted as a
136// standard style.
137func WithStylePath(stylePath string) TermRendererOption {
138	return func(tr *TermRenderer) error {
139		styles, err := getDefaultStyle(stylePath)
140		if err != nil {
141			jsonBytes, err := ioutil.ReadFile(stylePath)
142			if err != nil {
143				return err
144			}
145
146			return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles)
147		}
148		tr.ansiOptions.Styles = *styles
149		return nil
150	}
151}
152
153// WithStyles sets a TermRenderer's styles.
154func WithStyles(styles ansi.StyleConfig) TermRendererOption {
155	return func(tr *TermRenderer) error {
156		tr.ansiOptions.Styles = styles
157		return nil
158	}
159}
160
161// WithStylesFromJSONBytes sets a TermRenderer's styles by parsing styles from
162// jsonBytes.
163func WithStylesFromJSONBytes(jsonBytes []byte) TermRendererOption {
164	return func(tr *TermRenderer) error {
165		return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles)
166	}
167}
168
169// WithStylesFromJSONFile sets a TermRenderer's styles from a JSON file.
170func WithStylesFromJSONFile(filename string) TermRendererOption {
171	return func(tr *TermRenderer) error {
172		jsonBytes, err := ioutil.ReadFile(filename)
173		if err != nil {
174			return err
175		}
176		return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles)
177	}
178}
179
180// WithWordWrap sets a TermRenderer's word wrap.
181func WithWordWrap(wordWrap int) TermRendererOption {
182	return func(tr *TermRenderer) error {
183		tr.ansiOptions.WordWrap = wordWrap
184		return nil
185	}
186}
187
188// WithEmoji sets a TermRenderer's emoji rendering.
189func WithEmoji() TermRendererOption {
190	return func(tr *TermRenderer) error {
191		emoji.New().Extend(tr.md)
192		return nil
193	}
194}
195
196func (tr *TermRenderer) Read(b []byte) (int, error) {
197	return tr.renderBuf.Read(b)
198}
199
200func (tr *TermRenderer) Write(b []byte) (int, error) {
201	return tr.buf.Write(b)
202}
203
204// Close must be called after writing to TermRenderer. You can then retrieve
205// the rendered markdown by calling Read.
206func (tr *TermRenderer) Close() error {
207	err := tr.md.Convert(tr.buf.Bytes(), &tr.renderBuf)
208	if err != nil {
209		return err
210	}
211
212	tr.buf.Reset()
213	return nil
214}
215
216// Render returns the markdown rendered into a string.
217func (tr *TermRenderer) Render(in string) (string, error) {
218	b, err := tr.RenderBytes([]byte(in))
219	return string(b), err
220}
221
222// RenderBytes returns the markdown rendered into a byte slice.
223func (tr *TermRenderer) RenderBytes(in []byte) ([]byte, error) {
224	var buf bytes.Buffer
225	err := tr.md.Convert(in, &buf)
226	return buf.Bytes(), err
227}
228
229func getEnvironmentStyle() string {
230	glamourStyle := os.Getenv("GLAMOUR_STYLE")
231	if len(glamourStyle) == 0 {
232		glamourStyle = "auto"
233	}
234
235	return glamourStyle
236}
237
238func getDefaultStyle(style string) (*ansi.StyleConfig, error) {
239	if style == "auto" {
240		if termenv.HasDarkBackground() {
241			return &DarkStyleConfig, nil
242		}
243		return &LightStyleConfig, nil
244	}
245
246	styles, ok := DefaultStyles[style]
247	if !ok {
248		return nil, fmt.Errorf("%s: style not found", style)
249	}
250	return styles, nil
251}
252