Skip to content

Commit

Permalink
Fix: preserve newlines in stream output rendered with ansithml (#10)
Browse files Browse the repository at this point in the history
Unlike other HTML-rendering packages (chroma or goldmark), ansihtml does not wrap its output in <pre> tags, so newline characters \n are ignored in the HTML output.

This fix only applies to adapters.AnsiHtml and not to the broader extension.NewStream, because this is specific to ansihtml -- other stream renderers may already render the contents as pre-formatted text, or may want to display it altogether differently.

------

Added an executable example to quickly get the feeling for what nb is capable of and how it can be extended.
  • Loading branch information
bevzzz authored Jan 30, 2024
1 parent 104d4d6 commit a7c6647
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 3 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ c := nb.New(
err := nb.Convert(os.Stdout, b)
```

The snippet above uses these additional dependencies which I encourage you to check out:
The snippet above uses these additional dependencies:

- [`goldmark`](https://github.com/yuin/goldmark) with [`goldmark-jupyter`](./extension/extra/goldmark-jupyter)
- [`chroma`](https://github.com/alecthomas/chroma) with [`nb-synth`](https://github.com/bevzzz/nb-synth)
- [`ansihtml`](https://github.com/robert-nix/ansihtml) with built-in [`adapters.AnsiHtml`](./extension/adapter/ansi.go)

It's a combination of packages that worked really well for me; I encourage you to play around with this [**example CLI**](./example/nbee) to see how it renders different kind of notebooks.

Extending `nb` does not end here. Your project may already use a different Markdown renderer, or require custom handling of certain mime-/cell types, in which case I hope the existing extensions will serve as useful reference implementations.

### Styling the notebook: batteries included 🔋
Expand Down
29 changes: 29 additions & 0 deletions example/nbee/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# nbee

`nbee` ([ɛn-biː], nb extended example) is a small but diligent CLI tool that uses `nb` and several extensions packages to convert `.ipynb` files into beautiful Jupyter notebooks.

## Usage

Compile the package on the fly 🐝

```sh
go run github.com/bevzzz/nb/example/nbee
```

Or, install a binary 🗑

```sh
go install github.com/bevzzz/nb/example/nbee
```

Try it out:

```sh
nbee # convert the default notebook to HTML
nbee -f "my_notebook.ipynb" # convert your own notebooks
```

## Disclaimer

This package is only an example of how `nb` can be extended with other packages.
It's a showcase -- simple and minimal :)
18 changes: 18 additions & 0 deletions example/nbee/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module github.com/bevzzz/nb/example/nbee

go 1.18

require (
github.com/alecthomas/chroma/v2 v2.12.0
github.com/bevzzz/nb v0.2.0
github.com/bevzzz/nb-synth v0.0.0-20240128164931-35fdda0583a0
github.com/bevzzz/nb/extension/extra/goldmark-jupyter v0.0.0-20240130193600-104d4d61fcaf
github.com/robert-nix/ansihtml v1.0.1
github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
)

require (
github.com/dlclark/regexp2 v1.10.0 // indirect
golang.org/x/net v0.20.0 // indirect
)
39 changes: 39 additions & 0 deletions example/nbee/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/bevzzz/nb v0.2.0 h1:KcM1+12N/vCBl43M8kzsRbMxolHpYJlLWOEQe/PFHPA=
github.com/bevzzz/nb v0.2.0/go.mod h1:i8J311U4tUD6ZjBDE3HY8qPswTuUORiUfAFcWPqUTdA=
github.com/bevzzz/nb-synth v0.0.0-20240128164931-35fdda0583a0 h1:CH1+0p2ywCtqQbDL2KpwRn+XL71Peyhlshusdbn13kk=
github.com/bevzzz/nb-synth v0.0.0-20240128164931-35fdda0583a0/go.mod h1:e7rTPaz8bZ1RKH/jysZpz4Hlj8X/HIh9UIXnEeRhTBc=
github.com/bevzzz/nb/extension/extra/goldmark-jupyter v0.0.0-20240130193600-104d4d61fcaf h1:J9zI9odZx5bNZdeXrY6ops+ezyfFfr7AqSQ3bovD4SI=
github.com/bevzzz/nb/extension/extra/goldmark-jupyter v0.0.0-20240130193600-104d4d61fcaf/go.mod h1:Sv6EeiZd/9xiAJttSrXKS0z36/P0x5yYV30gt9bgDE4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
107 changes: 107 additions & 0 deletions example/nbee/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package main

import (
"bytes"
_ "embed"
"flag"
"io"
"log"
"os"
"strings"

chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/bevzzz/nb"
synth "github.com/bevzzz/nb-synth"
"github.com/bevzzz/nb/extension"
"github.com/bevzzz/nb/extension/adapter"
"github.com/bevzzz/nb/render"
"github.com/bevzzz/nb/render/html"
jupyter "github.com/bevzzz/nb/extension/extra/goldmark-jupyter"
"github.com/robert-nix/ansihtml"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
)

var (
file = flag.String("f", "notebook.ipynb", "Jupyter notebook file")
)

//go:embed notebook.ipynb
var defaultNotebook []byte

func main() {
flag.Parse()
var err error

b := defaultNotebook
outFile := "notebook.html"

if f := *file; file != nil {
if b, err = os.ReadFile(f); err != nil {
log.Fatal(err)
}
outFile = strings.ReplaceAll(f, ".ipynb", ".html")
}

_ = os.Remove(outFile)
out, err := os.Create(outFile)
if err != nil {
log.Fatal(err)
}
defer out.Close()

if err := convert(out, b); err != nil {
log.Fatal(err)
}

log.Printf("Done! %s -> %s", *file, outFile)
}

func convert(w io.Writer, b []byte) error {
var body, css bytes.Buffer

md := goldmark.New(
goldmark.WithExtensions(
jupyter.Attachments(),
highlighting.Highlighting,
),
)

c := nb.New(
nb.WithExtensions(
jupyter.Goldmark(md),
synth.NewHighlighting(
synth.WithStyle("monokailight"),
synth.WithFormatOptions(
chromahtml.WithLineNumbers(true),
),
),
extension.NewStream(
adapter.AnsiHtml(ansihtml.ConvertToHTML),
),
),
nb.WithRenderOptions(
render.WithCellRenderers(
html.NewRenderer(
html.WithCSSWriter(&css),
),
),
),
)

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

// Write styles to the final output
io.WriteString(w, "<html><head><meta charset=\"UTF-8\"><style>")
io.Copy(w, &css)
io.WriteString(w, "</style></head>")

// Copy notebook body
io.WriteString(w, "<body>")
io.Copy(w, &body)
io.WriteString(w, "</body></html>")
return nil
}
237 changes: 237 additions & 0 deletions example/nbee/notebook.ipynb

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions extension/adapter/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ func TestAdapter(t *testing.T) {
name: "AnsiHtml",
render: adapter.AnsiHtml(func(b []byte) []byte { return b }),
cell: test.Stdout("Hi, mom!"),
want: "Hi, mom!",
want: "<pre>Hi, mom!</pre>",
},
{
name: "AnsiHtml",
render: adapter.AnsiHtml(func(b []byte) []byte { return b }),
cell: test.Stderr("Hi, mom!"),
want: "Hi, mom!",
want: "<pre>Hi, mom!</pre>",
},
} {
t.Run(tt.name, func(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions extension/adapter/ansi.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import (
// [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) {
// Wrapping in <pre> helps preserve parts of the original
// formatting such as newlines and tabs.
io.WriteString(w, "<pre>")
_, err = w.Write(convert(cell.Text()))
io.WriteString(w, "</pre>")
return
}
}

0 comments on commit a7c6647

Please sign in to comment.