From 339b3e9df8f42372434dc4819be72e24d4ff0b72 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Fri, 27 Dec 2019 20:19:20 +0000 Subject: [PATCH] Switch to functional options --- README.md | 5 +- examples/custom_renderer/main.go | 8 +- glamour.go | 181 ++++++++++++++++++++----------- glamour_test.go | 14 +-- 4 files changed, 130 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index d656df6..52ba411 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,9 @@ import ( "github.com/charmbracelet/glamour/ansi" ) -r, _ := glamour.NewTermRenderer("dark", ansi.Options{ - WordWrap: int(40), +r, _ := glamour.NewTermRenderer( + glamour.WithStandardStyle("dark"), + glamour.WithWordWrap(40), }) out, _ := r.Render(in) diff --git a/examples/custom_renderer/main.go b/examples/custom_renderer/main.go index 32f51ea..e4d3a08 100644 --- a/examples/custom_renderer/main.go +++ b/examples/custom_renderer/main.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/charmbracelet/glamour" - "github.com/charmbracelet/glamour/ansi" ) func main() { @@ -17,9 +16,10 @@ line. Bye! ` - r, _ := glamour.NewTermRenderer("dark", ansi.Options{ - WordWrap: int(40), - }) + r, _ := glamour.NewTermRenderer( + glamour.WithStandardStyle("dark"), + glamour.WithWordWrap(40), + ) out, _ := r.Render(in) fmt.Print(out) diff --git a/glamour.go b/glamour.go index 6fd888b..751033d 100644 --- a/glamour.go +++ b/glamour.go @@ -3,9 +3,8 @@ package glamour import ( "bytes" "encoding/json" - "fmt" - "io" "io/ioutil" + "net/http" "os" "github.com/rakyll/statik/fs" @@ -19,12 +18,26 @@ import ( _ "github.com/charmbracelet/glamour/statik" // pre-generated styles ) +var statikFS http.FileSystem + +// A TermRendererOption sets an option on a TermRenderer. +type TermRendererOption func(*TermRenderer) error + // TermRenderer can be used to render markdown content, posing a depth of // customization and styles to fit your needs. type TermRenderer struct { - md goldmark.Markdown - buf bytes.Buffer - renderBuf bytes.Buffer + md goldmark.Markdown + ansiOptions ansi.Options + buf bytes.Buffer + renderBuf bytes.Buffer +} + +func init() { + var err error + statikFS, err = fs.New() + if err != nil { + panic(err) + } } // Render initializes a new TermRenderer and renders a markdown with a specific @@ -37,55 +50,116 @@ func Render(in string, stylePath string) (string, error) { // RenderBytes initializes a new TermRenderer and renders a markdown with a // specific style. func RenderBytes(in []byte, stylePath string) ([]byte, error) { - r, err := NewTermRenderer(stylePath, ansi.Options{ - WordWrap: 80, - }) + r, err := NewTermRenderer( + WithStylePath(stylePath), + ) if err != nil { return nil, err } return r.RenderBytes(in) } -// NewTermRenderer returns a new TermRenderer with style and options set. -func NewTermRenderer(stylePath string, options ansi.Options) (*TermRenderer, error) { - if stylePath == "" { - return NewTermRendererFromBytes([]byte("{}"), options) - } - - b, err := loadStyle(stylePath) - if err != nil { - return nil, err - } - return NewTermRendererFromBytes(b, options) -} - -// NewTermRendererFromBytes returns a new TermRenderer with style and options -// set. -func NewTermRendererFromBytes(b []byte, options ansi.Options) (*TermRenderer, error) { - err := json.Unmarshal(b, &options.Styles) - if err != nil { - // FIXME: wrap error once we depend on Go 1.13 - return nil, fmt.Errorf("parsing style: %v", err) - } - - md := goldmark.New( - goldmark.WithExtensions( - extension.GFM, - extension.DefinitionList, +// NewTermRenderer returns a new TermRenderer the given options. +func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) { + tr := &TermRenderer{ + md: goldmark.New( + goldmark.WithExtensions( + extension.GFM, + extension.DefinitionList, + ), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), ), - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), + ansiOptions: ansi.Options{ + WordWrap: 80, + }, + } + for _, o := range options { + if err := o(tr); err != nil { + return nil, err + } + } + ar := ansi.NewRenderer(tr.ansiOptions) + tr.md.SetRenderer( + renderer.NewRenderer( + renderer.WithNodeRenderers( + util.Prioritized(ar, 1000), + ), ), ) + return tr, nil +} - ar := ansi.NewRenderer(options) - md.SetRenderer( - renderer.NewRenderer( - renderer.WithNodeRenderers(util.Prioritized(ar, 1000)))) +// WithBaseURL sets a TermRenderer's base URL. +func WithBaseURL(baseURL string) TermRendererOption { + return func(tr *TermRenderer) error { + tr.ansiOptions.BaseURL = baseURL + return nil + } +} - return &TermRenderer{ - md: md, - }, nil +// WithStandardStyle sets a TermRenderer's styles with a standard (builtin) +// style. +func WithStandardStyle(style string) TermRendererOption { + return func(tr *TermRenderer) error { + jsonBytes, err := fs.ReadFile(statikFS, "/"+style+".json") + if err != nil { + return err + } + return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles) + } +} + +// WithStylePath sets a TermRenderer's style from stylePath. stylePath is first +// interpreted as a filename. If no such file exists, it is re-interpreted as a +// standard style. +func WithStylePath(stylePath string) TermRendererOption { + return func(tr *TermRenderer) error { + jsonBytes, err := ioutil.ReadFile(stylePath) + if os.IsNotExist(err) { + jsonBytes, err = fs.ReadFile(statikFS, "/"+stylePath+".json") + } + if err != nil { + return err + } + return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles) + } +} + +// WithStyles sets a TermRenderer's styles. +func WithStyles(styles ansi.StyleConfig) TermRendererOption { + return func(tr *TermRenderer) error { + tr.ansiOptions.Styles = styles + return nil + } +} + +// WithStylesFromJSONBytes sets a TermRenderer's styles by parsing styles from +// jsonBytes. +func WithStylesFromJSONBytes(jsonBytes []byte) TermRendererOption { + return func(tr *TermRenderer) error { + return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles) + } +} + +// WithStylesFromJSONFile sets a TermRenderer's styles from a JSON file. +func WithStylesFromJSONFile(filename string) TermRendererOption { + return func(tr *TermRenderer) error { + jsonBytes, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + return json.Unmarshal(jsonBytes, &tr.ansiOptions.Styles) + } +} + +// WithWordWrap sets a TermRenderer's word wrap. +func WithWordWrap(wordWrap int) TermRendererOption { + return func(tr *TermRenderer) error { + tr.ansiOptions.WordWrap = wordWrap + return nil + } } func (tr *TermRenderer) Read(b []byte) (int, error) { @@ -120,24 +194,3 @@ func (tr *TermRenderer) RenderBytes(in []byte) ([]byte, error) { err := tr.md.Convert(in, &buf) return buf.Bytes(), err } - -func loadStyle(f string) ([]byte, error) { - var r io.ReadCloser - - r, err := os.Open(f) - if err != nil { - statikFS, err := fs.New() - if err != nil { - return nil, err - } - - r, err = statikFS.Open("/" + f + ".json") - if err != nil { - // FIXME: wrap error once we depend on Go 1.13 - return nil, fmt.Errorf("loading style %s: %v", f, err) - } - } - - defer r.Close() - return ioutil.ReadAll(r) -} diff --git a/glamour_test.go b/glamour_test.go index af4022e..7403f4c 100644 --- a/glamour_test.go +++ b/glamour_test.go @@ -4,8 +4,6 @@ import ( "bytes" "io/ioutil" "testing" - - "github.com/charmbracelet/glamour/ansi" ) const ( @@ -15,9 +13,9 @@ const ( ) func TestTermRendererWriter(t *testing.T) { - r, err := NewTermRenderer("styles/dark.json", ansi.Options{ - WordWrap: 80, - }) + r, err := NewTermRenderer( + WithStandardStyle("dark"), + ) if err != nil { t.Fatal(err) } @@ -63,9 +61,9 @@ func TestTermRendererWriter(t *testing.T) { } func TestTermRenderer(t *testing.T) { - r, err := NewTermRenderer("styles/dark.json", ansi.Options{ - WordWrap: 80, - }) + r, err := NewTermRenderer( + WithStandardStyle("dark"), + ) if err != nil { t.Fatal(err) }