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.
2 Answers
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
(* ::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[]
-
1$\begingroup$ I think rather than
ToString[ToExpression[...]]
you'll have better luck withMathLink`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$– b3m2a1Commented 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$– gdmCommented 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$– gdmCommented 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$– WizardCommented 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$– rhermansCommented Jul 28, 2021 at 15:10
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.
-
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$– GeremiaCommented Nov 11 at 20:47