diff --git a/pkg/test/render.go b/pkg/test/render.go
index 78b65ba..860af44 100644
--- a/pkg/test/render.go
+++ b/pkg/test/render.go
@@ -17,6 +17,7 @@ type fakeWrapper struct{}
var _ render.CellWrapper = (*fakeWrapper)(nil)
func (*fakeWrapper) RegisterFuncs(render.RenderCellFuncRegistry) {}
+func (*fakeWrapper) WrapAll(io.Writer, func(io.Writer) error) error { return nil }
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)
diff --git a/render/html/html.go b/render/html/html.go
index fa598c3..699c97a 100644
--- a/render/html/html.go
+++ b/render/html/html.go
@@ -15,10 +15,10 @@ type Config struct {
type Option func(*Config)
-// WithCSSWriter
+// WithCSSWriter registers a writer for CSS stylesheet.
func WithCSSWriter(w io.Writer) Option {
return func(c *Config) {
- c.CSSWriter = &WriterOnce{w: w}
+ c.CSSWriter = w
}
}
@@ -77,18 +77,12 @@ func (r *Renderer) renderCode(w io.Writer, cell schema.Cell) error {
return nil
}
- div.Open(w, attributes{"class": {"cm-editor", "cm-s-jupyter"}}, true)
- div.Open(w, attributes{"class": {"highlight", "hl-ipython3"}}, true)
-
io.WriteString(w, "
(then there's no need for WriterOnce)
+ - make class prefixes configurable (probably on the html.Config level).
*/
// Wrapper wraps cells in the HTML produced by the original Jupyter's nbconvert.
@@ -25,11 +22,24 @@ type Wrapper struct {
Config
}
-func (wr *Wrapper) Wrap(w io.Writer, cell schema.Cell, render render.RenderCellFunc) error {
+var _ render.CellWrapper = (*Wrapper)(nil)
+
+func (wr *Wrapper) WrapAll(w io.Writer, render func(io.Writer) error) error {
+ tag := tagger{Writer: w}
+ defer tag.Close()
+
if wr.CSSWriter != nil {
wr.CSSWriter.Write(jupyterCSS)
}
+ tag.Open("div", attributes{"class": {"jp-Notebook"}})
+ return render(w)
+}
+
+func (wr *Wrapper) Wrap(w io.Writer, cell schema.Cell, render render.RenderCellFunc) error {
+ tag := tagger{Writer: w}
+ defer tag.Close()
+
var ct string
switch cell.Type() {
case schema.Markdown:
@@ -41,73 +51,73 @@ func (wr *Wrapper) Wrap(w io.Writer, cell schema.Cell, render render.RenderCellF
ct = "jp-RawCell"
}
- div.Open(w, attributes{"class": {"jp-Cell", ct, "jp-Notebook-cell"}}, true)
+ tag.Open("div", attributes{"class": {"jp-Cell", ct, "jp-Notebook-cell"}})
render(w, cell)
- div.Close(w)
return nil
}
func (wr *Wrapper) WrapInput(w io.Writer, cell schema.Cell, render render.RenderCellFunc) error {
- div.Open(w, attributes{
+ tag := tagger{Writer: w}
+ defer tag.Close()
+
+ tag.Open("div", attributes{
"class": {"jp-Cell-inputWrapper"},
- "tabindex": {0}}, true)
+ "tabindex": {0}})
- div.Open(w, attributes{"class": {"jp-Collapser", "jp-InputCollapser", "jp-Cell-inputCollapser"}}, true)
+ tag.Open("div", attributes{"class": {"jp-Collapser", "jp-InputCollapser", "jp-Cell-inputCollapser"}})
io.WriteString(w, " ")
- div.Close(w)
+ tag.CloseLast()
// TODO: add collapser-child
and collapsing functionality
// Pure CSS Collapsible: https://www.digitalocean.com/community/tutorials/css-collapsible
- div.Open(w, attributes{"class": {"jp-InputArea", "jp-Cell-inputArea"}}, true)
+ tag.Open("div", attributes{"class": {"jp-InputArea", "jp-Cell-inputArea"}})
// Prompt In:[1]
- div.Open(w, attributes{"class": {"jp-InputPrompt", "jp-InputArea-prompt"}}, false)
+ tag.OpenInline("div", attributes{"class": {"jp-InputPrompt", "jp-InputArea-prompt"}})
if ex, ok := cell.(interface{ ExecutionCount() int }); ok {
fmt.Fprintf(w, "In\u00a0[%d]:", ex.ExecutionCount())
}
- div.Close(w)
+ tag.CloseLast()
- isCode := cell.Type() == schema.Code
- isMd := cell.Type() == schema.Markdown
- if isCode {
- div.Open(w, attributes{
+ switch cell.Type() {
+ case schema.Code:
+ tag.Open("div", attributes{
"class": {
"jp-CodeMirrorEditor",
"jp-Editor",
"jp-InputArea-editor",
},
"data-type": {"inline"},
- }, true)
- } else if isMd {
- div.Open(w, attributes{
+ })
+
+ tag.Open("div", attributes{"class": {"cm-editor", "cm-s-jupyter"}})
+ tag.Open("div", attributes{"class": {"highlight", "hl-ipython3"}})
+
+ case schema.Markdown:
+ tag.Open("div", attributes{
"class": {
"jp-RenderedMarkdown",
"jp-MarkdownOutput",
"jp-RenderedHTMLCommon",
},
"data-mime-type": {common.MarkdownText},
- }, true)
+ })
}
- // Cell itself
_ = render(w, cell)
-
- if isCode || isMd {
- div.Close(w)
- }
-
- div.Close(w)
- div.Close(w)
return nil
}
func (wr *Wrapper) WrapOutput(w io.Writer, cell schema.Outputter, render render.RenderCellFunc) error {
- div.Open(w, attributes{"class": {"jp-Cell-outputWrapper"}}, true)
- div.OpenClose(w, attributes{"class": {"jp-Collapser", "jp-OutputCollapser", "jp-Cell-outputCollapser"}})
- div.Open(w, attributes{"class": {"jp-OutputArea jp-Cell-outputArea"}}, true)
+ tag := tagger{Writer: w}
+ defer tag.Close()
+
+ tag.Open("div", attributes{"class": {"jp-Cell-outputWrapper"}})
+ tag.OpenInline("div", attributes{"class": {"jp-Collapser", "jp-OutputCollapser", "jp-Cell-outputCollapser"}})
+ tag.CloseLast()
+ tag.Open("div", attributes{"class": {"jp-OutputArea jp-Cell-outputArea"}})
- // TODO: see how application/json would be handled
// TODO: jp-RenderedJavaScript is a thing and so is jp-RenderedLatex (but I don't think we need to do anything about the latter)
var child bool
@@ -141,99 +151,101 @@ func (wr *Wrapper) WrapOutput(w io.Writer, cell schema.Outputter, render render.
} else if strings.HasPrefix(datamimetype, "image/") {
renderedClass = "jp-RenderedImage"
child = true
- } else if datamimetype == "application/vnd.jupyter.stderr" {
+ } else if datamimetype == common.Stderr {
renderedClass = "jp-RenderedText"
}
- // Looks like this will always wrap the whole output area!
if child {
- div.Open(w, attributes{"class": {childClass}}, true)
+ tag.Open("div", attributes{"class": {childClass}})
}
- div.Open(w, attributes{"class": {"jp-OutputPrompt", "jp-OutputArea-prompt"}}, false)
+ tag.OpenInline("div", attributes{"class": {"jp-OutputPrompt", "jp-OutputArea-prompt"}})
for _, out := range cell.Outputs() {
if ex, ok := out.(interface{ ExecutionCount() int }); ok {
fmt.Fprintf(w, "Out\u00a0[%d]:", ex.ExecutionCount())
break
}
}
- div.Close(w)
+ tag.CloseLast()
- div.Open(w, attributes{
+ tag.Open("div", attributes{
"class": {renderedClass, "jp-OutputArea-output", outputtypeclass},
"data-mime-type": {datamimetype},
- }, true)
+ })
for _, out := range cell.Outputs() {
_ = render(w, out)
}
- div.Close(w)
-
- if child {
- div.Close(w)
- }
-
- div.Close(w)
- div.Close(w)
+ tag.CloseLast()
return nil
}
-const (
- div tag = "div"
-)
-
-type tag string
-
-// Open the tag with the attributes, e.g.
.
-func (t tag) Open(w io.Writer, attrs attributes, newline bool) {
- t._open(w, attrs, newline)
-}
-
-func (t tag) _open(w io.Writer, attrs attributes, newline bool) {
- io.WriteString(w, "<")
- io.WriteString(w, string(t))
- attrs.WriteTo(w)
- io.WriteString(w, ">")
- if newline {
- io.WriteString(w, "\n")
- }
+// tagger is a straightforward utility for writing HTML tags.
+//
+// Example:
+//
+// tag := tagger{Writer: os.Stdout}
+// defer tag.Close()
+// tag.Open("div", attributes{"class": {"box"}})
+// tag.Open("pre", attributes{"class": {"hl", "python"}})
+//
+// tagger also supports empty tags.
+type tagger struct {
+ Writer io.Writer
+ opened []string
}
-func (t tag) Close(w io.Writer) {
- fmt.Fprintf(w, "%s>\n", t)
+// Open opens the tag with the attributes.
+func (t *tagger) Open(tag string, attr attributes) {
+ t.openTag(tag, attr, true)
}
-func (t tag) OpenClose(w io.Writer, attrs attributes) {
- t._open(w, attrs, false)
- t.Close(w)
+// Open inline does not add a newline '\n' after the opening tag.
+func (t *tagger) OpenInline(tag string, attr attributes) {
+ t.openTag(tag, attr, false)
}
-// Empty writes the attributes in an empty-element tag, e.g.
.
-func (t tag) Empty(w io.Writer, attrs attributes) {
- io.WriteString(w, "<")
- io.WriteString(w, string(t))
- attrs.WriteTo(w)
- io.WriteString(w, " />")
+// Empty creates an empty HTML tag, like
.
+func (t *tagger) Empty(tag string, attr attributes) {
+ io.WriteString(t.Writer, "<")
+ io.WriteString(t.Writer, tag)
+ attr.WriteTo(t.Writer)
+ io.WriteString(t.Writer, " />")
}
-type tagger struct {
- opened []tag
-}
-
-// Open opens the tag with the attributes.
-func (t *tagger) Open(tag tag, w io.Writer, attr attributes) {
- tag.Open(w, attr, true) // TODO: redo
+func (t *tagger) openTag(tag string, attr attributes, newline bool) {
+ io.WriteString(t.Writer, "<")
+ io.WriteString(t.Writer, tag)
+ attr.WriteTo(t.Writer)
+ io.WriteString(t.Writer, ">")
+ if newline {
+ io.WriteString(t.Writer, "\n")
+ }
t.opened = append(t.opened, tag)
}
// Close closes all opened tags in reverse order.
-func (t *tagger) Close(w io.Writer) {
+// Always adds a newline after the tag.
+func (t *tagger) Close() {
l := len(t.opened)
if l == 0 {
return
}
for i := l - 1; i >= 0; i-- {
- t.opened[i].Close(w)
+ t.closeTag(t.opened[i])
+ }
+}
+
+func (t *tagger) CloseLast() {
+ l := len(t.opened)
+ if l == 0 {
+ return
}
+ t.closeTag(t.opened[l-1])
+ t.opened = t.opened[:l-1]
+}
+
+func (t *tagger) closeTag(tag string) {
+ fmt.Fprintf(t.Writer, "%s>\n", tag)
}
type attributes map[string][]interface{}
@@ -294,19 +306,3 @@ func (attrs attributes) WriteTo(w io.Writer) (n64 int64, err error) {
}
return
}
-
-// WriterOnce writes to the writer once only.
-// TODO: move to util
-type WriterOnce struct {
- w io.Writer
- once sync.Once
-}
-
-var _ io.Writer = (*WriterOnce)(nil)
-
-func (w *WriterOnce) Write(p []byte) (n int, err error) {
- w.once.Do(func() {
- n, err = w.w.Write(p)
- })
- return
-}
diff --git a/render/html/wrapper_test.go b/render/html/wrapper_test.go
index db95d7d..e64ea38 100644
--- a/render/html/wrapper_test.go
+++ b/render/html/wrapper_test.go
@@ -19,6 +19,22 @@ import (
func noopRender(w io.Writer, c schema.Cell) error { return nil }
+func TestWrapper_WrapAll(t *testing.T) {
+ // Arrange
+ var w html.Wrapper
+ var buf bytes.Buffer
+ want := node{tag: "div", attr: map[string][]string{
+ "class": {"jp-Notebook"},
+ }}
+
+ // Act
+ err := w.WrapAll(&buf, func(w io.Writer) error { return nil })
+ require.NoError(t, err)
+
+ // Assert
+ checkDOM(t, &buf, &want)
+}
+
func TestWrapper_Wrap(t *testing.T) {
for _, tt := range []struct {
name string
@@ -201,6 +217,22 @@ func TestWrapper_WrapInput(t *testing.T) {
},
"data-type": {"inline"},
},
+ children: []*node{
+ {
+ tag: "div",
+ attr: map[string][]string{
+ "class": {"cm-editor", "cm-s-jupyter"},
+ },
+ children: []*node{
+ {
+ tag: "div",
+ attr: map[string][]string{
+ "class": {"highlight"},
+ },
+ },
+ },
+ },
+ },
},
},
},
@@ -686,7 +718,7 @@ func findFirst(n *stdhtml.Node, target string) *stdhtml.Node {
// Add modifications are done on the copy, so the original node is not modified.
func dropChildElements(n *stdhtml.Node) *stdhtml.Node {
cp := *n
- if cp.FirstChild != nil && cp.FirstChild.Type != stdhtml.TextNode {
+ if fc := cp.FirstChild; fc != nil && (fc.Type != stdhtml.TextNode || fc.Data == "\n") {
cp.FirstChild = nil
}
cp.NextSibling = nil
diff --git a/render/render.go b/render/render.go
index b55a937..2547ddc 100644
--- a/render/render.go
+++ b/render/render.go
@@ -70,14 +70,19 @@ func WithCellRenderers(crs ...CellRenderer) Option {
// CellWrapper renders common wrapping elements for every cell type.
type CellWrapper interface {
- // Wrap the entire cell block.
+ // Wrap the entire cell.
Wrap(io.Writer, schema.Cell, RenderCellFunc) error
- // Wrap input block.
+ // WrapInput wraps input block.
WrapInput(io.Writer, schema.Cell, RenderCellFunc) error
- // Wrap output block (code cells).
+ // WrapOuput wraps output block (code cells).
WrapOutput(io.Writer, schema.Outputter, RenderCellFunc) error
+
+ // WrapAll wraps all cells in the notebook.
+ // This method will be called once and will receive a function
+ // to render the rest of the notebook.
+ WrapAll(io.Writer, func(io.Writer) error) error
}
// renderer is a base Renderer implementation.