Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

Commit

Permalink
Use Go templates for building HTML
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelmeuli committed Mar 18, 2020
1 parent aa25b09 commit 3179d82
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 264 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ TODO: Add screenshot
2. Use the library in your code:

```go
var notebookPath := "path/to/your/notebook.ipynb"
var notebookHTML, err := nbtohtml.ConvertFileToHTML(notebookPath)
notebookPath := "path/to/your/notebook.ipynb"
notebookHTML := new(bytes.Buffer)
err := nbtohtml.ConvertFile(notebookHTML, notebookPath)
```

## Styles
Expand Down
5 changes: 4 additions & 1 deletion cmd/nbtohtml/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"bytes"
"fmt"

"github.com/alecthomas/kong"
"github.com/samuelmeuli/nbtohtml"
)
Expand All @@ -27,7 +29,8 @@ func (r *convertCmd) Run() error {
var notebookPath = r.Path

// Convert notebook file to HTML and print result
notebookHTML, err := nbtohtml.ConvertFileToHTML(notebookPath)
notebookHTML := new(bytes.Buffer)
err := nbtohtml.ConvertFile(notebookHTML, notebookPath)
if err != nil {
return err
}
Expand Down
284 changes: 151 additions & 133 deletions convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ package nbtohtml
import (
"bytes"
"fmt"
"html"
"html/template"
"io"
"io/ioutil"
"strings"

"github.com/alecthomas/chroma"
htmlFormatter "github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"github.com/buildkite/terminal-to-html"
"github.com/russross/blackfriday/v2"
"html"
"io"
"io/ioutil"
"strings"
)

// 3rd party renderers
Expand Down Expand Up @@ -43,65 +45,51 @@ func highlightCode(writer io.Writer, source string, lexer string) error {
// renderMarkdown uses the Blackfriday library to convert the provided Markdown lines to HTML.
func renderMarkdown(markdownLines []string) string {
markdownString := strings.Join(markdownLines, "")
mdHTML := blackfriday.Run([]byte(markdownString))
return string(mdHTML)
return string(blackfriday.Run([]byte(markdownString)))
}

// Output renderers

func convertDataOutputToHTML(output Output) (string, error) {
// convertDataOutput converts data output (e.g. a base64-encoded plot image) to HTML.
func convertDataOutput(output Output) template.HTML {
var htmlString string

if output.Data.TextHTML != nil {
return fmt.Sprintf(
`<div class="output output-data-html">%s</div>`,
strings.Join(output.Data.TextHTML, ""),
), nil
}
if output.Data.ApplicationPDF != nil {
return "", fmt.Errorf("missing conversion logic for `application/pdf` data type")
}
if output.Data.TextLaTeX != nil {
return "", fmt.Errorf("missing conversion logic for `text/latex` data type")
}
if output.Data.ImageSVGXML != nil {
return fmt.Sprintf(
`<div class="output output-data-svg">%s</div>`,
strings.Join(output.Data.ImageSVGXML, ""),
), nil
}
if output.Data.ImagePNG != nil {
return fmt.Sprintf(
`<div class="output output-data-png"><img src="data:image/png;base64,%s"></div>`,
*output.Data.ImagePNG,
), nil
}
if output.Data.ImageJPEG != nil {
return fmt.Sprintf(
`<div class="output output-data-jpeg"><img src="data:image/jpeg;base64,%s"></div>`,
*output.Data.ImageJPEG,
), nil
}
if output.Data.TextMarkdown != nil {
return fmt.Sprintf(
`<div class="output output-data-markdown">%s</div>`,
renderMarkdown(output.Data.TextMarkdown),
), nil
}
if output.Data.TextPlain != nil {
return fmt.Sprintf(
`<div class="output output-data-plain-text"><pre>%s</pre></div>`,
htmlString = strings.Join(output.Data.TextHTML, "")
} else if output.Data.ApplicationPDF != nil {
// TODO: Implement PDF conversion
fmt.Printf("missing conversion logic for `application/pdf` data type\n")
htmlString = "<pre>PDF output</pre>"
} else if output.Data.TextLaTeX != nil {
// TODO: Implement LaTeX conversion
fmt.Printf("missing conversion logic for `text/latex` data type\n")
htmlString = "<pre>LaTeX output</pre>"
} else if output.Data.ImageSVGXML != nil {
htmlString = strings.Join(output.Data.ImageSVGXML, "")
} else if output.Data.ImagePNG != nil {
htmlString = fmt.Sprintf(`<img src="data:image/png;base64,%s">`, *output.Data.ImagePNG)
} else if output.Data.ImageJPEG != nil {
htmlString = fmt.Sprintf(`<img src="data:image/jpeg;base64,%s">`, *output.Data.ImageJPEG)
} else if output.Data.TextMarkdown != nil {
htmlString = renderMarkdown(output.Data.TextMarkdown)
} else if output.Data.TextPlain != nil {
htmlString = fmt.Sprintf(
`<pre>%s</pre>`,
html.EscapeString(strings.Join(output.Data.TextPlain, "")),
), nil
)
} else {
htmlString = ""
fmt.Printf("missing `execute_result` data type in output of type `%s`\n", output.OutputType)
}

return "", fmt.Errorf(
"missing `execute_result` data type in output of type `%s`",
output.OutputType,
)
return template.HTML(htmlString)
}

func convertErrorOutputToHTML(output Output) (string, error) {
// convertErrorOutput converts error output (e.g. generated by a Python exception) to HTML.
func convertErrorOutput(output Output) template.HTML {
if output.Traceback == nil {
return "", fmt.Errorf("missing `traceback` key in output of type `error`")
fmt.Printf("missing `traceback` key in output of type `error`\n")
return "<pre>An unknown error occurred</pre>"
}

// Convert ANSI colors to HTML
Expand All @@ -110,126 +98,156 @@ func convertErrorOutputToHTML(output Output) (string, error) {
lineHTML := terminal.Render([]byte(tracebackLine))
linesHTML = append(linesHTML, string(lineHTML))
}

return fmt.Sprintf(
`<div class="output output-error"><pre>%s</pre></div>`,
strings.Join(linesHTML, "\n"),
), nil
htmlString := fmt.Sprintf(`<pre>%s</pre>`, strings.Join(linesHTML, "\n"))
return template.HTML(htmlString)
}

func convertStreamOutputToHTML(output Output) (string, error) {
// convertStreamOutput converts stream output (e.g. stdout written by a Python program) to HTML.
func convertStreamOutput(output Output) template.HTML {
if output.Text == nil {
return "", fmt.Errorf("missing `text` key in output of type `stream`")
fmt.Printf("missing `text` key in output of type `stream`\n")
return ""
}

return fmt.Sprintf(
`<div class="output output-stream"><pre>%s</pre></div>`,
strings.Join(output.Text, ""),
), nil
}

func convertOutputToHTML(output Output) (string, error) {
switch output.OutputType {
case "display_data":
return convertDataOutputToHTML(output)
case "error":
return convertErrorOutputToHTML(output)
case "execute_result":
return convertDataOutputToHTML(output)
case "stream":
return convertStreamOutputToHTML(output)
default:
return "", fmt.Errorf("missing conversion logic for output type `%s`", output.OutputType)
}
htmlString := fmt.Sprintf(`<pre>%s</pre>`, strings.Join(output.Text, ""))
return template.HTML(htmlString)
}

// Cell renderers

// convertMarkdownCellToHTML converts a Markdown cell to HTML.
func convertMarkdownCellToHTML(cell Cell) string {
return fmt.Sprintf(`<div class="cell cell-markdown">%s</div>`, renderMarkdown(cell.Source))
// convertMarkdownCell converts a Markdown cell to HTML.
func convertMarkdownCell(cell Cell) template.HTML {
return template.HTML(renderMarkdown(cell.Source))
}

// convertCodeCellToHTML converts a code cell to HTML with classes for syntax highlighting.
func convertCodeCellToHTML(cell Cell, fileExtension string) (string, error) {
cellHTML := `<div class="cell cell-code">`

// Source
// convertCodeCell converts a code cell to HTML with classes for syntax highlighting.
func convertCodeCell(cell Cell, fileExtension string) template.HTML {
codeString := strings.Join(cell.Source, "")
codeBuffer := new(bytes.Buffer)
err := highlightCode(codeBuffer, codeString, fileExtension)
if err != nil {
return "", err
}
cellHTML += fmt.Sprintf(`<div class="input">%s</div>`, codeBuffer.String())

// Outputs
if cell.Outputs != nil {
for _, output := range cell.Outputs {
var outputHTML, err = convertOutputToHTML(output)
if err == nil {
cellHTML += outputHTML
} else {
fmt.Printf("skipping output: %s\n", err)
}
}
fmt.Printf("skipping syntax highlighting: %d\n", err)
return template.HTML(fmt.Sprintf("<pre>%s</pre>", codeString))
}

return cellHTML + "</div>", nil
return template.HTML(codeBuffer.String())
}

// convertRawCellToHTML returns a simple HTML element for the raw notebook cell.
func convertRawCellToHTML(cell Cell) string {
return fmt.Sprintf(
`<div class="cell cell-raw"><pre>%s</pre></div>`,
// convertRawCell returns a simple HTML element for the raw notebook cell.
func convertRawCell(cell Cell) template.HTML {
htmlString := fmt.Sprintf(
`<pre>%s</pre>`,
html.EscapeString(strings.Join(cell.Source, "")),
)
return template.HTML(htmlString)
}

// Input/output renderers

// convertPrompt returns an HTML string which indicates the input/output's execution count.
func convertPrompt(executionCount *int) template.HTML {
if executionCount == nil {
return ""
}
return template.HTML(fmt.Sprintf("[%d]:", *executionCount))
}

// convertOutput converts the provided cell input to HTML.
func convertInput(fileExtension string, cell Cell) template.HTML {
switch cell.CellType {
case "markdown":
return convertMarkdownCell(cell)
case "code":
return convertCodeCell(cell, fileExtension)
case "raw":
return convertRawCell(cell)
default:
fmt.Printf("skipping cell (unrecognized cell type \"%s\")\n", cell.CellType)
return ""
}
}

// convertOutput converts the provided output of a cell execution to HTML.
func convertOutput(output Output) template.HTML {
switch output.OutputType {
case "display_data":
return convertDataOutput(output)
case "error":
return convertErrorOutput(output)
case "execute_result":
return convertDataOutput(output)
case "stream":
return convertStreamOutput(output)
default:
fmt.Printf("missing conversion logic for output type `%s`\n", output.OutputType)
return ""
}
}

// Notebook renderers

// ConvertFileToHTML reads the file at the provided path and converts its content (the Jupyter
// Notebook JSON) to HTML.
func ConvertFileToHTML(notebookPath string) (string, error) {
// ConvertFile reads the file at the provided path and converts its content (the Jupyter Notebook
// JSON) to HTML.
func ConvertFile(writer io.Writer, notebookPath string) error {
// Read file
fileContent, err := ioutil.ReadFile(notebookPath)
if err != nil {
return "", fmt.Errorf("could not read Jupyter Notebook file at %s", notebookPath)
return fmt.Errorf("could not read Jupyter Notebook file at %s", notebookPath)
}

// Convert file content
return ConvertStringToHTML(string(fileContent))
return ConvertString(writer, string(fileContent))
}

// ConvertStringToHTML converts the provided Jupyter Notebook JSON string to HTML.
func ConvertStringToHTML(notebookString string) (string, error) {
// ConvertString converts the provided Jupyter Notebook JSON string to HTML.
func ConvertString(writer io.Writer, notebookString string) error {
notebook, err := parseNotebook(notebookString)
if err != nil {
return "", err
return err
}

// Get format extension of Jupyter Kernel language (e.g. "py")
fileExtension := notebook.Metadata.LanguageInfo.FileExtension[1:]

// Build HTML string from converted cells
htmlString := `<div class="jupyter-notebook">`
for _, cell := range notebook.Cells {
switch cell.CellType {
case "markdown":
htmlString += convertMarkdownCellToHTML(cell)
case "code":
codeHTMLString, err := convertCodeCellToHTML(cell, fileExtension)
if err == nil {
htmlString += codeHTMLString
} else {
fmt.Printf("skipping cell (syntax highlighting error: %d)\n", err)
}
case "raw":
htmlString += convertRawCellToHTML(cell)
default:
fmt.Printf("skipping cell (unrecognized cell type \"%s\")\n", cell.CellType)
}
t := template.New("notebook")
t = t.Funcs(template.FuncMap{
"convertPrompt": convertPrompt,
"convertInput": convertInput,
"convertOutput": convertOutput,
})
t, err = t.Parse(`
<div class="notebook">
{{ $fileExtension := .FileExtension }}
{{ range .Notebook.Cells }}
<div class="cell cell-{{ .CellType }}">
<div class="input-wrapper">
<div class="input-prompt">
{{ .ExecutionCount | convertPrompt }}
</div>
<div class="input">
{{ . | convertInput $fileExtension }}
</div>
</div>
{{ range .Outputs }}
<div class="output-wrapper">
<div class="output-prompt">
{{ .ExecutionCount | convertPrompt }}
</div>
<div class="output output-{{ .OutputType }}">
{{ . | convertOutput }}
</div>
</div>
{{ end }}
</div>
{{ end }}
</div>
`)
if err != nil {
return err
}

return htmlString + "</div>", nil
templateVars := map[string]interface{}{
"FileExtension": fileExtension,
"Notebook": notebook,
}
return t.Execute(writer, templateVars)
}
Loading

0 comments on commit 3179d82

Please sign in to comment.