Skip to content

Commit

Permalink
feat(ext): add adapter for ansihtml.ConvertToHTML
Browse files Browse the repository at this point in the history
Collected all adapters in a single extension/adapter package, because
there is a lot of shared logic and structure and no naming clashes.
  • Loading branch information
bevzzz committed Jan 24, 2024
1 parent bd87b45 commit 782a14d
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 63 deletions.
62 changes: 62 additions & 0 deletions extension/adapter/adapter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package adapter_test

import (
"io"
"strings"
"testing"

"github.com/bevzzz/nb/extension/adapter"
"github.com/bevzzz/nb/internal/test"
"github.com/bevzzz/nb/render"
"github.com/bevzzz/nb/schema"
)

func TestAdapter(t *testing.T) {
for _, tt := range []struct {
name string
render render.RenderCellFunc
cell schema.Cell
want string
}{
{
name: "Goldmark",
render: adapter.Goldmark(func(b []byte, w io.Writer) error {
w.Write(b)
return nil
}),
cell: test.Markdown("Hi, mom!"),
want: "Hi, mom!",
},
{
name: "Blackfriday",
render: adapter.Blackfriday(func(b []byte) []byte { return b }),
cell: test.Markdown("Hi, mom!"),
want: "Hi, mom!",
},
{
name: "AnsiHtml",
render: adapter.AnsiHtml(func(b []byte) []byte { return b }),
cell: test.Stdout("Hi, mom!"),
want: "Hi, mom!",
},
{
name: "AnsiHtml",
render: adapter.AnsiHtml(func(b []byte) []byte { return b }),
cell: test.Stderr("Hi, mom!"),
want: "Hi, mom!",
},
} {
t.Run(tt.name, func(t *testing.T) {
// Arrange
var sb strings.Builder

// Act
tt.render(&sb, tt.cell)

// Assert
if got := sb.String(); got != tt.want {
t.Errorf("wrong content: want %q, got %q", tt.want, got)
}
})
}
}
32 changes: 32 additions & 0 deletions extension/adapter/ansi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package adapter

import (
"io"

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

// AnsiHtml wraps [ansihtml]-style function in RenderCellFunc.
//
// Usage:
//
// extension.NewStream(
// adapter.AnsiHtml(ansihtml.ConvertToHTML)
// )
//
// To force ansihtml to use classes instead of inline styles, pass an anonymous function intead:
//
// extension.NewStream(
// adapter.AnsiHtml(func([]byte) []byte) {
// ansihtml.ConvertToHTMLWithClasses(b, "class-", false)
// })
// )
//
// [ansihtml]: https://github.com/robert-nix/ansihtml
func AnsiHtml(convert func([]byte) []byte) render.RenderCellFunc {
return func(w io.Writer, cell schema.Cell) (err error) {
_, err = w.Write(convert(cell.Text()))
return
}
}
10 changes: 10 additions & 0 deletions extension/adapter/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Package adapter provides convenient adapters for other popular packages
// making it simple to use those as nb extensions.
//
// - Markdown: [goldmark] and [blackfriday]
// - ANSI to HTML conversion: [ansihtml]
//
// [goldmark]: https://github.com/yuin/goldmark
// [blackfriday]: https://github.com/russross/blackfriday
// [ansihtml]: https://github.com/robert-nix/ansihtml
package adapter
16 changes: 6 additions & 10 deletions extension/markdown/md.go → extension/adapter/md.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// Package markdown provides convenient adapters for some popular packages
// for rendering Markdown, making it simple to use those as nb extensions.
package markdown
package adapter

import (
"io"
Expand All @@ -14,16 +12,14 @@ import (
// Usage:
//
// extension.NewMarkdown(
// markdown.Blackfriday(blackfriday.MarkdownCommon)
// adapter.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
return func(w io.Writer, cell schema.Cell) (err error) {
_, err = w.Write(convert(cell.Text()))
return
}
}

Expand All @@ -32,7 +28,7 @@ func Blackfriday(convert func([]byte) []byte) render.RenderCellFunc {
// Usage:
//
// extension.NewMarkdown(
// markdown.Goldmark(func(b []byte, w io.Writer) error {
// adapter.Goldmark(func(b []byte, w io.Writer) error {
// return goldmark.Convert(b, w, parseOptions...)
// })
// )
Expand Down
57 changes: 51 additions & 6 deletions extension/extension_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package extension_test

import (
"bytes"
"io"
"strings"
"testing"

"github.com/bevzzz/nb"
Expand All @@ -15,11 +15,11 @@ import (

func TestMarkdown(t *testing.T) {
// Arrange
var buf bytes.Buffer
want := []byte("Hi, mom!")
var sb strings.Builder
want := "Hi, mom!"
c := nb.New(nb.WithExtensions(
extension.NewMarkdown(func(w io.Writer, c schema.Cell) error {
w.Write(want)
io.WriteString(w, want)
return nil
}),
))
Expand All @@ -29,15 +29,60 @@ func TestMarkdown(t *testing.T) {
r.AddOptions(render.WithCellRenderers(&fakeWrapper{}))

// Act
err := r.Render(&buf, test.Notebook(test.Markdown("Bye!")))
err := r.Render(&sb, test.Notebook(test.Markdown("Bye!")))
require.NoError(t, err)

// Assert
if got := buf.Bytes(); !bytes.Equal(want, got) {
if got := sb.String(); got != want {
t.Errorf("wrong content: want %q, got %q", want, got)
}
}

func TestStream(t *testing.T) {
for _, tt := range []struct {
name string
cell schema.Cell
}{
{
name: "handles stream to stdout",
cell: test.Stdout("Hi, mom!"),
},
{
name: "handles stream to stderr",
cell: test.Stderr("Hi, mom!"),
},
{
name: "handles error output",
cell: test.ErrorOutput("Hi, mom!"),
},
} {
t.Run(tt.name, func(t *testing.T) {
// Arrange
var sb strings.Builder
want := "Hi, mom!"
c := nb.New(nb.WithExtensions(
extension.NewStream(func(w io.Writer, c schema.Cell) error {
io.WriteString(w, want)
return nil
}),
))

// Override default CellWrapper to compare bare cell contents only.
r := c.Renderer()
r.AddOptions(render.WithCellRenderers(&fakeWrapper{}))

// Act
err := r.Render(&sb, test.Notebook(tt.cell))
require.NoError(t, err)

// Assert
if got := sb.String(); got != want {
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{}

Expand Down
8 changes: 4 additions & 4 deletions extension/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import (
)

// 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:
// Package extension/adapters offers elegant wrappers for some of the popular options:
//
// extension.NewMarkdown(
// markdown.Blackfriday(blackfriday.MarkdownCommon)
// adapter.Blackfriday(blackfriday.MarkdownCommon)
// )
//
// or
//
// extension.NewMarkdown(
// markdown.Goldmark(func(b []byte, w io.Writer) error {
// adapter.Goldmark(func(b []byte, w io.Writer) error {
// return goldmark.Convert(b, w)
// })
// )
Expand Down
43 changes: 0 additions & 43 deletions extension/markdown/md_test.go

This file was deleted.

44 changes: 44 additions & 0 deletions extension/stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package extension

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

// NewStream overrides the default rendering function for "stream" and "error" output cells.
// These will often be formatted with ANSI-color codes, which you may want to replace with
// styled HTML tags or strip from the output completely.
//
// For example, use [ansihtml] with a dedicated adapter:
//
// extension.NewStream(
// adapter.AnsiHtml(ansihtml.ConvertToHTML)
// )
//
// [ansihtml]: https://github.com/robert-nix/ansihtml
func NewStream(f render.RenderCellFunc) nb.Extension {
return &stream{
render: f,
}
}

type stream struct {
render render.RenderCellFunc
}

var _ nb.Extension = (*stream)(nil)
var _ render.CellRenderer = (*stream)(nil)

// RegisterFuncs registers a new RenderCellFunc for stream output cells.
func (s *stream) RegisterFuncs(reg render.RenderCellFuncRegistry) {
reg.Register(render.Pref{Type: schema.Stream, MimeType: common.Stdout}, s.render)
reg.Register(render.Pref{Type: schema.Stream, MimeType: common.Stderr}, s.render)
reg.Register(render.Pref{Type: schema.Error, MimeType: common.Stderr}, s.render)
}

// Extend adds stream as a cell renderer.
func (s *stream) Extend(n *nb.Notebook) {
n.Renderer().AddOptions(render.WithCellRenderers(s))
}

0 comments on commit 782a14d

Please sign in to comment.