1 4
module ElectronDisplay
2

3
export electrondisplay
4

5
using Electron, Base64, Markdown
6

7
import IteratorInterfaceExtensions, TableTraits, TableShowUtils
8

9
Base.@kwdef mutable struct ElectronDisplayConfig
10 6
    showable = electron_showable
11
    single_window::Bool = false
12
    focus::Bool = true
13
    max_json_bytes::Int = 2^20
14
end
15

16
"""
17
    setconfig(config; kwargs...)
18

19
Update a copy of `config` based on `kwargs`.
20
"""
21
setconfig(
22
    config::ElectronDisplayConfig;
23
    showable = config.showable,
24
    single_window::Bool = config.single_window,
25
    focus::Bool = config.focus,
26
    max_json_bytes::Int = config.max_json_bytes,
27 6
) =
28
    ElectronDisplayConfig(
29
        showable = showable,
30
        single_window = single_window,
31
        focus = focus,
32
        max_json_bytes = max_json_bytes,
33
    )
34

35
struct ElectronDisplayType <: Base.AbstractDisplay
36 6
    config::ElectronDisplayConfig
37
    window::Union{Window, Nothing}
38
end
39

40 6
ElectronDisplayType() = ElectronDisplayType(CONFIG, nothing)
41 6
newdisplay(window::Union{Window, Nothing} = nothing; config...) =
42
    ElectronDisplayType(setconfig(CONFIG; config...), window)
43

44 6
electron_showable(m, x) =
45
    m  ("application/vnd.dataresource+json", "text/html", "text/markdown") &&
46
    showable(m, x)
47

48
"""
49
    ElectronDisplay.CONFIG
50

51
Configuration for ElectronDisplay.
52

53
* `showable`: A callable with signature `showable(mime::String,
54
  x::Any) :: Bool`.  This determines if object `x` is displayed by
55
  `ElectronDisplay`.  Default is to return `false` if `mime` is
56
  `"text/html"` or `"text/markdown"` and otherwise fallbacks to
57
  `Base.showable(mime, x)`.
58

59
* `single_window::Bool = false`: If `true`, reuse existing window for
60
  displaying a new content.  If `false` (default), create a new window
61
  for each display.
62

63
* `focus::Bool = true`: Focus the Electron window on `display` if `true`
64
  (default).
65

66
* `max_json_bytes::Int = $(ElectronDisplayConfig().max_json_bytes)`:
67
  Maximum size in bytes for which JSON representation is used.  Otherwise,
68
  convert visualization locally in a binary form before sending it to the
69
  Electron display.  Currently only Vega and Vega-Lite support this.
70
"""
71
const CONFIG = ElectronDisplayConfig()
72

73
const _window = Ref{Window}()
74

75
function _getglobalwindow()
76 6
    if !(isdefined(_window, 1) && _window[].exists)
77 6
        _window[] = Electron.Window(
78
            URI("about:blank"),
79
            options=Dict("webPreferences" => Dict("webSecurity" => false)))
80
    end
81 6
    return _window[]
82
end
83

84
function displayhtml(d::ElectronDisplayType, payload; options::Dict=Dict{String,Any}())
85 6
    if d.window !== nothing || d.config.single_window
86 6
        w = d.window !== nothing ? d.window : _getglobalwindow()
87 6
        load(w, payload)
88 6
        showfun = get(options, "show", d.config.focus) ? "show" : "showInactive"
89 6
        run(w.app, "BrowserWindow.fromId($(w.id)).$showfun()")
90 6
        return w
91
    else
92 6
        options = Dict{String,Any}(options)
93 6
        get!(options, "show", d.config.focus)
94 6
        return Electron.Window(payload; options=options)
95
    end
96
end
97

98 6
displayhtmlbody(d::ElectronDisplayType, payload) =
99
    displayhtml(d, string(
100
        """
101
        <!doctype html>
102
        <html>
103

104
        <head>
105
        <meta name="viewport" content="width=device-width, initial-scale=1">
106
        <link rel="stylesheet" href="file:///$(asset("github-markdown-css", "github-markdown.css"))">
107
        <style>
108
            .markdown-body {
109
                box-sizing: border-box;
110
                padding: 15px;
111
            }
112
        </style>
113

114
        <link rel="stylesheet" href="file://$(asset("katex-0.11.1", "katex.min.css"))" integrity="sha384-zB1R0rpPzHqg7Kpt0Aljp8JPLqbXI3bhnPWROx27a9N0Ll6ZP/+DiW/UqRcLbRjq" crossorigin="anonymous">
115
        <script defer src="file://$(asset("katex-0.11.1", "katex.min.js"))" integrity="sha384-y23I5Q6l+B6vatafAwxRu/0oK/79VlbSz7Q9aiSZUvyWYIYsd+qj+o24G5ZU2zJz" crossorigin="anonymous"></script>
116
        <script defer src="file://$(asset("katex-0.11.1", "auto-render.min.js"))" integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous"
117
            onload="renderMathInElement(document.body, {delimiters: [{left: '\$', right: '\$', display: false}]});"></script>
118

119
        </head>
120
        <body>
121
        <article class="markdown-body">
122
        """,
123
        payload,
124
        """
125
         </article>
126
        </body>
127
         </html>
128
        """,
129
    ))
130

131

132
function Base.display(d::ElectronDisplayType, ::MIME{Symbol("text/html")}, x)
133 6
    html_page = repr("text/html", x)
134 6
    if occursin(r"<html\b"i, html_page)
135
        # Detect if object `x` rendered itself as a "standalone" HTML page.
136
        # If so, display it as-is:
137 0
        displayhtml(d, html_page)
138
    else
139
        # Otherwise, i.e., if `x` only produced an HTML fragment, apply
140
        # our default CSS to it:
141 6
        displayhtmlbody(d, html_page)
142
    end
143
end
144

145 6
Base.displayable(d::ElectronDisplayType, ::MIME{Symbol("text/html")}) = true
146

147 6
Base.display(d::ElectronDisplayType, ::MIME{Symbol("text/markdown")}, x) =
148
    displayhtmlbody(d, repr("text/html", asmarkdown(x)))
149

150 0
asmarkdown(x::Markdown.MD) = x
151 6
asmarkdown(x) = Markdown.parse(repr("text/markdown", x))
152

153 6
Base.displayable(d::ElectronDisplayType, ::MIME{Symbol("text/markdown")}) = true
154

155
function Base.display(d::ElectronDisplayType, ::MIME{Symbol("image/png")}, x)
156 6
    img = stringmime(MIME("image/png"), x)
157

158 6
    payload = string("<img src=\"data:image/png;base64,", img, "\"/>")
159

160 6
    displayhtml(d, payload)
161
end
162

163 6
Base.displayable(d::ElectronDisplayType, ::MIME{Symbol("image/png")}) = true
164

165
function Base.display(d::ElectronDisplayType, ::MIME{Symbol("image/svg+xml")}, x)
166 6
    payload = stringmime(MIME("image/svg+xml"), x)
167

168 6
    displayhtml(d, payload)
169
end
170

171 6
Base.displayable(d::ElectronDisplayType, ::MIME{Symbol("image/svg+xml")}) = true
172

173 6
asset(url...) = replace(normpath(joinpath(@__DIR__, "..", "assets", url...)), "\\" => "/")
174

175
function Base.display(d::ElectronDisplayType, ::MIME{Symbol("application/vnd.plotly.v1+json")}, x)
176 0
    payload = stringmime(MIME("application/vnd.plotly.v1+json"), x)
177

178 0
    html_page = """
179
    <html>
180

181
    <head>
182
        <script src="file:///$(asset("plotly", "plotly-latest.min.js"))"></script>
183
    </head>
184
    <body>
185
    </body>
186

187
    <script type="text/javascript">
188
        gd = (function() {
189
            var WIDTH_IN_PERCENT_OF_PARENT = 100
190
            var HEIGHT_IN_PERCENT_OF_PARENT = 100;
191
            var gd = Plotly.d3.select('body')
192
                .append('div').attr("id", "plotdiv")
193
                .style({
194
                    width: WIDTH_IN_PERCENT_OF_PARENT + '%',
195
                    'margin-left': (100 - WIDTH_IN_PERCENT_OF_PARENT) / 2 + '%',
196
                    height: HEIGHT_IN_PERCENT_OF_PARENT + 'vh',
197
                    'margin-top': (100 - HEIGHT_IN_PERCENT_OF_PARENT) / 2 + 'vh'
198
                })
199
                .node();
200
            var spec = $payload
201
            Plotly.newPlot(gd, spec.data, spec.layout);
202
            window.onresize = function() {
203
                Plotly.Plots.resize(gd);
204
                };
205
            return gd;
206
        })();
207
    </script>
208

209
    </html>
210
    """
211

212 0
    displayhtml(d, html_page, options=Dict("webPreferences" => Dict("webSecurity" => false)))
213
end
214

215 6
Base.displayable(d::ElectronDisplayType, ::MIME{Symbol("application/vnd.plotly.v1+json")}) = true
216

217
function Base.display(d::ElectronDisplayType, ::MIME{Symbol("application/vnd.dataresource+json")}, x)
218 6
    payload = stringmime(MIME("application/vnd.dataresource+json"), x)
219

220 6
    html_page = """
221
    <html>
222

223
    <head>
224
        <script src="file://$(asset("ag-grid", "ag-grid-community.min.noStyle.js"))"></script>
225
        <link rel="stylesheet" href="file://$(asset("ag-grid", "ag-grid.css"))">
226
        <link rel="stylesheet" href="file://$(asset("ag-grid", "ag-theme-balham.css"))">
227
    </head>
228
    <body>
229
        <div id="myGrid" style="height: 100%; width: 100%;" class="ag-theme-balham"></div>
230
    </body>
231

232
    <script type="text/javascript">
233
        var payload = $payload;
234
        var gridOptions = {
235
            onGridReady: event => event.api.sizeColumnsToFit(),
236
            onGridSizeChanged: event => event.api.sizeColumnsToFit(),
237
            defaultColDef: {
238
                resizable: true,
239
                filter: true,
240
                sortable: true
241
            },
242
            columnDefs: payload.schema.fields.map(function(x) {
243
                if (x.type == "number" || x.type == "integer") {
244
                    return {
245
                        field: x.name,
246
                        type: "numericColumn",
247
                        filter: "agNumberColumnFilter"
248
                    };
249
                } else if (x.type == "date") {
250
                    return {
251
                        field: x.name,
252
                        filter: "agDateColumnFilter"
253
                    };
254
                } else {
255
                    return {field: x.name};
256
                };
257
            }),
258
            rowData: payload.data
259
        };
260
        var eGridDiv = document.querySelector('#myGrid');
261
        new agGrid.Grid(eGridDiv, gridOptions);
262
    </script>
263

264
    </html>
265
    """
266

267 6
    displayhtml(d, html_page, options=Dict("webPreferences" => Dict("webSecurity" => false)))
268
end
269

270 6
Base.displayable(d::ElectronDisplayType, ::MIME{Symbol("application/vnd.dataresource+json")}) = true
271

272
function Base.display(d::ElectronDisplayType, x)
273 6
    showable = d.config.showable
274 6
    if showable("application/vnd.vegalite.v3+json", x)
275 6
        display(d,MIME("application/vnd.vegalite.v3+json"), x)
276 6
    elseif showable("application/vnd.vegalite.v2+json", x)
277 6
        display(d,MIME("application/vnd.vegalite.v2+json"), x)
278 6
    elseif showable("application/vnd.vega.v5+json", x)
279 6
        display(d,MIME("application/vnd.vega.v5+json"), x)
280 6
    elseif showable("application/vnd.vega.v4+json", x)
281 6
        display(d,MIME("application/vnd.vega.v4+json"), x)
282 6
    elseif showable("application/vnd.vega.v3+json", x)
283 6
        display(d,MIME("application/vnd.vega.v3+json"), x)
284 6
    elseif showable("application/vnd.plotly.v1+json", x)
285 0
        display(d,MIME("application/vnd.plotly.v1+json"), x)
286 6
    elseif showable("application/vnd.dataresource+json", x)
287 6
        display(d, "application/vnd.dataresource+json", x)
288 6
    elseif showable("image/svg+xml", x)
289 6
        display(d,"image/svg+xml", x)
290 6
    elseif showable("image/png", x)
291 6
        display(d,"image/png", x)
292 6
    elseif showable("text/html", x)
293 6
        display(d, "text/html", x)
294 6
    elseif showable("text/markdown", x)
295 6
        display(d, "text/markdown", x)
296
    else
297 0
        throw(MethodError(Base.display,(d,x)))
298
    end
299
end
300

301
"""
302
    electrondisplay([window,] [mime,] x; config...)
303

304
Show `x` in Electron `window`.  Use MIME `mime` if specified.  The keyword
305
arguments can be used to override [`ElectronDisplay.CONFIG`](@ref) without
306
mutating it.
307

308
# Examples
309
```julia
310
w = electrondisplay(@doc reduce; single_window=true, focus=false)
311
electrondisplay(w, @doc mapreduce)
312
```
313
"""
314 0
electrondisplay(mime, x; config...) =
315
    _electrondisplay(nothing, mime, x; config...)
316 6
electrondisplay(window::Window, mime, x; config...) =
317
    _electrondisplay(window, mime, x; config...)
318 6
_electrondisplay(window, mime, x; config...) =
319
    display(newdisplay(window; config...), mime, x)
320
# `_electrondisplay` is for not exposing implementation detail that
321
# `window = nothing` means the default window.
322

323
struct DataresourceTableTraitsWrapper{T}
324 0
    source::T
325
end
326

327
function Base.show(io::IO, ::MIME"application/vnd.dataresource+json", source::DataresourceTableTraitsWrapper)
328 0
    TableShowUtils.printdataresource(io, IteratorInterfaceExtensions.getiterator(source.source))
329
end
330

331 0
Base.showable(::MIME"application/vnd.dataresource+json", dt::DataresourceTableTraitsWrapper) = true
332

333
struct CachedDataResourceString
334
    content::String
335
end
336

337 0
Base.show(io::IO, ::MIME"application/vnd.dataresource+json", source::CachedDataResourceString) = print(io, source.content)
338

339 0
Base.showable(::MIME"application/vnd.dataresource+json", dt::CachedDataResourceString) = true
340

341 6
electrondisplay(x; config...) = _electrondisplay(nothing, x; config...)
342 6
electrondisplay(window::Window, x; config...) = _electrondisplay(window, x; config...)
343
function _electrondisplay(window, x; config...)
344 6
    d = newdisplay(window; showable=showable, config...)
345 6
    if TableTraits.isiterabletable(x)!==false
346 6
        if showable("application/vnd.dataresource+json", x)
347 6
            display(d, x)
348 0
        elseif TableTraits.isiterabletable(x)===true
349 0
            display(d, DataresourceTableTraitsWrapper(x))
350
        else
351 0
            try
352 0
                buffer = IOBuffer()
353 0
                TableShowUtils.printdataresource(buffer, IteratorInterfaceExtensions.getiterator(x))
354

355
                buffer_asstring = CachedDataResourceString(String(take!(buffer)))
356 0
                display(d, buffer_asstring)
357
            catch err
358 0
                display(d, x)
359
            end
360
        end
361
    else
362 6
        display(d, x)
363
    end
364
end
365

366
include("vega.jl")
367

368
function __init__()
369 6
    Base.Multimedia.pushdisplay(ElectronDisplayType())
370
end
371

372
end # module

Read our documentation on viewing source code .

Loading