okami.mom/glamour
git clone https://git.nkp.pet/okami.mom/glamour
4046973
main
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 ( 26defaultWidth = 80 27highPriority = 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 { 36md goldmark.Markdown 37ansiOptions ansi.Options 38buf bytes.Buffer 39renderBuf 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 ) { 45b ,err := RenderBytes ([]byte (in ),stylePath ) 46return 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 ) { 52b ,err := RenderBytes ([]byte (in ),getEnvironmentStyle ()) 53return 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 ) { 59r ,err := NewTermRenderer ( 60WithStylePath (stylePath ), 61 ) 62if err != nil { 63return nil ,err 64 } 65return r .RenderBytes (in ) 66} 67 68// NewTermRenderer returns a new TermRenderer the given options. 69func NewTermRenderer (options ... TermRendererOption ) (* TermRenderer ,error ) { 70tr := & TermRenderer { 71md :goldmark .New ( 72goldmark .WithExtensions ( 73extension .GFM , 74extension .DefinitionList , 75 ), 76goldmark .WithParserOptions ( 77parser .WithAutoHeadingID (), 78 ), 79 ), 80ansiOptions : ansi.Options { 81WordWrap :defaultWidth , 82 }, 83 } 84for _ ,o := range options { 85if err := o (tr );err != nil { 86return nil ,err 87 } 88 } 89ar := ansi .NewRenderer (tr .ansiOptions ) 90tr .md .SetRenderer ( 91renderer .NewRenderer ( 92renderer .WithNodeRenderers ( 93util .Prioritized (ar ,highPriority ), 94 ), 95 ), 96 ) 97return tr ,nil 98} 99 100// WithBaseURL sets a TermRenderer's base URL. 101func WithBaseURL (baseURL string )TermRendererOption { 102return func (tr * TermRenderer )error { 103tr .ansiOptions .BaseURL = baseURL 104return nil 105 } 106} 107 108// WithStandardStyle sets a TermRenderer's styles with a standard (builtin) 109// style. 110func WithStandardStyle (style string )TermRendererOption { 111return func (tr * TermRenderer )error { 112styles ,err := getDefaultStyle (style ) 113if err != nil { 114return err 115 } 116tr .ansiOptions .Styles = * styles 117return nil 118 } 119} 120 121// WithEnvironmentConfig sets a TermRenderer's styles based on the 122// GLAMOUR_STYLE environment variable. 123func WithEnvironmentConfig ()TermRendererOption { 124return 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 { 131return func (tr * TermRenderer )error { 132styles ,err := getDefaultStyle (stylePath ) 133if err != nil { 134jsonBytes ,err := os .ReadFile (stylePath ) 135if err != nil { 136return fmt .Errorf ("glamour: error reading file: %w" ,err ) 137 } 138 139return json .Unmarshal (jsonBytes ,& tr .ansiOptions .Styles ) 140 } 141tr .ansiOptions .Styles = * styles 142return nil 143 } 144} 145 146// WithStyles sets a TermRenderer's styles. 147func WithStyles (styles ansi.StyleConfig )TermRendererOption { 148return func (tr * TermRenderer )error { 149tr .ansiOptions .Styles = styles 150return nil 151 } 152} 153 154// WithStylesFromJSONBytes sets a TermRenderer's styles by parsing styles from 155// jsonBytes. 156func WithStylesFromJSONBytes (jsonBytes []byte )TermRendererOption { 157return func (tr * TermRenderer )error { 158return 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 { 164return func (tr * TermRenderer )error { 165jsonBytes ,err := os .ReadFile (filename ) 166if err != nil { 167return fmt .Errorf ("glamour: error reading file: %w" ,err ) 168 } 169return json .Unmarshal (jsonBytes ,& tr .ansiOptions .Styles ) 170 } 171} 172 173// WithWordWrap sets a TermRenderer's word wrap. 174func WithWordWrap (wordWrap int )TermRendererOption { 175return func (tr * TermRenderer )error { 176tr .ansiOptions .WordWrap = wordWrap 177return 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 { 185return func (tr * TermRenderer )error { 186tr .ansiOptions .TableWrap = & tableWrap 187return 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 { 194return func (tr * TermRenderer )error { 195tr .ansiOptions .InlineTableLinks = inlineTableLinks 196return nil 197 } 198} 199 200// WithPreservedNewLines preserves newlines from being replaced. 201func WithPreservedNewLines ()TermRendererOption { 202return func (tr * TermRenderer )error { 203tr .ansiOptions .PreserveNewLines = true 204return nil 205 } 206} 207 208// WithEmoji sets a TermRenderer's emoji rendering. 209func WithEmoji ()TermRendererOption { 210return func (tr * TermRenderer )error { 211emoji .New ().Extend (tr .md ) 212return 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 { 219return func (tr * TermRenderer )error { 220for _ ,e := range extensions { 221e .Extend (tr .md ) 222 } 223return 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 { 230return func (tr * TermRenderer )error { 231if tr .ansiOptions .Elements == nil { 232tr .ansiOptions .Elements = map [ast.NodeKind ]ansi.ElementFunc {} 233 } 234tr .ansiOptions .Elements [kind ]= element 235return nil 236 } 237} 238 239// WithChromaFormatter sets a TermRenderer's chroma formatter used for code blocks. 240func WithChromaFormatter (formatter string )TermRendererOption { 241return func (tr * TermRenderer )error { 242tr .ansiOptions .ChromaFormatter = formatter 243return nil 244 } 245} 246 247// WithOptions sets multiple TermRenderer options within a single TermRendererOption. 248func WithOptions (options ... TermRendererOption )TermRendererOption { 249return func (tr * TermRenderer )error { 250for _ ,o := range options { 251if err := o (tr );err != nil { 252return err 253 } 254 } 255return nil 256 } 257} 258 259func (tr * TermRenderer )Read (b []byte ) (int ,error ) { 260n ,err := tr .renderBuf .Read (b ) 261if err == io .EOF { 262return n ,io .EOF 263 } 264if err != nil { 265return 0 ,fmt .Errorf ("glamour: error reading from buffer: %w" ,err ) 266 } 267return n ,nil 268} 269 270func (tr * TermRenderer )Write (b []byte ) (int ,error ) { 271n ,err := tr .buf .Write (b ) 272if err != nil { 273return 0 ,fmt .Errorf ("glamour: error writing bytes: %w" ,err ) 274 } 275return 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 { 281err := tr .md .Convert (tr .buf .Bytes (),& tr .renderBuf ) 282if err != nil { 283return fmt .Errorf ("glamour: error converting markdown: %w" ,err ) 284 } 285 286tr .buf .Reset () 287return nil 288} 289 290// Render returns the markdown rendered into a string. 291func (tr * TermRenderer )Render (in string ) (string ,error ) { 292b ,err := tr .RenderBytes ([]byte (in )) 293return string (b ),err 294} 295 296// RenderBytes returns the markdown rendered into a byte slice. 297func (tr * TermRenderer )RenderBytes (in []byte ) ([]byte ,error ) { 298var buf bytes.Buffer 299err := tr .md .Convert (in ,& buf ) 300return buf .Bytes (),err 301} 302 303func getEnvironmentStyle ()string { 304glamourStyle := os .Getenv ("GLAMOUR_STYLE" ) 305if len (glamourStyle )== 0 { 306glamourStyle = styles .DarkStyle 307 } 308 309return glamourStyle 310} 311 312func getDefaultStyle (style string ) (* ansi.StyleConfig ,error ) { 313styles ,ok := styles .DefaultStyles [style ] 314if ! ok { 315return nil ,fmt .Errorf ("%s: style not found" ,style ) 316 } 317return styles ,nil 318}