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