glamour.go

  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}