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