CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/jupyter/ipynb/export-to-ipynb.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Exporting from our in-memory sync-friendly format to ipynb
8
*/
9
10
import * as immutable from "immutable";
11
import * as misc from "@cocalc/util/misc";
12
13
// In coffeescript still, so we at least say what we use of it here.
14
interface BlobStore {
15
get_ipynb: (string) => string;
16
}
17
18
export function export_to_ipynb(opts: any) {
19
opts = misc.defaults(opts, {
20
cell_list: misc.required,
21
cells: misc.required,
22
metadata: undefined, // custom metadata only
23
kernelspec: {}, // official jupyter will give an error on load without properly giving this (and ask to select a kernel)
24
language_info: undefined,
25
blob_store: undefined,
26
more_output: undefined,
27
}); // optional map id --> list of additional output messages to replace last output message.
28
29
const ipynb = {
30
cells: opts.cell_list.toJS().map((id: string) => cell_to_ipynb(id, opts)),
31
metadata: opts.metadata ? opts.metadata.toJS() || {} : {},
32
nbformat: 4,
33
nbformat_minor: 4,
34
};
35
36
ipynb.metadata.kernelspec = opts.kernelspec;
37
if (opts.language_info != null) {
38
ipynb.metadata.language_info = opts.language_info.toJS() || {};
39
}
40
41
return ipynb;
42
}
43
44
// Return ipynb version of the given cell as object
45
function cell_to_ipynb(id: string, opts: any) {
46
let left, left1, left2;
47
const cell = opts.cells.get(id);
48
const metadata: any = {};
49
const obj: any = {
50
cell_type: (left = cell.get("cell_type")) != null ? left : "code",
51
source: diff_friendly((left1 = cell.get("input")) != null ? left1 : ""),
52
metadata,
53
id,
54
};
55
56
// Handle any extra metadata (mostly user defined) that we don't handle in a special
57
// way for efficiency reasons.
58
const other_metadata = cell.get("metadata");
59
if (other_metadata != null) {
60
process_other_metadata(obj, other_metadata.toJS());
61
}
62
63
// consistenty with jupyter -- they explicitly give collapsed true or false state no matter what
64
metadata.collapsed = !!cell.get("collapsed");
65
66
// Jupyter only gives scrolled state when true.
67
if (cell.get("scrolled")) {
68
metadata.scrolled = true;
69
}
70
71
const exec_count = (left2 = cell.get("exec_count")) != null ? left2 : 0;
72
if (obj.cell_type === "code") {
73
obj.execution_count = exec_count;
74
}
75
76
process_slides(obj, cell.get("slide"));
77
process_attachments(obj, cell.get("attachments"), opts.blob_store);
78
process_tags(obj, cell.get("tags"));
79
80
if (obj.cell_type !== "code") {
81
// Code is the only cell type that is allowed to have an outputs field.
82
return obj;
83
}
84
85
const output = cell.get("output");
86
if ((output != null ? output.size : undefined) > 0) {
87
obj.outputs = ipynb_outputs(
88
output,
89
exec_count,
90
opts.more_output != null ? opts.more_output[id] : undefined,
91
opts.blob_store,
92
);
93
} else if (obj.outputs == null && obj.cell_type === "code") {
94
obj.outputs = []; // annoying requirement of ipynb file format.
95
}
96
for (const n in obj.outputs) {
97
const x = obj.outputs[n];
98
if (x.cocalc != null) {
99
// alternative version of cell that official Jupyter doesn't support can only
100
// stored in the **cell-level** metadata, not output.
101
if (metadata.cocalc == null) {
102
metadata.cocalc = { outputs: {} };
103
}
104
metadata.cocalc.outputs[n] = x.cocalc;
105
delete x.cocalc;
106
}
107
}
108
return obj;
109
}
110
111
function process_slides(obj: any, slide: any) {
112
if (slide != null) {
113
obj.metadata.slideshow = { slide_type: slide };
114
}
115
}
116
117
function process_tags(obj: any, tags: any) {
118
if (tags != null) {
119
// we store tags internally as an immutable js map (for easy
120
// efficient add/remove), but .ipynb uses a list.
121
obj.metadata.tags = misc.keys(tags.toJS()).sort();
122
}
123
}
124
125
function process_other_metadata(obj: any, other_metadata: any) {
126
if (other_metadata != null) {
127
Object.assign(obj.metadata, other_metadata);
128
}
129
}
130
131
function process_attachments(
132
obj: any,
133
attachments: any,
134
blob_store: BlobStore | undefined,
135
) {
136
if (attachments == null || blob_store == null) {
137
// don't have to or can't do anything (https://github.com/sagemathinc/cocalc/issues/4272)
138
return;
139
}
140
obj.attachments = {};
141
attachments.forEach((val: any, name: string) => {
142
if (val.get("type") !== "sha1") {
143
return; // didn't even upload
144
}
145
const sha1 = val.get("value");
146
const base64 = blob_store.get_ipynb(sha1);
147
let ext = misc.filename_extension(name);
148
if (ext === "jpg") {
149
ext = "jpeg";
150
}
151
obj.attachments[name] = { [`image/${ext}`]: base64 }; // TODO -- other types?
152
});
153
}
154
155
function ipynb_outputs(
156
output: any,
157
exec_count: any,
158
more_output: any,
159
blob_store: BlobStore | undefined,
160
) {
161
// If the last message has the more_output field, then there may be
162
// more output messages stored, which are not in the cells object.
163
if (output && output.getIn([`${output.size - 1}`, "more_output"]) != null) {
164
let n: number = output.size - 1;
165
const cnt = (more_output && (more_output.length || 0)) || 0;
166
if (cnt === 0) {
167
// For some reason more output is not available for this cell. So we replace
168
// the more_output message by an error explaining what happened.
169
output = output.set(
170
`${n}`,
171
immutable.fromJS({
172
text: "WARNING: Some output was deleted.\n",
173
name: "stderr",
174
}),
175
);
176
} else {
177
// Indeed, the last message has the more_output field.
178
// Before converting to ipynb, we remove that last message...
179
output = output.delete(`${n}`);
180
// Then we put in the known more output.
181
for (const mesg of more_output) {
182
output = output.set(`${n}`, immutable.fromJS(mesg));
183
n += 1;
184
}
185
}
186
}
187
// Now, everything continues as normal.
188
189
const outputs: any[] = [];
190
if (output && output.size > 0) {
191
for (let i = 0; i < output.size + 1; i++) {
192
const x = output?.get(`${i}`);
193
if (x != null && typeof x.toJS == "function") {
194
const output_n = x.toJS();
195
if (output_n != null) {
196
process_output_n(output_n, exec_count, blob_store);
197
outputs.push(output_n);
198
}
199
}
200
}
201
}
202
return outputs;
203
}
204
205
function process_output_n(
206
output_n: any,
207
exec_count: any,
208
blob_store: BlobStore | undefined,
209
) {
210
if (output_n == null) {
211
return;
212
}
213
if (output_n.exec_count != null) {
214
delete output_n.exec_count;
215
}
216
if (output_n.text != null) {
217
output_n.text = diff_friendly(output_n.text);
218
}
219
if (output_n.data != null) {
220
for (let k in output_n.data) {
221
const v = output_n.data[k];
222
if (k.slice(0, 5) === "text/") {
223
output_n.data[k] = diff_friendly(output_n.data[k]);
224
}
225
if (
226
misc.startswith(k, "image/") ||
227
k === "application/pdf" ||
228
k === "iframe"
229
) {
230
if (blob_store != null) {
231
const value = blob_store.get_ipynb(v);
232
if (value == null) {
233
// The image is no longer known; this could happen if the user reverts in the history
234
// browser and there is an image in the output that was not saved in the latest version.
235
// TODO: instead return an error.
236
return;
237
}
238
if (k === "iframe") {
239
delete output_n.data[k];
240
k = "text/html";
241
}
242
output_n.data[k] = value;
243
} else {
244
return; // impossible to include in the output without blob_store
245
}
246
}
247
}
248
output_n.output_type = "execute_result";
249
if (output_n.metadata == null) {
250
output_n.metadata = {};
251
}
252
output_n.execution_count = exec_count;
253
} else if (output_n.name != null) {
254
output_n.output_type = "stream";
255
if (output_n.name === "input") {
256
process_stdin_output(output_n);
257
}
258
} else if (output_n.ename != null) {
259
output_n.output_type = "error";
260
}
261
}
262
263
function process_stdin_output(output: any) {
264
output.cocalc = misc.deep_copy(output);
265
output.name = "stdout";
266
output.text =
267
output.opts.prompt + " " + (output.value != null ? output.value : "");
268
delete output.opts;
269
delete output.value;
270
}
271
272
// Transform a string s with newlines into an array v of strings
273
// such that v.join('') == s.
274
function diff_friendly(s: any) {
275
if (typeof s !== "string") {
276
// might already be an array or undefined.
277
return s;
278
}
279
const v = s.split("\n");
280
for (let i = 0; i < v.length - 1; i++) {
281
v[i] += "\n";
282
}
283
if (v[v.length - 1] === "") {
284
v.pop(); // remove last elt
285
}
286
return v;
287
}
288
289