From bd87b45aacc7d3ebdd5bfe98323e618404e1722e Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Wed, 24 Jan 2024 10:48:20 +0100 Subject: [PATCH] feat: provide adapters for goldmark and blackfriday These are the 2 most popular Markdown renderers, which can no be passed as extensions and used to render markdown cells (without actually pulling them in as package deps) --- extension/extension_test.go | 58 +++++++++++++++++++++++++++++++++++ extension/markdown.go | 48 +++++++++++++++++++++++++++++ extension/markdown/md.go | 49 +++++++++++++++++++++++++++++ extension/markdown/md_test.go | 43 ++++++++++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 extension/extension_test.go create mode 100644 extension/markdown.go create mode 100644 extension/markdown/md.go create mode 100644 extension/markdown/md_test.go diff --git a/extension/extension_test.go b/extension/extension_test.go new file mode 100644 index 0000000..e7beeb0 --- /dev/null +++ b/extension/extension_test.go @@ -0,0 +1,58 @@ +package extension_test + +import ( + "bytes" + "io" + "testing" + + "github.com/bevzzz/nb" + "github.com/bevzzz/nb/extension" + "github.com/bevzzz/nb/internal/test" + "github.com/bevzzz/nb/render" + "github.com/bevzzz/nb/schema" + "github.com/stretchr/testify/require" +) + +func TestMarkdown(t *testing.T) { + // Arrange + var buf bytes.Buffer + want := []byte("Hi, mom!") + c := nb.New(nb.WithExtensions( + extension.NewMarkdown(func(w io.Writer, c schema.Cell) error { + w.Write(want) + return nil + }), + )) + + // Override default CellWrapper to compare bare cell contents only. + r := c.Renderer() + r.AddOptions(render.WithCellRenderers(&fakeWrapper{})) + + // Act + err := r.Render(&buf, test.Notebook(test.Markdown("Bye!"))) + require.NoError(t, err) + + // Assert + if got := buf.Bytes(); !bytes.Equal(want, got) { + t.Errorf("wrong content: want %q, got %q", want, got) + } +} + +// fakeWrapper calls the passed RenderCellFunc immediately without any additional writes to w. +type fakeWrapper struct{} + +var _ render.CellWrapper = (*fakeWrapper)(nil) + +func (*fakeWrapper) RegisterFuncs(render.RenderCellFuncRegistry) {} +func (*fakeWrapper) Wrap(w io.Writer, c schema.Cell, r render.RenderCellFunc) error { return r(w, c) } +func (*fakeWrapper) WrapInput(w io.Writer, c schema.Cell, r render.RenderCellFunc) error { + return r(w, c) +} +func (*fakeWrapper) WrapOutput(w io.Writer, out schema.Outputter, r render.RenderCellFunc) error { + for _, c := range out.Outputs() { + if err := r(w, c); err != nil { + return err + } + } + return nil +} diff --git a/extension/markdown.go b/extension/markdown.go new file mode 100644 index 0000000..3c45d6b --- /dev/null +++ b/extension/markdown.go @@ -0,0 +1,48 @@ +package extension + +import ( + "github.com/bevzzz/nb" + "github.com/bevzzz/nb/render" + "github.com/bevzzz/nb/schema" + "github.com/bevzzz/nb/schema/common" +) + +// NewMarkdown overrides the default rendering function for markdown cells. +// +// While its lax signature allows passing any arbitrary RenderCellFunc, +// it will be best used to extend nb with existing markdown converters. +// Package extension/markdown offers elegant wrappers for some of the popular options: +// +// extension.NewMarkdown( +// markdown.Blackfriday(blackfriday.MarkdownCommon) +// ) +// +// or +// +// extension.NewMarkdown( +// markdown.Goldmark(func(b []byte, w io.Writer) error { +// return goldmark.Convert(b, w) +// }) +// ) +func NewMarkdown(f render.RenderCellFunc) nb.Extension { + return &markdown{ + render: f, + } +} + +type markdown struct { + render render.RenderCellFunc +} + +var _ nb.Extension = (*markdown)(nil) +var _ render.CellRenderer = (*markdown)(nil) + +// RegisterFuncs registers a new RenderCellFunc for markdown cells. +func (md *markdown) RegisterFuncs(reg render.RenderCellFuncRegistry) { + reg.Register(render.Pref{Type: schema.Markdown, MimeType: common.MarkdownText}, md.render) +} + +// Extend adds markdown as a cell renderer. +func (md *markdown) Extend(n *nb.Notebook) { + n.Renderer().AddOptions(render.WithCellRenderers(md)) +} diff --git a/extension/markdown/md.go b/extension/markdown/md.go new file mode 100644 index 0000000..42071c7 --- /dev/null +++ b/extension/markdown/md.go @@ -0,0 +1,49 @@ +// Package markdown provides convenient adapters for some popular packages +// for rendering Markdown, making it simple to use those as nb extensions. +package markdown + +import ( + "io" + + "github.com/bevzzz/nb/render" + "github.com/bevzzz/nb/schema" +) + +// Blackfriday wraps [blackfriday]-style function in RenderCellFunc. +// +// Usage: +// +// extension.NewMarkdown( +// markdown.Blackfriday(blackfriday.MarkdownCommon) +// ) +// +// [blackfriday]: https://github.com/russross/blackfriday +func Blackfriday(convert func([]byte) []byte) render.RenderCellFunc { + return func(w io.Writer, cell schema.Cell) error { + if _, err := w.Write(convert(cell.Text())); err != nil { + return err + } + return nil + } +} + +// Goldmark wraps [goldmark]-style function in RenderCellFunc. +// +// Usage: +// +// extension.NewMarkdown( +// markdown.Goldmark(func(b []byte, w io.Writer) error { +// return goldmark.Convert(b, w, parseOptions...) +// }) +// ) +// +// Notice, how Goldmark is a bit more verbose compared to Blackfriday: +// this is because goldmark.Convert accepts variadic parser.ParseOptions, which +// is a dependency the client should capture in the closure and pass manually. +// +// [goldmark]: https://github.com/yuin/goldmark +func Goldmark(write func([]byte, io.Writer) error) render.RenderCellFunc { + return func(w io.Writer, cell schema.Cell) error { + return write(cell.Text(), w) + } +} diff --git a/extension/markdown/md_test.go b/extension/markdown/md_test.go new file mode 100644 index 0000000..c4a7cbc --- /dev/null +++ b/extension/markdown/md_test.go @@ -0,0 +1,43 @@ +package markdown_test + +import ( + "io" + "strings" + "testing" + + "github.com/bevzzz/nb/extension/markdown" + "github.com/bevzzz/nb/internal/test" +) + +func TestBlackfriday(t *testing.T) { + // Arrange + var sb strings.Builder + want := "Hi, mom!" + render := markdown.Blackfriday(func(b []byte) []byte { return b }) + + // Act + render(&sb, test.Markdown("Hi, mom!")) + + // Assert + if got := sb.String(); got != want { + t.Errorf("wrong content: want %q, got %q", want, got) + } +} + +func TestGoldmark(t *testing.T) { + // Arrange + var sb strings.Builder + want := "Hi, mom!" + render := markdown.Goldmark(func(b []byte, w io.Writer) error { + w.Write(b) + return nil + }) + + // Act + render(&sb, test.Markdown("Hi, mom!")) + + // Assert + if got := sb.String(); got != want { + t.Errorf("wrong content: want %q, got %q", want, got) + } +}