Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension API #3

Merged
merged 6 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ c := nb.New(
)

err := c.Convert(&body, b)
if er != nil {
if err != nil {
panic(err)
}

Expand Down
29 changes: 25 additions & 4 deletions convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,22 @@ type Converter interface {
Convert(w io.Writer, source []byte) error
}

// WithExtensions adds extensions.
func WithExtensions(exts ...Extension) Option {
return func(n *Notebook) {
n.extensions = append(n.extensions, exts...)
}
}

// WithRenderer sets a new notebook renderer.
// This option should be supplied before passing any WithRenderOptions.
// Set this option before passing any WithRenderOptions.
func WithRenderer(r render.Renderer) Option {
return func(n *Notebook) {
n.renderer = r
}
}

// WithRendererOptions adds configuration to the notebook renderer.
// WithRendererOptions adds configuration to the current notebook renderer.
func WithRenderOptions(opts ...render.Option) Option {
return func(n *Notebook) {
n.renderer.AddOptions(opts...)
Expand All @@ -37,7 +44,8 @@ func WithRenderOptions(opts ...render.Option) Option {

// Notebook is an extensible Converter implementation.
type Notebook struct {
renderer render.Renderer
renderer render.Renderer
extensions []Extension
}

var _ Converter = (*Notebook)(nil)
Expand All @@ -52,12 +60,15 @@ func New(opts ...Option) *Notebook {
for _, opt := range opts {
opt(&nb)
}
for _, ext := range nb.extensions {
ext.Extend(&nb)
}
return &nb
}

// DefaultRenderer configures an HTML renderer.
func DefaultRenderer() render.Renderer {
return render.New(
return render.NewRenderer(
render.WithCellRenderers(html.NewRenderer()),
)
}
Expand All @@ -71,3 +82,13 @@ func (n *Notebook) Convert(w io.Writer, source []byte) error {
}
return n.renderer.Render(w, nb)
}

// Renderer exposes current renderer, allowing it to be further configured and/or extended.
func (n *Notebook) Renderer() render.Renderer {
return n.renderer
}

// Extension adds new capabilities to the base Notebook.
type Extension interface {
Extend(n *Notebook)
}
78 changes: 77 additions & 1 deletion convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import (
"testing"

"github.com/bevzzz/nb"
"github.com/bevzzz/nb/render"
"github.com/bevzzz/nb/render/html"
"github.com/bevzzz/nb/schema"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)

// update allows updating golden files via `go test -update`
// update allows updating golden files via `go test -update`.
var update = flag.Bool("update", false, "update .golden files in testdata/")

func TestMain(m *testing.M) {
Expand Down Expand Up @@ -104,3 +107,76 @@ func cmpGolden(tb testing.TB, goldenFile string, got []byte, upd bool) {
tb.Logf("failed to save %s: %v", dotnew, err)
}
}

func TestOptions(t *testing.T) {
t.Run("WithRenderer", func(t *testing.T) {
// Arrange
r := render.NewRenderer()

// Act
n := nb.New(nb.WithRenderer(r))

// Assert
if n.Renderer() != r {
t.Error("option was not applied")
}
})

t.Run("WithRendererOptions", func(t *testing.T) {
// Arrange
var spy spyRenderer

// Act
_ = nb.New(
nb.WithRenderer(&spy),
nb.WithRenderOptions(
render.WithCellRenderers(html.NewRenderer()),
render.WithCellRenderers(html.NewRenderer()),
),
)

// Assert
if l := len(spy.AddedOptions); l != 2 {
t.Errorf("expected %d options applied, got %d", 2, l)
}
})

t.Run("WithExtensions", func(t *testing.T) {
// Arrange
var spy spyRenderer
ext := mockExtension{options: []render.Option{
render.WithCellRenderers(html.NewRenderer()),
}}

// Act
_ = nb.New(
nb.WithRenderer(&spy),
nb.WithExtensions(&ext),
)

// Assert
if len(spy.AddedOptions) == 0 {
t.Errorf("option not applied or applied incorrectly")
}
})
}

// spyRenderer records info about options that were applied to it.
type spyRenderer struct{ AddedOptions []render.Option }

func (r *spyRenderer) Render(io.Writer, schema.Notebook) error { return nil }

func (r *spyRenderer) AddOptions(opts ...render.Option) {
r.AddedOptions = append(r.AddedOptions, opts...)
}

// mockExtension extends Notebook's renderer with options.
type mockExtension struct {
options []render.Option
}

var _ nb.Extension = (*mockExtension)(nil)

func (ext *mockExtension) Extend(n *nb.Notebook) {
n.Renderer().AddOptions(ext.options...)
}
2 changes: 1 addition & 1 deletion decode/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ func TestDecodeBytes(t *testing.T) {
// checkCell compares the cell's type and content to expected.
func checkCell(tb testing.TB, got schema.Cell, want Cell) {
tb.Helper()
require.Equalf(tb, want.Type, got.CellType(), "reported cell type: want %q, got %q", want.Type, got.CellType())
require.Equalf(tb, want.Type, got.Type(), "reported cell type: want %q, got %q", want.Type, got.Type())
require.Equal(tb, want.MimeType, got.MimeType(), "reported mime type")
if got, want := got.Text(), want.Text; !bytes.Equal(want, got) {
tb.Errorf("text:\n(+want) %q\n(-got) %q", want, got)
Expand Down
32 changes: 19 additions & 13 deletions render/html/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/bevzzz/nb/render"
"github.com/bevzzz/nb/schema"
"github.com/bevzzz/nb/schema/common"
)

type Config struct {
Expand All @@ -28,8 +29,7 @@ type Renderer struct {
cfg Config
}

// NewRenderer configures a new HTML renderer.
// By default, it embeds a *Wrapper and will panic if it is set to nil by one of the options.
// NewRenderer configures a new HTML renderer and embeds a *Wrapper to implement render.CellWrapper.
func NewRenderer(opts ...Option) *Renderer {
var cfg Config
for _, opt := range opts {
Expand All @@ -43,18 +43,23 @@ func NewRenderer(opts ...Option) *Renderer {
}
}

func (r *Renderer) RegisterFuncs(reg render.RenderCellFuncRegisterer) {
reg.Register(schema.MarkdownCellType, r.renderMarkdown)
reg.Register(schema.CodeCellType, r.renderCode)
reg.Register(schema.PNG, r.renderImage)
reg.Register(schema.JPEG, r.renderImage)
reg.Register(schema.HTML, r.renderRawHTML)
reg.Register(schema.JSON, r.renderRaw)
reg.Register(schema.StdoutCellType, r.renderRaw)
reg.Register(schema.StderrCellType, r.renderRaw)
reg.Register(schema.PlainTextCellType, r.renderRaw)
func (r *Renderer) RegisterFuncs(reg render.RenderCellFuncRegistry) {
// r.renderMarkdown should provide exact MimeType to override "text/*".
reg.Register(render.Pref{Type: schema.Markdown, MimeType: common.MarkdownText}, r.renderMarkdown)
reg.Register(render.Pref{Type: schema.Code}, r.renderCode)

// Stream (stdout+stderr) and "error" outputs.
reg.Register(render.Pref{Type: schema.Stream}, r.renderRaw)
reg.Register(render.Pref{MimeType: common.Stderr}, r.renderRaw) // renders both "error" output and "stderr" stream

// Various types of raw cell contents and display_data/execute_result outputs.
reg.Register(render.Pref{MimeType: "application/json"}, r.renderRaw)
reg.Register(render.Pref{MimeType: "text/*"}, r.renderRaw)
reg.Register(render.Pref{MimeType: "text/html"}, r.renderRawHTML)
reg.Register(render.Pref{MimeType: "image/*"}, r.renderImage)
}

// renderMarkdown renders markdown cells as pre-formatted text.
func (r *Renderer) renderMarkdown(w io.Writer, cell schema.Cell) error {
io.WriteString(w, "<pre>")
w.Write(cell.Text())
Expand Down Expand Up @@ -93,9 +98,10 @@ func (r *Renderer) renderRawHTML(w io.Writer, cell schema.Cell) error {
return nil
}

// renderImage writes base64-encoded image data.
func (r *Renderer) renderImage(w io.Writer, cell schema.Cell) error {
io.WriteString(w, "<img src=\"data:")
io.WriteString(w, string(cell.Type()))
io.WriteString(w, string(cell.MimeType()))
io.WriteString(w, ";base64, ")
w.Write(cell.Text())
io.WriteString(w, "\" />\n")
Expand Down
Loading