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  }