okami.mom/glamour

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

nekomimi.petadd WithExtensions and WithElement so callers can parse and render their own nodes4046973

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