2018-06-02 17:55:12 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2018-10-01 21:16:39 +02:00
|
|
|
"bytes"
|
2018-06-02 17:55:12 +02:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2019-04-10 22:15:44 +02:00
|
|
|
"image"
|
2018-10-01 21:16:39 +02:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
2018-06-02 17:55:12 +02:00
|
|
|
r "reflect"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/cosmos72/gomacro/imports"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Support an interface similar - but not identical - to the IPython (canonical Jupyter kernel).
|
|
|
|
// See http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display
|
2019-04-10 22:31:51 +02:00
|
|
|
// for a good overview of the support types.
|
2018-06-02 17:55:12 +02:00
|
|
|
|
|
|
|
const (
|
2018-06-04 21:55:35 +02:00
|
|
|
MIMETypeHTML = "text/html"
|
|
|
|
MIMETypeJavaScript = "application/javascript"
|
|
|
|
MIMETypeJPEG = "image/jpeg"
|
|
|
|
MIMETypeJSON = "application/json"
|
|
|
|
MIMETypeLatex = "text/latex"
|
|
|
|
MIMETypeMarkdown = "text/markdown"
|
|
|
|
MIMETypePNG = "image/png"
|
|
|
|
MIMETypePDF = "application/pdf"
|
|
|
|
MIMETypeSVG = "image/svg+xml"
|
2019-04-11 22:22:37 +02:00
|
|
|
MIMETypeText = "text/plain"
|
2018-06-02 17:55:12 +02:00
|
|
|
)
|
|
|
|
|
2019-04-10 22:31:51 +02:00
|
|
|
/**
|
2019-04-11 22:22:37 +02:00
|
|
|
* general interface, allows libraries to fully specify
|
|
|
|
* how their data is displayed by Jupyter.
|
2019-04-10 22:31:51 +02:00
|
|
|
* Supports multiple MIME formats.
|
|
|
|
*
|
2019-04-11 22:22:37 +02:00
|
|
|
* Note that Data defined above is an alias:
|
|
|
|
* libraries can implement Renderer without importing gophernotes
|
2019-04-10 22:31:51 +02:00
|
|
|
*/
|
|
|
|
type Renderer = interface {
|
|
|
|
Render() Data
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-11 22:22:37 +02:00
|
|
|
* simplified interface, allows libraries to specify
|
|
|
|
* how their data is displayed by Jupyter.
|
2019-04-10 22:31:51 +02:00
|
|
|
* It only supports a single MIME format.
|
|
|
|
*
|
2019-04-11 22:22:37 +02:00
|
|
|
* Note that MIMEMap defined above is an alias:
|
|
|
|
* libraries can implement SimpleRenderer without importing gophernotes
|
2019-04-10 22:31:51 +02:00
|
|
|
*/
|
|
|
|
type SimpleRenderer = interface {
|
|
|
|
Render() MIMEMap
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* specialized interfaces, each is dedicated to a specific MIME type.
|
|
|
|
*
|
|
|
|
* They are type aliases to emphasize that method signatures
|
|
|
|
* are the only important thing, not the interface names.
|
|
|
|
* Thus libraries can implement them without importing gophernotes
|
|
|
|
*/
|
|
|
|
type HTMLer = interface {
|
|
|
|
HTML() string
|
|
|
|
}
|
2019-04-11 22:22:37 +02:00
|
|
|
type JavaScripter = interface {
|
2019-04-10 22:31:51 +02:00
|
|
|
JavaScript() string
|
|
|
|
}
|
|
|
|
type JPEGer = interface {
|
|
|
|
JPEG() []byte
|
|
|
|
}
|
|
|
|
type JSONer = interface {
|
|
|
|
JSON() map[string]interface{}
|
|
|
|
}
|
|
|
|
type Latexer = interface {
|
|
|
|
Latex() string
|
|
|
|
}
|
|
|
|
type Markdowner = interface {
|
|
|
|
Markdown() string
|
|
|
|
}
|
|
|
|
type PNGer = interface {
|
|
|
|
PNG() []byte
|
|
|
|
}
|
|
|
|
type PDFer = interface {
|
|
|
|
PDF() []byte
|
|
|
|
}
|
|
|
|
type SVGer = interface {
|
|
|
|
SVG() string
|
|
|
|
}
|
|
|
|
|
2018-06-04 21:55:35 +02:00
|
|
|
// injected as placeholder in the interpreter, it's then replaced at runtime
|
|
|
|
// by a closure that knows how to talk with Jupyter
|
2018-06-10 23:09:07 +02:00
|
|
|
func stubDisplay(Data) error {
|
|
|
|
return errors.New("cannot display: connection with Jupyter not available")
|
2018-06-04 21:55:35 +02:00
|
|
|
}
|
|
|
|
|
2019-04-11 22:22:37 +02:00
|
|
|
// return true if data type should be auto-rendered graphically
|
|
|
|
func canAutoRender(data interface{}) bool {
|
|
|
|
switch data.(type) {
|
|
|
|
case Data, Renderer, SimpleRenderer, HTMLer, JavaScripter, JPEGer, JSONer,
|
|
|
|
Latexer, Markdowner, PNGer, PDFer, SVGer, image.Image:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
2018-06-02 17:55:12 +02:00
|
|
|
|
2019-04-11 22:22:37 +02:00
|
|
|
// detect and render data types that should be auto-rendered graphically
|
|
|
|
func autoRender(mimeType string, data interface{}) Data {
|
|
|
|
var s string
|
2018-10-01 21:16:39 +02:00
|
|
|
var b []byte
|
2019-04-11 22:22:37 +02:00
|
|
|
var err error
|
|
|
|
var ret Data
|
|
|
|
switch data := data.(type) {
|
|
|
|
case Data:
|
|
|
|
ret = data
|
|
|
|
case Renderer:
|
|
|
|
ret = data.Render()
|
|
|
|
case SimpleRenderer:
|
|
|
|
ret.Data = data.Render()
|
|
|
|
case HTMLer:
|
|
|
|
s = data.HTML()
|
|
|
|
case JavaScripter:
|
|
|
|
mimeType = MIMETypeJavaScript
|
|
|
|
s = data.JavaScript()
|
|
|
|
case JPEGer:
|
|
|
|
mimeType = MIMETypeJPEG
|
|
|
|
b = data.JPEG()
|
|
|
|
case JSONer:
|
|
|
|
ret.Data = MIMEMap{MIMETypeJSON: data.JSON()}
|
|
|
|
case Latexer:
|
|
|
|
mimeType = MIMETypeLatex
|
|
|
|
s = data.Latex()
|
|
|
|
case Markdowner:
|
|
|
|
mimeType = MIMETypeMarkdown
|
|
|
|
s = data.Markdown()
|
|
|
|
case PNGer:
|
|
|
|
mimeType = MIMETypePNG
|
|
|
|
b = data.PNG()
|
|
|
|
case PDFer:
|
|
|
|
mimeType = MIMETypePDF
|
|
|
|
b = data.PDF()
|
|
|
|
case SVGer:
|
|
|
|
mimeType = MIMETypeSVG
|
|
|
|
s = data.SVG()
|
|
|
|
case image.Image:
|
|
|
|
b, mimeType, err = encodePng(data)
|
|
|
|
if err == nil {
|
|
|
|
ret.Metadata = imageMetadata(data)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
panic(fmt.Errorf("internal error, autoRender invoked on unexpected type %T", data))
|
|
|
|
}
|
|
|
|
return fillDefaults(ret, data, s, b, mimeType, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func fillDefaults(ret Data, data interface{}, s string, b []byte, mimeType string, err error) Data {
|
|
|
|
if err != nil {
|
|
|
|
return makeDataErr(err)
|
|
|
|
}
|
|
|
|
if ret.Data == nil {
|
|
|
|
ret.Data = make(MIMEMap)
|
|
|
|
}
|
|
|
|
if ret.Data[MIMETypeText] == "" {
|
|
|
|
if len(s) == 0 {
|
|
|
|
s = fmt.Sprint(data)
|
|
|
|
}
|
|
|
|
ret.Data[MIMETypeText] = s
|
|
|
|
}
|
|
|
|
if len(b) != 0 {
|
|
|
|
if len(mimeType) == 0 {
|
|
|
|
mimeType = http.DetectContentType(b)
|
|
|
|
}
|
|
|
|
if len(mimeType) != 0 && mimeType != MIMETypeText {
|
|
|
|
ret.Data[mimeType] = b
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
// do our best to render data graphically
|
|
|
|
func render(mimeType string, data interface{}) Data {
|
|
|
|
if canAutoRender(data) {
|
|
|
|
return autoRender(mimeType, data)
|
|
|
|
}
|
2018-10-01 21:16:39 +02:00
|
|
|
var s string
|
2019-04-11 22:22:37 +02:00
|
|
|
var b []byte
|
|
|
|
var err error
|
|
|
|
switch data := data.(type) {
|
2018-10-01 21:16:39 +02:00
|
|
|
case string:
|
2019-04-11 22:22:37 +02:00
|
|
|
s = data
|
2018-10-01 21:16:39 +02:00
|
|
|
case []byte:
|
2019-04-11 22:22:37 +02:00
|
|
|
b = data
|
2018-10-01 21:16:39 +02:00
|
|
|
case io.Reader:
|
2019-04-11 22:22:37 +02:00
|
|
|
b, err = ioutil.ReadAll(data)
|
2018-10-01 21:16:39 +02:00
|
|
|
case io.WriterTo:
|
|
|
|
var buf bytes.Buffer
|
2019-04-11 22:22:37 +02:00
|
|
|
data.WriteTo(&buf)
|
2018-10-01 21:16:39 +02:00
|
|
|
b = buf.Bytes()
|
|
|
|
default:
|
2019-04-11 22:22:37 +02:00
|
|
|
panic(fmt.Errorf("unsupported type, cannot render: %T", data))
|
2018-10-01 21:16:39 +02:00
|
|
|
}
|
2019-04-11 22:22:37 +02:00
|
|
|
return fillDefaults(Data{}, data, s, b, mimeType, err)
|
2018-10-01 21:16:39 +02:00
|
|
|
}
|
|
|
|
|
2019-04-11 22:22:37 +02:00
|
|
|
func makeDataErr(err error) Data {
|
|
|
|
return Data{
|
2019-04-10 22:15:44 +02:00
|
|
|
Data: MIMEMap{
|
2019-04-11 22:22:37 +02:00
|
|
|
"ename": "ERROR",
|
|
|
|
"evalue": err.Error(),
|
|
|
|
"traceback": nil,
|
|
|
|
"status": "error",
|
2018-10-01 21:16:39 +02:00
|
|
|
},
|
|
|
|
}
|
2019-04-11 22:22:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func Any(mimeType string, data interface{}) Data {
|
|
|
|
return render(mimeType, data)
|
2018-10-01 21:16:39 +02:00
|
|
|
}
|
|
|
|
|
2019-04-10 22:15:44 +02:00
|
|
|
// same as Any("", data), autodetects MIME type
|
|
|
|
func Auto(data interface{}) Data {
|
2019-04-11 22:22:37 +02:00
|
|
|
return render("", data)
|
2019-04-10 22:15:44 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func MakeData(mimeType string, data interface{}) Data {
|
2018-10-01 21:16:39 +02:00
|
|
|
d := Data{
|
2019-04-10 22:15:44 +02:00
|
|
|
Data: MIMEMap{
|
2018-10-01 21:16:39 +02:00
|
|
|
mimeType: data,
|
2018-06-04 21:55:35 +02:00
|
|
|
},
|
|
|
|
}
|
2019-04-11 22:22:37 +02:00
|
|
|
if mimeType != MIMETypeText {
|
|
|
|
d.Data[MIMETypeText] = fmt.Sprint(data)
|
2018-10-01 21:16:39 +02:00
|
|
|
}
|
|
|
|
return d
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func MakeData3(mimeType string, plaintext string, data interface{}) Data {
|
|
|
|
return Data{
|
2019-04-10 22:15:44 +02:00
|
|
|
Data: MIMEMap{
|
2019-04-11 22:22:37 +02:00
|
|
|
MIMETypeText: plaintext,
|
2018-06-02 17:55:12 +02:00
|
|
|
mimeType: data,
|
2018-06-04 21:55:35 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-01 21:16:39 +02:00
|
|
|
func File(mimeType string, path string) Data {
|
|
|
|
bytes, err := ioutil.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2019-04-10 22:15:44 +02:00
|
|
|
return Any(mimeType, bytes)
|
2018-10-01 21:16:39 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func HTML(html string) Data {
|
2019-04-10 22:15:44 +02:00
|
|
|
return MakeData(MIMETypeHTML, html)
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func JavaScript(javascript string) Data {
|
2019-04-10 22:15:44 +02:00
|
|
|
return MakeData(MIMETypeJavaScript, javascript)
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func JPEG(jpeg []byte) Data {
|
2019-04-10 22:15:44 +02:00
|
|
|
return MakeData(MIMETypeJPEG, jpeg)
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2019-04-10 22:31:51 +02:00
|
|
|
func JSON(json map[string]interface{}) Data {
|
|
|
|
return MakeData(MIMETypeJSON, json)
|
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func Latex(latex string) Data {
|
|
|
|
return MakeData3(MIMETypeLatex, latex, "$"+strings.Trim(latex, "$")+"$")
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func Markdown(markdown string) Data {
|
2019-04-10 22:15:44 +02:00
|
|
|
return MakeData(MIMETypeMarkdown, markdown)
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func Math(latex string) Data {
|
|
|
|
return MakeData3(MIMETypeLatex, latex, "$$"+strings.Trim(latex, "$")+"$$")
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func PDF(pdf []byte) Data {
|
2019-04-10 22:15:44 +02:00
|
|
|
return MakeData(MIMETypePDF, pdf)
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func PNG(png []byte) Data {
|
2019-04-10 22:15:44 +02:00
|
|
|
return MakeData(MIMETypePNG, png)
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
func SVG(svg string) Data {
|
2019-04-10 22:15:44 +02:00
|
|
|
return MakeData(MIMETypeSVG, svg)
|
2018-06-04 21:55:35 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 23:09:07 +02:00
|
|
|
// MIME encapsulates the data and metadata into a Data.
|
2018-06-04 21:55:35 +02:00
|
|
|
// The 'data' map is expected to contain at least one {key,value} pair,
|
|
|
|
// with value being a string, []byte or some other JSON serializable representation,
|
|
|
|
// and key equal to the MIME type of such value.
|
|
|
|
// The exact structure of value is determined by what the frontend expects.
|
|
|
|
// Some easier-to-use functions for common formats supported by the Jupyter frontend
|
2018-06-02 17:55:12 +02:00
|
|
|
// are provided by the various functions above.
|
2019-04-10 22:15:44 +02:00
|
|
|
func MIME(data, metadata MIMEMap) Data {
|
2018-06-10 23:09:07 +02:00
|
|
|
return Data{data, metadata, nil}
|
2018-06-02 17:55:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// prepare imports.Package for interpreted code
|
|
|
|
var display = imports.Package{
|
|
|
|
Binds: map[string]r.Value{
|
2018-10-01 21:16:39 +02:00
|
|
|
"Any": r.ValueOf(Any),
|
2019-04-10 22:15:44 +02:00
|
|
|
"Auto": r.ValueOf(Auto),
|
2018-10-01 21:16:39 +02:00
|
|
|
"File": r.ValueOf(File),
|
2018-06-04 21:55:35 +02:00
|
|
|
"HTML": r.ValueOf(HTML),
|
|
|
|
"Image": r.ValueOf(Image),
|
|
|
|
"JPEG": r.ValueOf(JPEG),
|
|
|
|
"JSON": r.ValueOf(JSON),
|
|
|
|
"JavaScript": r.ValueOf(JavaScript),
|
|
|
|
"Latex": r.ValueOf(Latex),
|
2018-06-10 23:09:07 +02:00
|
|
|
"MakeData": r.ValueOf(MakeData),
|
|
|
|
"MakeData3": r.ValueOf(MakeData3),
|
2018-06-04 21:55:35 +02:00
|
|
|
"Markdown": r.ValueOf(Markdown),
|
|
|
|
"Math": r.ValueOf(Math),
|
|
|
|
"MIME": r.ValueOf(MIME),
|
|
|
|
"MIMETypeHTML": r.ValueOf(MIMETypeHTML),
|
|
|
|
"MIMETypeJavaScript": r.ValueOf(MIMETypeJavaScript),
|
|
|
|
"MIMETypeJPEG": r.ValueOf(MIMETypeJPEG),
|
|
|
|
"MIMETypeJSON": r.ValueOf(MIMETypeJSON),
|
|
|
|
"MIMETypeLatex": r.ValueOf(MIMETypeLatex),
|
|
|
|
"MIMETypeMarkdown": r.ValueOf(MIMETypeMarkdown),
|
|
|
|
"MIMETypePDF": r.ValueOf(MIMETypePDF),
|
|
|
|
"MIMETypePNG": r.ValueOf(MIMETypePNG),
|
|
|
|
"MIMETypeSVG": r.ValueOf(MIMETypeSVG),
|
|
|
|
"PDF": r.ValueOf(PDF),
|
|
|
|
"PNG": r.ValueOf(PNG),
|
|
|
|
"SVG": r.ValueOf(SVG),
|
|
|
|
},
|
|
|
|
Types: map[string]r.Type{
|
2019-04-10 22:15:44 +02:00
|
|
|
"Data": r.TypeOf((*Data)(nil)).Elem(),
|
|
|
|
"MIMEMap": r.TypeOf((*MIMEMap)(nil)).Elem(),
|
2018-06-02 17:55:12 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2018-06-04 21:55:35 +02:00
|
|
|
// allow importing "display" and "github.com/gopherdata/gophernotes" packages
|
2018-06-02 17:55:12 +02:00
|
|
|
func init() {
|
|
|
|
imports.Packages["display"] = display
|
|
|
|
imports.Packages["github.com/gopherdata/gophernotes"] = display
|
|
|
|
}
|