1// Package glamour lets you render markdown documents & templates on ANSI
2// compatible terminals. You can create your own stylesheet or simply use one of
3// the stylish defaults
4package glamour
5
6import (
7 "bytes"
8 "encoding/json"
9 "fmt"
10 "io"
11 "os"
12
13 "github.com/yuin/goldmark"
14 emoji "github.com/yuin/goldmark-emoji"
15 "github.com/yuin/goldmark/extension"
16 "github.com/yuin/goldmark/parser"
17 "github.com/yuin/goldmark/renderer"
18 "github.com/yuin/goldmark/util"
19
20 "github.com/charmbracelet/glamour/v2/ansi"
21 styles "github.com/charmbracelet/glamour/v2/styles"
22)
23
24const (
25 defaultWidth = 80
26 highPriority = 1000
27)
28
29// A TermRendererOption sets an option on a TermRenderer.
30type TermRendererOption func(*TermRenderer) error
31
32// TermRenderer can be used to render markdown content, posing a depth of
33// customization and styles to fit your needs.
34type TermRenderer struct {
35 md goldmark.Markdown
36 ansiOptions ansi.Options
37 buf bytes.Buffer
38 renderBuf bytes.Buffer
39}
40
41// Render initializes a new TermRenderer and renders a markdown with a specific
42// style.
43func Render(in string, stylePath string) (string, error) {
44 b, err := RenderBytes([]byte(in), stylePath)
45 return string(b), err
46}
47
48// RenderWithEnvironmentConfig initializes a new TermRenderer and renders a
49// markdown with a specific style defined by the GLAMOUR_STYLE environment variable.
50func RenderWithEnvironmentConfig(in string) (string, error) {
51 b, err := RenderBytes([]byte(in), getEnvironmentStyle())
52 return string(b), err
53}
54
55// RenderBytes initializes a new TermRenderer and renders a markdown with a
56// specific style.
57func RenderBytes(in []byte, stylePath string) ([]byte, error) {
58 r, err := NewTermRenderer(
59 WithStylePath(stylePath),
60 )
61 if err != nil {
62 return nil, err
63 }
64 return r.RenderBytes(in)
65}
66
67// NewTermRenderer returns a new TermRenderer the given options.
68func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) {
69 tr := &TermRenderer{
70 md: goldmark.New(
71 goldmark.WithExtensions(
72 extension.GFM,
73 extension.DefinitionList,
74 ),
75 goldmark.WithParserOptions(
76 parser.WithAutoHeadingID(),
77 ),
78 ),
79 ansiOptions: ansi.Options{
80 WordWrap: defaultWidth,
81 },
82 }
83 for _, o := range options {
84 if err := o(tr); err != nil {
85 return nil, err
86 }
87 }
88 ar := ansi.NewRenderer(tr.ansiOptions)
89 tr.md.SetRenderer(
90 renderer.NewRenderer(
91 renderer.WithNodeRenderers(
92 util.Prioritized(ar, highPriority),
93 ),
94 ),
95 )
96 return tr, nil
97}
98
99// WithBaseURL sets a TermRenderer's base URL.
100func WithBaseURL(baseURL string) TermRendererOption {
101 return func(tr *TermRenderer) error {
102 tr.ansiOptions.BaseURL = baseURL
103 return nil
104 }
105}
106
107// WithStandardStyle sets a TermRenderer's styles with a standard (builtin)
108// style.
109func WithStandardStyle(style string) TermRendererOption {
110 return func(tr *TermRenderer) error {
111 styles, err := getDefaultStyle(style)
112 if err != nil {
113 return err
114 }
115 tr.ansiOptions.Styles = *styles
116 return nil
117 }
118}
119
120// WithEnvironmentConfig sets a TermRenderer's styles based on the
121// GLAMOUR_STYLE environment variable.
122func WithEnvironmentConfig() TermRendererOption {
123 return WithStylePath(getEnvironmentStyle())
124}
125
126// WithStylePath sets a TermRenderer's style from stylePath. stylePath is first
127// interpreted as a filename. If no such file exists, it is re-interpreted as a
128// standard style.
129func WithStylePath(stylePath string) TermRendererOption {
130 return func(tr *TermRenderer) error {
131 styles, err := getDefaultStyle(stylePath)
132 if err != nil {
133 jsonBytes, err := os.ReadFile(stylePath)
134 if err != nil {
135 return fmt.Errorf("glamour: error reading file: %w", err)
136 }
137
138 return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles)
139 }
140 tr.ansiOptions.Styles = *styles
141 return nil
142 }
143}
144
145// WithStyles sets a TermRenderer's styles.
146func WithStyles(styles ansi.StyleConfig) TermRendererOption {
147 return func(tr *TermRenderer) error {
148 tr.ansiOptions.Styles = styles
149 return nil
150 }
151}
152
153// WithStylesFromJSONBytes sets a TermRenderer's styles by parsing styles from
154// jsonBytes.
155func WithStylesFromJSONBytes(jsonBytes []byte) TermRendererOption {
156 return func(tr *TermRenderer) error {
157 return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles)
158 }
159}
160
161// WithStylesFromJSONFile sets a TermRenderer's styles from a JSON file.
162func WithStylesFromJSONFile(filename string) TermRendererOption {
163 return func(tr *TermRenderer) error {
164 jsonBytes, err := os.ReadFile(filename)
165 if err != nil {
166 return fmt.Errorf("glamour: error reading file: %w", err)
167 }
168 return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles)
169 }
170}
171
172// WithWordWrap sets a TermRenderer's word wrap.
173func WithWordWrap(wordWrap int) TermRendererOption {
174 return func(tr *TermRenderer) error {
175 tr.ansiOptions.WordWrap = wordWrap
176 return nil
177 }
178}
179
180// WithTableWrap controls whether table content will wrap if too long.
181// This is true by default. If false, table content will be truncated with an
182// ellipsis if too long to fit.
183func WithTableWrap(tableWrap bool) TermRendererOption {
184 return func(tr *TermRenderer) error {
185 tr.ansiOptions.TableWrap = &tableWrap
186 return nil
187 }
188}
189
190// WithInlineTableLinks forces tables to render links inline. By default,links
191// are rendered as a list of links at the bottom of the table.
192func WithInlineTableLinks(inlineTableLinks bool) TermRendererOption {
193 return func(tr *TermRenderer) error {
194 tr.ansiOptions.InlineTableLinks = inlineTableLinks
195 return nil
196 }
197}
198
199// WithPreservedNewLines preserves newlines from being replaced.
200func WithPreservedNewLines() TermRendererOption {
201 return func(tr *TermRenderer) error {
202 tr.ansiOptions.PreserveNewLines = true
203 return nil
204 }
205}
206
207// WithEmoji sets a TermRenderer's emoji rendering.
208func WithEmoji() TermRendererOption {
209 return func(tr *TermRenderer) error {
210 emoji.New().Extend(tr.md)
211 return nil
212 }
213}
214
215// WithChromaFormatter sets a TermRenderer's chroma formatter used for code blocks.
216func WithChromaFormatter(formatter string) TermRendererOption {
217 return func(tr *TermRenderer) error {
218 tr.ansiOptions.ChromaFormatter = formatter
219 return nil
220 }
221}
222
223// WithOptions sets multiple TermRenderer options within a single TermRendererOption.
224func WithOptions(options ...TermRendererOption) TermRendererOption {
225 return func(tr *TermRenderer) error {
226 for _, o := range options {
227 if err := o(tr); err != nil {
228 return err
229 }
230 }
231 return nil
232 }
233}
234
235func (tr *TermRenderer) Read(b []byte) (int, error) {
236 n, err := tr.renderBuf.Read(b)
237 if err == io.EOF {
238 return n, io.EOF
239 }
240 if err != nil {
241 return 0, fmt.Errorf("glamour: error reading from buffer: %w", err)
242 }
243 return n, nil
244}
245
246func (tr *TermRenderer) Write(b []byte) (int, error) {
247 n, err := tr.buf.Write(b)
248 if err != nil {
249 return 0, fmt.Errorf("glamour: error writing bytes: %w", err)
250 }
251 return n, nil
252}
253
254// Close must be called after writing to TermRenderer. You can then retrieve
255// the rendered markdown by calling Read.
256func (tr *TermRenderer) Close() error {
257 err := tr.md.Convert(tr.buf.Bytes(), &tr.renderBuf)
258 if err != nil {
259 return fmt.Errorf("glamour: error converting markdown: %w", err)
260 }
261
262 tr.buf.Reset()
263 return nil
264}
265
266// Render returns the markdown rendered into a string.
267func (tr *TermRenderer) Render(in string) (string, error) {
268 b, err := tr.RenderBytes([]byte(in))
269 return string(b), err
270}
271
272// RenderBytes returns the markdown rendered into a byte slice.
273func (tr *TermRenderer) RenderBytes(in []byte) ([]byte, error) {
274 var buf bytes.Buffer
275 err := tr.md.Convert(in, &buf)
276 return buf.Bytes(), err
277}
278
279func getEnvironmentStyle() string {
280 glamourStyle := os.Getenv("GLAMOUR_STYLE")
281 if len(glamourStyle) == 0 {
282 glamourStyle = styles.DarkStyle
283 }
284
285 return glamourStyle
286}
287
288func getDefaultStyle(style string) (*ansi.StyleConfig, error) {
289 styles, ok := styles.DefaultStyles[style]
290 if !ok {
291 return nil, fmt.Errorf("%s: style not found", style)
292 }
293 return styles, nil
294}