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