23
$\begingroup$

I would like a tool to convert any Mathematica notebook into a Jupyter labs notebook, preserving as much content as possible (.ipynb files are just special json). Obviously, this would need to rasterize output cells and possibly cull any other non-textual elements wherever needed.

$\endgroup$
1

2 Answers 2

13
+125
$\begingroup$

The main point is that Jupyter is a flat structure, but the Notebook is a nested structure. It took me a long time to fully solve this problem.

Thanks for @b3m2a1 's advice, text problems has reduced.

Now there still some problem:

  • Picture rendering has incomprehensible edges
  • More issues not found, put test files at GalAster/JupyterConvert if find any.

Test file can be downloaded from: Test/Notebook2Jupyter/code.nb

enter image description here

(* ::Package:: *)

Notebook2Jupyter::usage = "";

Begin["`Private`"];
JupyterInputCell::usage = "";
JupyterCodeCell::usage = "";
JupyterMarkdownCell::usage = "";
JupyterRawCell::usage = "";


Options[Notebook2Jupyter] = {};
Notebook2Jupyter[nb_NotebookObject, o : OptionsPattern[]] := Block[
    {jp = $JupyterTemplate, parsed, cells},
    parsed = Flatten[parseCell /@ Cells[nb]];
    cells = SequenceSplit[parsed, {
        {text__JupyterMarkdownCell} :> JupyterMarkdownBuild[First /@ {text}],
        {in_JupyterInputCell, other___JupyterCodeCell} :> JupyterCodeBuild[First /@ {in, other}]
    }];
    jp["cells"] = cells;
    Return@jp;
];
Notebook2Jupyter[nb_NotebookObject, path_String, o : OptionsPattern[]] := Block[
    {jp = Notebook2Jupyter[nb, o]},
    File@Export[path, jp, "JSON"]
];


JupyterMarkdownBuild[text_List] := <|
    "cell_type" -> "markdown",
    "source" -> StringRiffle[text, "\n\n"]
|>;
JupyterCodeBuild[{code_}] := <|
    "cell_type" -> "code",
    "source" -> code
|>;
JupyterCodeBuild[{code_, out_}] := <|
    "cell_type" -> "code",
    "source" -> code,
    "outputs" -> {
        <|
            "output_type" -> "execute_result",
            "data" -> out
        |>
    }
|>;
JupyterCodeBuild[{code_, print__, out_}] := <|
    "cell_type" -> "code",
    "source" -> code,
    "outputs" -> Flatten@{
        <|"name" -> "stdout", "output_type" -> "stream", "text" -> #|>& /@ {print},
        <|
            "output_type" -> "execute_result",
            "data" -> out
        |>
    }
|>;


(* ::Chapter:: *)
(*Cell*)


(* ::Section:: *)
(*Template*)


$JupyterTemplate = <|
    "metadata" -> <||>
|>;


(* ::Section:: *)
(*Default*)


parseCell[co_CellObject] := parseCell[NotebookRead[co], co];
parseCell[c_Cell, co_CellObject] := parseCell[#2, #, co]& @@ c;
parseCell[s_, o___] := (
    Echo[Inactive[parseCell][s, o], "Todo: "];
    JupyterMarkdownCell@TemplateApply["[//]: # (No rules defined for ``)\n\n", {s}]
);


(* ::Section:: *)
(*Normal*)


parseCell["Title", data_, co_CellObject] := JupyterMarkdownCell["# " <> parseData@data];
parseCell["Subtitle", data_, co_CellObject] := JupyterMarkdownCell["## " <> parseData@data];
parseCell["Chapter", data_, co_CellObject] := JupyterMarkdownCell["### " <> parseData@data];
parseCell["Section", data_, co_CellObject] := JupyterMarkdownCell["#### " <> parseData@data];
parseCell["Subsection", data_, co_CellObject] := JupyterMarkdownCell["##### " <> parseData@data];
parseCell["Subsubsection", data_, co_CellObject] := JupyterMarkdownCell["###### " <> parseData@data];


parseCell["Text", data_, co_CellObject] := JupyterMarkdownCell[parseData@data];
parseCell["WolframAlphaShort", data_String, co_CellObject] := JupyterMarkdownCell[data];


(* ::Section:: *)
(*Code*)


toASCII[a_] := StringTake[ToString[a, InputForm, CharacterEncoding -> "ASCII"], {10, -2}];
parseCell["Input", boxes_, co_CellObject] := Block[
    {expr = MakeExpression[Cell@boxes, StandardForm], out},
    out = expr //. {
        HoldComplete[ExpressionCell[{a___, Null, b___}]] :> StringJoin[toASCII[HoldForm@a], ";\n", toASCII[HoldForm@b]],
        HoldComplete[ExpressionCell[a_]] :> toASCII[HoldForm@a]
    };
    JupyterInputCell[out]
];
parseCell["Print", boxes_, o___] := JupyterCodeCell[First@MathLink`CallFrontEnd[ExportPacket[Cell@boxes, "PlainText"]]];
parseCell["Echo", data___] := parseCell["Print", data];
parseCell["Message", data___] := parseCell["Print", data];
parseCell["Output", boxes_, co_CellObject] := Block[
    {dump = First@MathLink`CallFrontEnd[ExportPacket[Cell@boxes, "PlainText"]]},
    JupyterCodeCell@If[
        dump == "",
        <|"image/png" -> ExportString[Rasterize[co, Background -> None], {"Base64", "PNG"}, Background -> None]|>,
        <|"text/plain" -> dump|>
    ]
];


(* ::Section:: *)
(*TeX*)


boxesToTeX = ToString[ToExpression@#, TeXForm] &;
parseCell["Output", BoxData[FormBox[boxes_, TraditionalForm]], cellObj_CellObject] := TemplateApply["$$``$$\n\n", {boxesToTeX@boxes}];


(* ::Section:: *)
(*Pass*)



parseCell["Code", data___] := {};
parseCell[$Failed, data___] := {};


(* ::Chapter:: *)
(*Data*)


parseData[list_List] := parseData /@ list;
parseData[string_String] := string;
parseData[cell_Cell] := parseData@First@cell;
parseData[boxes_] := (
    Echo[Inactive[parseData][boxes], "Todo: "];
    parseData@First@boxes
);


parseData[data_BoxData] := List @@ (parseData /@ data);
parseData[data_TextData] := List @@ (parseData /@ data);


parseData[TemplateBox[{text_String, link_String}, "HyperlinkURL"]] := TemplateApply["[``](``)", {text, link}];




End[]
$\endgroup$
8
  • 1
    $\begingroup$ I think rather than ToString[ToExpression[...]] you'll have better luck with MathLink`CallFrontEnd[ExportPacket[boxes, "PlainText"] although "InputText" might also be a good approach. I think that can automatically go to "TeX" as well... I do a lot of this in my NotebookToMarkdown package. $\endgroup$
    – b3m2a1
    Commented Dec 8, 2019 at 19:15
  • 1
    $\begingroup$ Hi, is it possible to have a minimum advice on how to use this? Using Notebook2Jupyter["filenname.nb"] produces an unreadable .ipynb file... $\endgroup$
    – gdm
    Commented Apr 23, 2020 at 21:35
  • 1
    $\begingroup$ Maybe it is the fact that I am using it on the Wolfram Engine for Developers? It seems that NotebookRead function is not activated in this engine... Does anybody have the same problem? $\endgroup$
    – gdm
    Commented Apr 24, 2020 at 11:27
  • 1
    $\begingroup$ @gdm: Yes, the code uses NotebookOpen and NotebookRead, which seem to need the frontend, which the wolfram engine does not provide. Does anybody have a suggestion on how to modify the code without having to use the fronend? $\endgroup$
    – Wizard
    Commented Apr 28, 2020 at 14:50
  • $\begingroup$ This would be more useful if it could run in Wolfram Engine, as requested also in this other question. $\endgroup$
    – rhermans
    Commented Jul 28, 2021 at 15:10
10
$\begingroup$

This nb2ipynb function takes the notebook filename as input and it returns a jupyter notebook compatible with JWLS.

nb2ipynb = Module[{

  cellF = {
   "cell_type" -> "code",
   "execution_count" -> 1,
   "metadata" -> <||>,
   "outputs" -> {<|"name" -> "stdout",
                   "output_type" -> "stream",
                   "text" -> StringRiffle[ToString /@ {##2}, "\n"]|>},
   "source" -> ToString /@ List @@ HoldForm /@ (#1/.Null -> "\n\n")
  }&,

 ipynbF = {
   "cells" -> cellF @@@ #,
   "metadata" -> {"kernelspec" -> {"display_name" -> "JWLS_2",
                                   "language" -> "text",
                                   "name" -> "jwls_2"},
                  "language_info" -> {"codemirror_mode" -> "mathematica",
                                      "file_extension" -> ".wl",
                                      "mimetype" -> "text/x-mathematica",
                                      "name" -> "WolframScript"}},
   "nbformat" -> 4,
   "nbformat_minor" -> 2
  }&,

 nb = NotebookImport[# _,"FlattenCellGroups" -> False]~
      DeleteCases~ HoldComplete[Null]},

Export[FileBaseName@#<>".ipynb", ipynbF@nb, "JSON"]
]&

I haven't try it with the official WRI kernel but I guess you simply need to modify the metadata field.

$\endgroup$
2
  • 1
    $\begingroup$ This did not work for me, is there a mistake where NotebookImport[# _,"FlattenCellGroups" -> False] should be (note the comma between # and _ in the following)NotebookImport[#, _,"FlattenCellGroups" -> False]? However, even if I make that correction it still did not work for me. $\endgroup$ Commented Jun 18, 2023 at 10:30
  • 1
    $\begingroup$ @userrandrand I've opened an issue for that. $\endgroup$
    – Geremia
    Commented Nov 11 at 20:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.