├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── WolframLanguageForJupyter ├── Kernel │ └── init.m ├── PacletInfo.m ├── Resources │ ├── CompletionUtilities.wl │ ├── EvaluationUtilities.wl │ ├── Initialization.wl │ ├── KernelForWolframLanguageForJupyter.wl │ ├── MessagingUtilities.wl │ ├── OutputHandlingUtilities.wl │ ├── RequestHandlers.wl │ └── SocketUtilities.wl └── WolframLanguageForJupyter.m ├── configure-jupyter.wls ├── extras └── custom.js └── images ├── in-out-01.png ├── in-out-02.png ├── in-out-03.png └── menu-01.png /.gitignore: -------------------------------------------------------------------------------- 1 | .paclet 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Wolfram® 2 | 3 | Thank you for taking the time to contribute to the [Wolfram Research](https://github.com/wolframresearch) repos on GitHub. 4 | 5 | ## Licensing of Contributions 6 | 7 | By contributing to Wolfram, you agree and affirm that: 8 | 9 | > Wolfram may release your contribution under the terms of the [MIT license](https://opensource.org/licenses/MIT); and 10 | 11 | > You have read and agreed to the [Developer Certificate of Origin](http://developercertificate.org/), version 1.1 or later. 12 | 13 | Please see [LICENSE](LICENSE) for licensing conditions pertaining 14 | to individual repositories. 15 | 16 | 17 | ## Bug reports 18 | 19 | ### Security Bugs 20 | 21 | Please **DO NOT** file a public issue regarding a security issue. 22 | Rather, send your report privately to security@wolfram.com. Security 23 | reports are appreciated and we will credit you for it. We do not offer 24 | a security bounty, but the forecast in your neighborhood will be cloudy 25 | with a chance of Wolfram schwag! 26 | 27 | ### General Bugs 28 | 29 | Please use the repository issues page to submit general bug issues. 30 | 31 | Please do not duplicate issues. 32 | 33 | Please do send a complete and well-written report to us. Note: **the 34 | thoroughness of your report will positively correlate to our willingness 35 | and ability to address it**. 36 | 37 | When reporting issues, always include: 38 | 39 | * Your version of *Mathematica*® or the Wolfram Language. 40 | * Your operating system. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Wolfram Research Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wolfram Language kernel for Jupyter notebooks 2 | 3 | Jupyter provides a protocol (ZMQ) to connect their notebooks to various languages. This project defines a Wolfram Language kernel which can be used in Jupyter notebooks. 4 | 5 | # Prerequisites 6 | 7 | On your machine, you will need: 8 | 9 | * Jupyter 10 | * Wolfram Engine, i.e., a Wolfram Desktop or Mathematica installation 11 | * Optional, but recommended: `wolframscript` 12 | 13 | # Installation 14 | 15 | There are **two** ways to make the Wolfram Language available in Jupyter: 16 | 17 | * Using the `wolframscript` command line script interpreter 18 | * Using Wolfram Language commands from the `WolframLanguageForJupyter` paclet. 19 | 20 | ## Method 1: Using `wolframscript` 21 | 22 | On macOS/Unix: Clone the repository 23 | 24 | git clone https://github.com/WolframResearch/WolframLanguageForJupyter.git 25 | 26 | Run the following command in your shell to make the Wolfram Language engine available to Jupyter: 27 | 28 | ./configure-jupyter.wls add 29 | 30 | On Windows: Follow the fist two steps [here](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository), and on the the third step select `Download Zip`, and unzip the file using a tool for Windows. Open PowerShell in the directory of the unzipped folder 31 | 32 | Run the following command in your shell to make the Wolfram Language engine available to Jupyter: 33 | 34 | .\configure-jupyter.wls add 35 | 36 | **Notes:** 37 | 38 | * If the location of the Wolfram Engine changes, you will have to run `configure-jupyter.wls` again. 39 | 40 | * `configure-jupyter.wls` gives an error if the Wolfram Engine could not be added. 41 | 42 | For more configuration options run: 43 | 44 | ./configure-jupyter.wls help 45 | 46 | ## Method 2: Using Wolfram Language 47 | 48 | You can download the latest version of the paclet here: 49 | 50 | https://github.com/WolframResearch/WolframLanguageForJupyter/releases 51 | 52 | To install the paclet, run the following command with Wolfram Language (replacing x, y, and z with the correct values): 53 | 54 | PacletInstall["WolframLanguageForJupyter-x.y.z.paclet"] 55 | 56 | To load the paclet, run: 57 | 58 | Needs["WolframLanguageForJupyter`"] 59 | 60 | To add the Wolfram Language to Jupyter, run: 61 | 62 | ConfigureJupyter["Add"] 63 | 64 | To specify a specific Jupyter binary, run: 65 | 66 | ConfigureJupyter["Add", "JupyterInstallation" -> "..."] 67 | 68 | To specify a specific Wolfram Engine binary, run: 69 | 70 | ConfigureJupyter["Add", "WolframEngineBinary" -> "..." ] 71 | 72 | Please note, however, that the value for the `"WolframEngineBinary"` option should not be a `wolframscript` path. 73 | 74 | # Testing your installation 75 | 76 | The following command should now list the Wolfram Engine: 77 | 78 | jupyter kernelspec list 79 | 80 | The output should include a line like this: 81 | 82 | wolframlanguage12 C:\ProgramData\jupyter\kernels\wolframlanguage12 83 | 84 | ## To test your installation in a notebook, run the following command: 85 | 86 | jupyter notebook 87 | 88 | Then select Wolfram Language from the drop down menu: 89 | 90 | ![menu](images/menu-01.png) 91 | 92 | After the In[] prompt you can now type a Wolfram Language command (use shift-enter to evaluate): 93 | 94 | ![in-out-1](images/in-out-01.png) 95 | 96 | Outputs are either strings, for simple textual results, or images, for graphics and typeset results: 97 | 98 | ![in-out-2](images/in-out-02.png) 99 | 100 | Any messages that occur during evaluation are displayed: 101 | 102 | ![in-out-3](images/in-out-03.png) 103 | 104 | ## To test your installation in the terminal, run the following command: 105 | jupyter-console --kernel=wolframlanguage12 106 | 107 | # Building the WolframLanguageForJupyter paclet 108 | 109 | To build the WolframLanguageForJupyter paclet file yourself, run: 110 | 111 | ./configure-jupyter.wls build 112 | 113 | This creates the `WolframLanguageForJupyter-x.y.z.paclet` file (use the `PacletInfo.m` to increment the version). 114 | 115 | # Removing your installation 116 | 117 | ## Method 1: Using `wolframscript` 118 | 119 | Run the following command to remove the Wolfram Language engine from Jupyter: 120 | 121 | ./configure-jupyter.wls remove 122 | 123 | ## Method 2: Using Wolfram Language 124 | 125 | Run the following command: 126 | 127 | ConfigureJupyter["Remove"] 128 | 129 | # Links 130 | 131 | * https://github.com/WolframResearch/WolframLanguageForJupyter 132 | * https://jupyter.readthedocs.io/en/latest/projects/content-projects.html 133 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Kernel/init.m: -------------------------------------------------------------------------------- 1 | (* Wolfram Language Init File *) 2 | 3 | Get["WolframLanguageForJupyter`WolframLanguageForJupyter`"]; -------------------------------------------------------------------------------- /WolframLanguageForJupyter/PacletInfo.m: -------------------------------------------------------------------------------- 1 | (* ::Package:: *) 2 | 3 | (* Paclet Info File *) 4 | 5 | (* created Sept. 14 2018 *) 6 | 7 | Paclet[ 8 | Name -> "WolframLanguageForJupyter", 9 | Version -> "0.9.3", 10 | MathematicaVersion -> "11.2+", 11 | Extensions -> { 12 | {"Kernel", Context -> {"WolframLanguageForJupyter`"}}, 13 | {"Documentation", Language -> "English"} 14 | } 15 | ] 16 | 17 | 18 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Resources/CompletionUtilities.wl: -------------------------------------------------------------------------------- 1 | (************************************************ 2 | CompletionUtilities.wl 3 | ************************************************* 4 | Description: 5 | Utilities for the aiding in the 6 | (auto-)completion of Wolfram Language 7 | code 8 | Symbols defined: 9 | rewriteNamedCharacters 10 | *************************************************) 11 | 12 | (************************************ 13 | Get[] guard 14 | *************************************) 15 | 16 | If[ 17 | !TrueQ[WolframLanguageForJupyter`Private`$GotCompletionUtilities], 18 | 19 | WolframLanguageForJupyter`Private`$GotCompletionUtilities = True; 20 | 21 | (************************************ 22 | load required 23 | WolframLanguageForJupyter 24 | files 25 | *************************************) 26 | 27 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* unicodeNamedCharactersReplacements, 28 | verticalEllipsis *) 29 | 30 | (************************************ 31 | private symbols 32 | *************************************) 33 | 34 | (* begin the private context for WolframLanguageForJupyter *) 35 | Begin["`Private`"]; 36 | 37 | (************************************ 38 | utilities for rewriting 39 | Wolfram Language code 40 | *************************************) 41 | 42 | (* rewrite names (in a code string) into named characters *) 43 | rewriteNamedCharacters[codeToAnalyze_?StringQ] := 44 | Module[ 45 | {codeUsingFullReplacements}, 46 | codeUsingFullReplacements = 47 | StringReplace[ 48 | codeToAnalyze, 49 | Normal @ unicodeNamedCharactersReplacements 50 | ]; 51 | If[ 52 | StringCount[ 53 | codeUsingFullReplacements, 54 | verticalEllipsis | "\\[" 55 | ] != 1, 56 | Return[{codeUsingFullReplacements}]; 57 | ]; 58 | Return[ 59 | Flatten[ 60 | StringCases[ 61 | codeUsingFullReplacements, 62 | before___ ~~ name : ((verticalEllipsis | "\\[") ~~ rest__ ~~ EndOfString) :> 63 | ( 64 | (StringJoin[before, #1] &) /@ 65 | Values[ 66 | KeySelect[ 67 | unicodeNamedCharactersReplacements, 68 | StringMatchQ[#1, name ~~ ___] & 69 | ] 70 | ] 71 | ) 72 | ] 73 | ] 74 | ]; 75 | ]; 76 | 77 | (* end the private context for WolframLanguageForJupyter *) 78 | End[]; (* `Private` *) 79 | 80 | (************************************ 81 | Get[] guard 82 | *************************************) 83 | 84 | ] (* WolframLanguageForJupyter`Private`$GotCompletionUtilities *) 85 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Resources/EvaluationUtilities.wl: -------------------------------------------------------------------------------- 1 | (************************************************ 2 | EvaluationUtilities.wl 3 | ************************************************* 4 | Description: 5 | Utilities for evaluating input arriving 6 | from Jupyter, and for simulating 7 | the Mathematica REPL 8 | Symbols defined: 9 | Print, 10 | redirectPrint, 11 | redirectMessages, 12 | simulatedEvaluate 13 | *************************************************) 14 | 15 | (************************************ 16 | Get[] guard 17 | *************************************) 18 | 19 | If[ 20 | !TrueQ[WolframLanguageForJupyter`Private`$GotEvaluationUtilities], 21 | 22 | WolframLanguageForJupyter`Private`$GotEvaluationUtilities = True; 23 | 24 | (************************************ 25 | load required 26 | WolframLanguageForJupyter 27 | files 28 | *************************************) 29 | 30 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* loopState, applyHook, $canUseFrontEnd *) 31 | 32 | (************************************ 33 | wrapper for interacting 34 | with the cloud 35 | *************************************) 36 | 37 | (* Interact is a wrapper, open to the user, for asking that the result of the evaluation 38 | be displayed as an embedded cloud object that can be interacted with *) 39 | (* set Interact to not evaluate its arguments *) 40 | SetAttributes[Interact, HoldAll]; 41 | 42 | (************************************ 43 | private symbols 44 | *************************************) 45 | 46 | (* begin the private context for WolframLanguageForJupyter *) 47 | Begin["`Private`"]; 48 | 49 | (************************************ 50 | helper utilities for 51 | diagnosing and removing 52 | command wrappers 53 | (e.g., Interact[]) 54 | *************************************) 55 | 56 | (* check if expr is wrapped with Interact *) 57 | interactQ[expr___] := MatchQ[expr, Hold[Interact[___]]]; 58 | SetAttributes[interactQ, HoldAll]; 59 | 60 | (* remove any Interact wrappers, 61 | apply $Pre, 62 | and bring in the Front End for the evaluation of expr *) 63 | uninteract[Interact[expr_]] := UsingFrontEnd[applyHook[$Pre, expr]]; 64 | uninteract[expr_] := UsingFrontEnd[applyHook[$Pre, expr]]; 65 | SetAttributes[uninteract, HoldAll]; 66 | 67 | (************************************ 68 | version of Print that 69 | sends output to Jupyter 70 | *************************************) 71 | 72 | (* redirect Print calls into a message to Jupyter, in order to print in Jupyter *) 73 | (* TODO: review other methods: through EvaluationData or WSTP so we don't redefine Print *) 74 | (* TODO: remove this and just permanently set $Output to {..., loopState["WolframLanguageForJupyter-stdout"], ...} *) 75 | Unprotect[Print]; 76 | Print[ourArgs___, opts:OptionsPattern[]] := 77 | Block[ 78 | { 79 | $inPrint = True, 80 | $Output 81 | }, 82 | If[ 83 | loopState["printFunction"] =!= False, 84 | $Output = {OpenWrite[FormatType -> OutputForm]}; 85 | If[ 86 | !FailureQ[First[$Output]], 87 | Print[ourArgs, opts]; 88 | loopState["printFunction"][ 89 | Import[$Output[[1,1]], "String"] 90 | ]; 91 | Close[First[$Output]]; 92 | ]; 93 | ]; 94 | ] /; !TrueQ[$inPrint]; 95 | Protect[Print]; 96 | 97 | (************************************ 98 | version of Echo that 99 | sends output to Jupyter 100 | *************************************) 101 | 102 | (* redirect Echo calls into a message to Jupyter, in order to print/echo in Jupyter *) 103 | Echo[""]; 104 | Unprotect[Echo]; 105 | DownValues[Echo] = 106 | Prepend[ 107 | DownValues[Echo], 108 | HoldPattern[Echo[ourArgs___, opts:OptionsPattern[]]] :> Block[{$inEcho = True, $Notebooks = False}, Echo[ourArgs, opts]] /; !TrueQ[$inEcho] 109 | ]; 110 | Protect[Echo]; 111 | 112 | (************************************ 113 | version of Write that 114 | sends output to Jupyter 115 | *************************************) 116 | 117 | (* redirect Write["stdout", ourArgs___] calls to Write[loopState["WolframLanguageForJupyter-stdout"], ourArgs___], 118 | in order to print in Jupyter *) 119 | Unprotect[Write]; 120 | Write["stdout", ourArgs___, opts:OptionsPattern[]] := 121 | Block[ 122 | { 123 | $inWrite = True 124 | }, 125 | If[ 126 | loopState["WolframLanguageForJupyter-stdout"] =!= False, 127 | Write[loopState["WolframLanguageForJupyter-stdout"], ourArgs] 128 | ]; 129 | ] /; !TrueQ[$inWrite]; 130 | Protect[Write]; 131 | (* redirect Write[{before___, "stdout", after___}, ourArgs___] calls to 132 | Write[{before___, "WolframLanguageForJupyter-stdout", after___}, ourArgs___], 133 | in order to print in Jupyter *) 134 | Unprotect[Write]; 135 | Write[{before___, "stdout", after___}, ourArgs___, opts:OptionsPattern[]] := 136 | Block[ 137 | { 138 | $inWrite = True 139 | }, 140 | If[ 141 | loopState["WolframLanguageForJupyter-stdout"] =!= False, 142 | Write[{before, loopState["WolframLanguageForJupyter-stdout"], after}, ourArgs] 143 | ]; 144 | ] /; !TrueQ[$inWrite]; 145 | Protect[Write]; 146 | 147 | (************************************ 148 | version of WriteString that 149 | sends output to Jupyter 150 | *************************************) 151 | 152 | (* redirect WriteString["stdout", ourArgs___] calls to WriteString[loopState["WolframLanguageForJupyter-stdout"], ourArgs___], 153 | in order to print in Jupyter *) 154 | Unprotect[WriteString]; 155 | WriteString["stdout", ourArgs___, opts:OptionsPattern[]] := 156 | Block[ 157 | { 158 | $inWriteString = True 159 | }, 160 | If[ 161 | loopState["WolframLanguageForJupyter-stdout"] =!= False, 162 | WriteString[loopState["WolframLanguageForJupyter-stdout"], ourArgs] 163 | ]; 164 | ] /; !TrueQ[$inWriteString]; 165 | Protect[WriteString]; 166 | (* redirect WriteString[{before___, "stdout", after___}, ourArgs___] calls to 167 | WriteString[{before___, "WolframLanguageForJupyter-stdout", after___}, ourArgs___], 168 | in order to print in Jupyter *) 169 | Unprotect[WriteString]; 170 | WriteString[{before___, "stdout", after___}, ourArgs___, opts:OptionsPattern[]] := 171 | Block[ 172 | { 173 | $inWriteString = True 174 | }, 175 | If[ 176 | loopState["WolframLanguageForJupyter-stdout"] =!= False, 177 | WriteString[{before, loopState["WolframLanguageForJupyter-stdout"], after}, ourArgs] 178 | ]; 179 | ] /; !TrueQ[$inWriteString]; 180 | Protect[WriteString]; 181 | 182 | (************************************ 183 | versions of Quit and Exit that 184 | ask the Jupyter console 185 | to quit, if running under 186 | a Jupyter console 187 | *************************************) 188 | 189 | Unprotect[Quit]; 190 | Quit[ourArgs___] := 191 | Block[ 192 | {$inQuit = True}, 193 | If[ 194 | loopState["isCompleteRequestSent"], 195 | loopState["askExit"] = True;, 196 | Quit[ourArgs]; 197 | ]; 198 | ] /; 199 | ( 200 | (!TrueQ[$inQuit]) && 201 | ( 202 | (Length[{ourArgs}] == 0) || 203 | ((Length[{ourArgs}] == 1) && IntegerQ[ourArgs]) 204 | ) 205 | ); 206 | Protect[Quit]; 207 | 208 | Unprotect[Exit]; 209 | Exit[ourArgs___] := 210 | Block[ 211 | {$inExit = True}, 212 | If[ 213 | loopState["isCompleteRequestSent"], 214 | loopState["askExit"] = True;, 215 | Exit[ourArgs]; 216 | ]; 217 | ] /; 218 | ( 219 | (!TrueQ[$inExit]) && 220 | ( 221 | (Length[{ourArgs}] == 0) || 222 | ((Length[{ourArgs}] == 1) && IntegerQ[ourArgs]) 223 | ) 224 | ); 225 | Protect[Exit]; 226 | 227 | (************************************ 228 | redirection utilities 229 | *************************************) 230 | 231 | (* redirect Print to Jupyter *) 232 | redirectPrint[currentSourceFrame_, printText_] := 233 | (* send a frame *) 234 | sendFrame[ 235 | (* on the IO Publish socket *) 236 | ioPubSocket, 237 | (* create the frame *) 238 | createReplyFrame[ 239 | (* using the current source frame *) 240 | currentSourceFrame, 241 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#streams-stdout-stderr-etc *) 242 | (* with a message type of "stream" *) 243 | "stream", 244 | (* and with message content that tells Jupyter what to Print, and to use stdout *) 245 | ExportString[ 246 | Association[ 247 | "name" -> "stdout", 248 | "text" -> printText 249 | ], 250 | "JSON", 251 | "Compact" -> True 252 | ], 253 | (* and without branching off *) 254 | False 255 | ] 256 | ]; 257 | 258 | (* redirect messages to Jupyter *) 259 | redirectMessages[currentSourceFrame_, messageName_, messageText_, addNewline_, dropMessageName_:False] := 260 | Module[ 261 | { 262 | (* string forms of the arguments messageName and messageText *) 263 | messageNameString, 264 | messageTextString 265 | }, 266 | (* generate string forms of the arguments *) 267 | messageNameString = ToString[HoldForm[messageName]]; 268 | messageTextString = ToString[messageText]; 269 | (* send a frame *) 270 | sendFrame[ 271 | (* on the IO Publish socket *) 272 | ioPubSocket, 273 | (* create the frame *) 274 | createReplyFrame[ 275 | (* using the current source frame *) 276 | currentSourceFrame, 277 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#execution-errors *) 278 | (* with a message type of "error" *) 279 | "error", 280 | (* and with appropriate message content *) 281 | ExportString[ 282 | Association[ 283 | (* use the provided message name here *) 284 | "ename" -> messageNameString, 285 | (* use the provided message text here *) 286 | "evalue" -> messageTextString, 287 | (* use the provided message name and message text here (unless dropMessageName) *) 288 | "traceback" -> 289 | { 290 | (* output the message in red *) 291 | StringJoin[ 292 | "\033[0;31m", 293 | If[ 294 | dropMessageName, 295 | (* use only the message text here if dropMessageName is True *) 296 | messageTextString, 297 | (* otherwise, combine the message name and message text *) 298 | ToString[System`ColonForm[HoldForm[messageName], messageText]] 299 | ], 300 | (* if addNewline, add a newline *) 301 | If[addNewline, "\n", ""], 302 | "\033[0m" 303 | ] 304 | } 305 | ], 306 | "JSON", 307 | "Compact" -> True 308 | ], 309 | (* and without branching off *) 310 | False 311 | ] 312 | ]; 313 | (* ... and return an empty string to the Wolfram Language message system *) 314 | Return[""]; 315 | ]; 316 | SetAttributes[redirectMessages, HoldAll]; 317 | 318 | (************************************ 319 | utilities for splitting 320 | input by Wolfram Language 321 | expression 322 | *************************************) 323 | 324 | (* start parsing the input, and return an Association for keeping track of said parse *) 325 | startParsingInput[codeStr_] := 326 | Module[ 327 | { 328 | (* an Association for keeping track of the parse containing: 329 | "LinesLeft" - the lines of input left to be processed, 330 | "ExpressionsParsed" - the number of expressions parsed out so far, 331 | "ExpressionString" - an expression string generated by a pass of the parser, 332 | "SyntaxError" - a flag for if the expression string contains a syntax error, 333 | "ParseDone" - a flag for if the parse is done *) 334 | parseTrackerInit 335 | }, 336 | 337 | (* initialize parseTrackerInit *) 338 | parseTrackerInit = Association[]; 339 | 340 | (* add the annotated lines left, and the defaults *) 341 | AssociateTo[ 342 | parseTrackerInit, 343 | { 344 | "LinesLeft" -> StringSplit[StringJoin[" ", codeStr], "\r\n" | "\n"], 345 | "ExpressionsParsed" -> 0, 346 | "ExpressionString" -> " ", 347 | "SyntaxError" -> False, 348 | "ParseDone" -> False 349 | } 350 | ]; 351 | 352 | (* return the parse tracker *) 353 | Return[parseTrackerInit]; 354 | ]; 355 | 356 | (* parse out an expression *) 357 | parseIntoExpr[tracker_] := 358 | Module[ 359 | { 360 | (* the parse tracker to be updated with the results of this parse *) 361 | newTracker, 362 | 363 | (* for storing the lines of input left to be processed, annotated with their line numbers *) 364 | annotatedLinesLeft, 365 | 366 | (* for storing the current line position when moving through annotatedLinesLeft *) 367 | linePos, 368 | 369 | (* for storing the result *) 370 | result, 371 | 372 | (* for storing the concatenation of some lines of input *) 373 | exprStr 374 | }, 375 | 376 | (* initialize the parse tracker to be updated *) 377 | newTracker = tracker; 378 | 379 | (* if there are no lines of input left to be processed, 380 | set newTracker["ParseDone"] to True and return the updated tracker *) 381 | If[Length[newTracker["LinesLeft"]] == 0, 382 | newTracker["ParseDone"] = True; 383 | (* return the updated tracker *) 384 | Return[newTracker]; 385 | ]; 386 | 387 | (* annotate the lines of input left to be processed with their line numbers *) 388 | annotatedLinesLeft = 389 | Partition[ 390 | Riffle[ 391 | newTracker["LinesLeft"], 392 | Range[Length[newTracker["LinesLeft"]]] 393 | ], 394 | 2 395 | ]; 396 | 397 | (* start with a line position of 1 *) 398 | linePos = 1; 399 | result = 400 | (* fold until an expression is built, or there are no lines of input left *) 401 | Fold[ 402 | ( 403 | (* save the new line position *) 404 | linePos = Last[{##}][[2]] + 1; 405 | (* save the new expression string with a new line of input *) 406 | exprStr = StringJoin[First /@ {##}]; 407 | (* if exprStr is syntactically correct, it represents a complete expression, 408 | and folding can be stopped *) 409 | If[SyntaxQ[exprStr], 410 | (* return the complete expression string *) 411 | Return[{exprStr, linePos, False}, Fold];, 412 | (* exprStr may be incomplete, keep going if possible *) 413 | {exprStr, linePos, True} 414 | ] 415 | ) &, 416 | (* starting value *) 417 | {"", -1, False}, 418 | (* the annotated lines of input left to be processed *) 419 | annotatedLinesLeft 420 | ]; 421 | 422 | AssociateTo[ 423 | newTracker, 424 | { 425 | (* discard the lines of input processed *) 426 | "LinesLeft" -> newTracker["LinesLeft"][[result[[2]];;]], 427 | (* increment ExpressionsParsed *) 428 | "ExpressionsParsed" -> newTracker["ExpressionsParsed"] + 1, 429 | (* save the result generated *) 430 | "ExpressionString" -> result[[1]], 431 | (* save the syntactic correctness of the result *) 432 | "SyntaxError" -> result[[3]] 433 | } 434 | ]; 435 | 436 | (* return the updated tracker *) 437 | Return[newTracker]; 438 | ]; 439 | 440 | (************************************ 441 | main evaluation command 442 | *************************************) 443 | 444 | (* evaluate input, and capture required information such as generated messages *) 445 | (* TODO: review other method: evaluate input through WSTP in another kernel *) 446 | simulatedEvaluate[codeStr_] := 447 | Module[ 448 | { 449 | (* for saving $Messages before changing it *) 450 | oldMessages, 451 | (* the stream used to capture generated messages *) 452 | stream, 453 | (* the string form of the generated messages, obtained from stream *) 454 | generatedMessages, 455 | 456 | (* for keeping track of parsing the input into separate expressions *) 457 | parseTracker, 458 | 459 | (* a raw evaluation result to be built, before Nulls have been removed *) 460 | rawEvaluationResult, 461 | 462 | (* for storing a single expression string *) 463 | exprStr, 464 | 465 | (* the result of evaluation *) 466 | evaluationResult, 467 | 468 | (* for storing final results *) 469 | result, 470 | 471 | (* the total result of the evaluation: 472 | an association containing 473 | the result of evaluation ("EvaluationResult"), 474 | indices of the output lines of the result ("EvaluationResultOutputLineIndices"), 475 | the total number of indices consumed by this evaluation ("ConsumedIndices"), 476 | generated messages ("GeneratedMessages"), 477 | if the input was one expression and wrapped with Interact[] ("InteractStatus") 478 | *) 479 | totalResult 480 | }, 481 | 482 | (* create the association for the total result of the evaluation *) 483 | totalResult = Association[]; 484 | 485 | (* save $Messages before overwrite *) 486 | oldMessages = $Messages; 487 | (* open stream to write messages into *) 488 | stream = OpenWrite[]; 489 | 490 | (* clear the list of generated messages *) 491 | Unprotect[$MessageList]; $MessageList = {}; Protect[$MessageList]; 492 | 493 | (* set $Messages to use the new stream *) 494 | $Messages = {stream}; 495 | 496 | (* start the parse of the input *) 497 | parseTracker = 498 | startParsingInput[ 499 | (* apply $PreRead to the input *) 500 | applyHook[$PreRead, codeStr] 501 | ]; 502 | 503 | (* initialize rawEvaluationResult to an empty list *) 504 | rawEvaluationResult = {}; 505 | (* while the parse is not done, keep evaluating expressions in the input *) 506 | While[ 507 | ( 508 | parseTracker = parseIntoExpr[parseTracker]; 509 | !parseTracker["ParseDone"] 510 | ), 511 | 512 | (* save the current expression string *) 513 | exprStr = parseTracker["ExpressionString"]; 514 | 515 | If[ 516 | !parseTracker["SyntaxError"], 517 | (* increment $Line *) 518 | $Line++; 519 | (* set InString *) 520 | Unprotect[InString]; 521 | InString[ 522 | loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1 523 | ] = exprStr; 524 | Protect[InString]; 525 | ]; 526 | 527 | (* evaluate the expression string *) 528 | (* regarding Internal`AllowExceptions, we need to generate the results and messages that 529 | are expected when a user evaluation is interrupted by behavior such as an uncaught Throw 530 | statement, while making sure that the simulated evaluation loop is not interrupted by the 531 | same behavior; my hope is that we can achieve this by using Internal`AllowExceptions as 532 | essentially a version of CheckAll that does not silence messages such as Throw::nocatch *) 533 | result = 534 | Internal`AllowExceptions[ 535 | ToExpression[ 536 | exprStr, 537 | InputForm, 538 | uninteract 539 | ] 540 | ]; 541 | 542 | If[ 543 | !parseTracker["SyntaxError"], 544 | (* set the In[] for this expression *) 545 | Unprotect[In]; 546 | Replace[ 547 | ToExpression[exprStr, InputForm, Hold], 548 | Hold[held_] :> 549 | SetDelayed[ 550 | In[ 551 | loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1 552 | ], 553 | held 554 | ] 555 | ]; 556 | Protect[In]; 557 | (* apply $Post to the result *) 558 | result = applyHook[$Post, result]; 559 | (* set the Out[] for this expression *) 560 | Unprotect[Out]; 561 | Out[loopState["executionCount"] + parseTracker["ExpressionsParsed"] - 1] = result; 562 | Protect[Out]; 563 | (* create the overall result with $PrePrint *) 564 | result = applyHook[$PrePrint, result]; 565 | , 566 | (* syntax error *) 567 | result = $Failed; 568 | ]; 569 | 570 | (* save the result in rawEvaluationResult *) 571 | AppendTo[rawEvaluationResult, result]; 572 | ]; 573 | 574 | (* add the Interact[] wrapper status of the input *) 575 | AssociateTo[ 576 | totalResult, 577 | "InteractStatus" -> 578 | ( 579 | (* if the input has no syntax errors, 580 | is made up of only one expression, 581 | and is wrapped with Interact[], 582 | mark "InteractStatus" as True 583 | *) 584 | parseTracker["ExpressionsParsed"] == 1 && 585 | !parseTracker["SyntaxError"] && 586 | (interactQ @ ToExpression[parseTracker["ExpressionString"], InputForm, Hold]) 587 | ) 588 | ]; 589 | 590 | (* evaluate the input from Jupyter, removing Nulls from the Output *) 591 | evaluationResult = DeleteCases[rawEvaluationResult, Null]; 592 | 593 | (* preserve the locations of the output lines with respect to the Nulls *) 594 | AssociateTo[ 595 | totalResult, 596 | "EvaluationResultOutputLineIndices" -> 597 | ( 598 | (loopState["executionCount"] - 1) + 599 | Flatten[Position[rawEvaluationResult, Except[Null], {1}, Heads -> False]] 600 | ) 601 | ]; 602 | 603 | (* restore $Messages *) 604 | $Messages = oldMessages; 605 | 606 | (* obtain generated messages *) 607 | generatedMessages = Import[stream[[1]], "String"]; 608 | (* close the opened stream *) 609 | Close[stream]; 610 | 611 | (* add the total number of indices consumed by this evaluation *) 612 | AssociateTo[ 613 | totalResult, 614 | "ConsumedIndices" -> 615 | (* if parseTracker["SyntaxError"] is true, one less index was consumed *) 616 | If[ 617 | parseTracker["SyntaxError"], 618 | parseTracker["ExpressionsParsed"] - 1, 619 | parseTracker["ExpressionsParsed"] 620 | ] 621 | ]; 622 | 623 | (* add the result of the evaluation and any generated messages to totalResult *) 624 | AssociateTo[totalResult, {"EvaluationResult" -> evaluationResult, "GeneratedMessages" -> generatedMessages}]; 625 | 626 | (* return totalResult *) 627 | Return[totalResult]; 628 | ]; 629 | (* set simulatedEvaluate to not implicitly evaluate its arguments *) 630 | SetAttributes[simulatedEvaluate, HoldAll]; 631 | 632 | (* end the private context for WolframLanguageForJupyter *) 633 | End[]; (* `Private` *) 634 | 635 | (************************************ 636 | Get[] guard 637 | *************************************) 638 | 639 | ] (* WolframLanguageForJupyter`Private`$GotEvaluationUtilities *) 640 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Resources/Initialization.wl: -------------------------------------------------------------------------------- 1 | (************************************************ 2 | Initialization.wl 3 | ************************************************* 4 | Description: 5 | Initialization for 6 | WolframLanguageForJupyter 7 | Symbols defined: 8 | $defaultPageWidth, 9 | loopState, 10 | applyHook, 11 | $canUseFrontEnd, 12 | $outputSetToTraditionalForm, 13 | $outputSetToTeXForm, 14 | $trueFormatType, 15 | $truePageWidth, 16 | connectionAssoc, 17 | bannerWarning, 18 | keyString, 19 | baseString, 20 | heartbeatString, 21 | verticalEllipsis, 22 | unicodeNamedCharactersReplacements, 23 | ioPubString, 24 | controlString, 25 | inputString, 26 | shellString, 27 | ioPubSocket, 28 | controlSocket, 29 | inputSocket, 30 | shellSocket, 31 | heldLocalSubmit 32 | *************************************************) 33 | 34 | (************************************ 35 | Get[] guard 36 | *************************************) 37 | 38 | If[ 39 | !TrueQ[WolframLanguageForJupyter`Private`$GotInitialization], 40 | 41 | WolframLanguageForJupyter`Private`$GotInitialization = True; 42 | 43 | (************************************ 44 | get required paclets 45 | *************************************) 46 | 47 | (* obtain ZMQ utilities *) 48 | Needs["ZeroMQLink`"]; (* SocketOpen *) 49 | 50 | (************************************ 51 | private symbols 52 | *************************************) 53 | 54 | (* begin the private context for WolframLanguageForJupyter *) 55 | Begin["`Private`"]; 56 | 57 | (************************************ 58 | set noise settings 59 | *************************************) 60 | 61 | (* make Short[] work *) 62 | $defaultPageWidth = 89; 63 | SetOptions[$Output, PageWidth -> $defaultPageWidth]; 64 | 65 | (* do not output messages to the jupyter notebook invocation 66 | $Messages = {}; 67 | $Output = {}; *) 68 | 69 | (************************************ 70 | discover the named unicode 71 | characters and their names 72 | *************************************) 73 | 74 | (* the vertical ellipsis character *) 75 | verticalEllipsis = FromCharacterCode[8942, "Unicode"]; 76 | 77 | (* pre-define the association of names and named characters as empty *) 78 | unicodeNamedCharactersReplacements = Association[]; 79 | 80 | Block[ 81 | { 82 | (* the absolute file name for "UnicodeCharacters.tr" *) 83 | unicodeCharactersTRFileName, 84 | 85 | (* raw data extracted from "UnicodeCharacters.tr" *) 86 | charactersAndTheirNames 87 | }, 88 | 89 | (* attempt to get the full location of "UnicodeCharacters.tr" *) 90 | unicodeCharactersTRFileName = UsingFrontEnd[System`Dump`unicodeCharactersTR]; 91 | 92 | (* try again if using System`Dump`unicodeCharactersTR does not work *) 93 | If[ 94 | !StringQ[unicodeCharactersTRFileName], 95 | unicodeCharactersTRFileName = 96 | UsingFrontEnd[ 97 | ToFileName[ 98 | FrontEnd`FileName[ 99 | { 100 | $InstallationDirectory, 101 | "SystemFiles", 102 | "FrontEnd", 103 | "TextResources" 104 | }, 105 | "UnicodeCharacters.tr" 106 | ] 107 | ] 108 | ]; 109 | ]; 110 | 111 | If[ 112 | StringQ[unicodeCharactersTRFileName], 113 | charactersAndTheirNames = 114 | ( 115 | (* parse the third item of a row (for a named character) into a list *) 116 | ReplacePart[ 117 | #1, 118 | 3 -> 119 | StringCases[ 120 | (* remove extraneous parentheses *) 121 | StringTrim[ 122 | #1[[3]], 123 | "(" | ")" 124 | ], 125 | (* extract the escape sequences for the named character *) 126 | Longest[escSeq : (Except[WhitespaceCharacter] ..)] :> 127 | StringJoin[verticalEllipsis, StringTrim[escSeq, "$"], verticalEllipsis] 128 | ] 129 | ] & 130 | ) /@ 131 | (* only use lists with at least three items *) 132 | Select[ 133 | (* parse the rows into their items (where each item is separated by two tabs) *) 134 | (StringSplit[#1, "\t\t"] &) /@ 135 | ( 136 | (* split unicodeCharactersTRFileName into its lines *) 137 | StringSplit[ 138 | Import[unicodeCharactersTRFileName, "String"], 139 | "\n" 140 | (* drop the first row *) 141 | ][[2 ;;]] 142 | ), 143 | (Length[#1] >= 3) & 144 | ]; 145 | 146 | (* parse the data into an association of names and the named characters they correspond to *) 147 | unicodeNamedCharactersReplacements = 148 | (* sort the keys by string length *) 149 | KeySort[ 150 | (* sort the keys in the default manner *) 151 | KeySort[ 152 | (* drop "empty names" *) 153 | KeyDrop[ 154 | (* make an association *) 155 | Association[ 156 | (* create a list of rules of names and named characters *) 157 | Thread[ 158 | Rule[ 159 | (Prepend[#1[[3]], #1[[2]]]), 160 | FromCharacterCode[FromDigits[StringDrop[#1[[1]], 2], 16], "Unicode"] 161 | ] 162 | ] & /@ charactersAndTheirNames 163 | ], 164 | { 165 | StringJoin[Table[verticalEllipsis, {2}]], 166 | "\\[]" 167 | } 168 | ] 169 | ], 170 | (StringLength[#1] < StringLength[#2]) & 171 | ]; 172 | ]; 173 | ]; 174 | 175 | (************************************ 176 | various important symbols 177 | for use by 178 | WolframLanguageForJupyter 179 | *************************************) 180 | 181 | (* create an association for maintaining state in the evaluation loop *) 182 | loopState = 183 | (* schema for the Association *) 184 | Association[ 185 | (* index for the execution of the next input *) 186 | "executionCount" -> 1, 187 | 188 | (* flag for if WolframLanguageForJupyter should shut down *) 189 | "doShutdown" -> False, 190 | 191 | (* flag for whether to redirect messages, or to, at the end of the execution of an input, 192 | bundle them with the result *) 193 | "redirectMessages" -> True, 194 | 195 | (* flag for if an is_complete_request has ever been sent to the kernel *) 196 | "isCompleteRequestSent" -> False, 197 | 198 | (* OutputStream for Jupyter's stdout *) 199 | "WolframLanguageForJupyter-stdout" -> False, 200 | 201 | (* local to an iteration *) 202 | (* a received frame as an Association *) 203 | "frameAssoc" -> Null, 204 | (* type of the reply message frame *) 205 | "replyMsgType" -> Null, 206 | (* content of the reply message frame *) 207 | "replyContent" -> Null, 208 | (* message relpy frame to send on the IO Publish socket, if it is not Null *) 209 | "ioPubReplyFrame" -> Null, 210 | (* the redirect function Print should use *) 211 | "printFunction" -> False, 212 | (* flag for if the Jupyter console (if running under a Jupyter console) 213 | should ask the user if it should exit *) 214 | "askExit" -> False 215 | ]; 216 | 217 | (* helper utility for applying hooks if they are set *) 218 | applyHook[hook_, value_] /; Length[OwnValues[hook]] != 0 := hook[value]; 219 | applyHook[hook_, value_] := value; 220 | Attributes[applyHook] := HoldAll; 221 | 222 | (* can we use the Front End? *) 223 | $canUseFrontEnd := (UsingFrontEnd[$FrontEnd] =!= Null); 224 | 225 | $outputSetToTraditionalForm := (Lookup[Options[$Output], FormatType] === TraditionalForm); 226 | $outputSetToTeXForm := (Lookup[Options[$Output], FormatType] === TeXForm); 227 | $trueFormatType := 228 | If[ 229 | $outputSetToTraditionalForm, 230 | TraditionalForm, 231 | If[$outputSetToTeXForm, TeXForm, #&] 232 | ]; 233 | $truePageWidth := 234 | Replace[ 235 | Lookup[Options[$Output], PageWidth], 236 | Except[ 237 | Alternatives[ 238 | Infinity, 239 | pageWidth_ /; ((IntegerQ[pageWidth]) && (pageWidth > 0)) 240 | ] 241 | ] -> 242 | $defaultPageWidth 243 | ]; 244 | 245 | (* hard-coded base64 rasterization of $Failed *) 246 | failedInBase64 = "iVBORw0KGgoAAAANSUhEUgAAADcAAAARCAIAAAD2TKM6AAAAhXpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjaVYvBDcMwDAP/mqIjyLJM2uMYiQNkg45fuu0n9yApgbT1vi97felp2dgxABc5csRU6P6juNciDXn+X9MfZGWgoRhTluEkhnJXkqKFN+LCAahYcIbnIV8gNQN3o86928QyPusLVffpbh/5eCey76LuBgAAAAlwSFlzAAALEwAACxMBAJqcGAAAADx0RVh0U29mdHdhcmUAQ3JlYXRlZCB3aXRoIHRoZSBXb2xmcmFtIExhbmd1YWdlIDogd3d3LndvbGZyYW0uY29tXKKmhQAAACF0RVh0Q3JlYXRpb24gVGltZQAyMDE5OjA3OjAyIDAzOjExOjExSFD8JQAAA8xJREFUSInVlk9IKl8Ux68ajjThLynMwCKLICSDjBbRxpVSFhFkCtFfBMMKAhcu3GXhQopaJBEFISjhsn9YUGE6zSJqkRURFVpKLQqNcYgZHectpszfez9exoP33u+zmnPm3HO/c865w2XRNA3+eth/WkBOfKjEMAzH8T8o5Sd8qJybm1tcXPz1jCaTyeVyMc8URXV3d2s0Go1GEwgEvppqZGQERdEPldfX1yiKIghydnaWCerp6WFn0dbW9mlekiTdbrfX62VMNpvd398/MDAQCATu7u6+qnJ9fT0SiQAA8gAAJycnzc3NnZ2dfD5foVA4nc7W1lYAQDqd1uv1drudWZOXl/dpXi6XG4lEOBwOY7JYLLVaDQDIz8//qsRs2AAAt9vd2NjodrsXFhYuLy9VKlXmNQRB/7wDwzAAIBaLmc1miUTC5/MHBwfPz8+ZSJvNJpVKpVKpTCazWq2fbkySpMViEYvFAoFAp9M9Pz8z/v39/bq6Oj6fr9FoMueEDQBQKpUoihqNxtvb2+Li4kwlAADxePz6nXQ6DQC4uroKBoPz8/MoimIYZjabmciuri6Hw+FwOMRi8ePj46cqLRaLx+NZXV1dW1u7v78fGxsDAESj0fb29qamJp/PV19f//Ly8hZN0zRN016vVy6Xs9nsoaGh19dXxqnT6QAAzFByOJxYLEa/Q5Lk6enp9PQ0BEE4jtNZ9Pb2Go1G+t9UVla6XK6MmUwmIQja3t5mzI2NjYKCApqm7Xa7RCJJpVKMv6yszOPx0DT9dnpUKtXx8fHW1tbm5ubk5GTmi0dHRymKoigqlUoVFhYCAAiCMBqNQqFweHj48PCQIIh4PP5p5b4jHA4TBNHS0sLlcrlcbkdHB0EQOI7f3NzU1tZmN/Oj4wcHBxiGMVpVKlVm1P6TqakpBEEuLi4QBJmdnc1RFovFoigqY4pEIhaL5ff7SZIkSTKVSpEkCcOwQCAIBoM/LmcDACYmJqqrq/v6+rRarcfjYRr9kzJUVVWJRKJEImGz2XJU2dDQsLu7m06nHx4eAAAwDKvVapPJxExwNBpFEAQAoFAoQqHQ0tISSZIrKytM8JvKnZ0dxoVhmM/n02q1mQL8uJ/BYPD7/UKhsKKioqioCIKgXFTq9fq9vT0YhuVyOVPU5eXlkpKS8vLy0tJSqVTK/POVSuX4+LjBYODxeE6ns6am5m19ZqKtVuvMzAydAwRBhEIhiqJyCc5AUVQoFEomk9lOHMfD4fB3qRKJxNPTU7aHRb/fiY6Ojng8nkwmy7GJv5MPlX8z/4+b2zdkknhkRbjZsAAAAABJRU5ErkJggg=="; 247 | 248 | (* obtain details on how to connect to Jupyter, from Jupyter's invocation of "KernelForWolframLanguageForJupyter.wl" *) 249 | (* NOTE: We remove the extension of the file to be imported so as to avoid accidentally loading the format "MXNet," which has a bad interaction with the paclet SetReplace (https://github.com/maxitg/SetReplace) as of June 22, 2020 *) 250 | Block[ 251 | {noExtensionFile}, 252 | noExtensionFile = CopyFile[$CommandLine[[4]], CreateFile[], OverwriteTarget -> True]; 253 | connectionAssoc = ToString /@ Association[Import[noExtensionFile, "JSON"]]; 254 | DeleteFile[noExtensionFile]; 255 | ]; 256 | 257 | (* warnings to display in kernel information *) 258 | bannerWarning = 259 | If[ 260 | Length[$CommandLine] > 4, 261 | "\\n\\nNote: This Jupyter kernel was installed through the WolframScript install method. Accordingly, updates to a WolframLanguageForJupyter paclet will not affect this kernel.", 262 | "" 263 | ]; 264 | 265 | (* key for generating signatures for reply message frames *) 266 | keyString = connectionAssoc["key"]; 267 | 268 | (* base string using protocol and IP address from Jupyter *) 269 | baseString = StringJoin[connectionAssoc["transport"], "://", connectionAssoc["ip"], ":"]; 270 | 271 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html for what the following correspond to *) 272 | heartbeatString = StringJoin[baseString, connectionAssoc["hb_port"]]; 273 | ioPubString = StringJoin[baseString, connectionAssoc["iopub_port"]]; 274 | controlString = StringJoin[baseString, connectionAssoc["control_port"]]; 275 | inputString = StringJoin[baseString, connectionAssoc["stdin_port"]]; 276 | shellString = StringJoin[baseString, connectionAssoc["shell_port"]]; 277 | 278 | Block[ 279 | { 280 | (* for storing the result of defining a new OutputStream method *) 281 | customOutputStreamMethod 282 | }, 283 | 284 | (* define an OutputStream method that will allow writing to Jupyter's stdout *) 285 | customOutputStreamMethod = 286 | DefineOutputStreamMethod[ 287 | "for-WolframLanguageForJupyter-stdout", 288 | { 289 | "ConstructorFunction" -> Function[{name, isAppend, caller, opts}, {True, {}}], 290 | "WriteFunction" -> 291 | Function[ 292 | {state, bytes}, 293 | If[ 294 | loopState["frameAssoc"] =!= Null, 295 | redirectPrint[loopState["frameAssoc"], FromCharacterCode[bytes]]; 296 | ]; 297 | {Length[bytes], {}} 298 | ] 299 | } 300 | ]; 301 | 302 | (* if defining a new OutputStream method did not fail, 303 | open an OutputStream using the new method, and store it in loopState *) 304 | If[ 305 | !FailureQ[customOutputStreamMethod], 306 | loopState["WolframLanguageForJupyter-stdout"] = 307 | OpenWrite["WolframLanguageForJupyter-stdout", Method -> "for-WolframLanguageForJupyter-stdout"]; 308 | (* -- also, if opening the OutputStream failed, reset loopState["WolframLanguageForJupyter-stdout"] back to False *) 309 | If[ 310 | FailureQ[loopState["WolframLanguageForJupyter-stdout"]], 311 | loopState["WolframLanguageForJupyter-stdout"] = False; 312 | ]; 313 | ]; 314 | ]; 315 | 316 | (************************************ 317 | open all the non-heartbeat 318 | sockets 319 | *************************************) 320 | 321 | (* open sockets using the set strings from above *) 322 | ioPubSocket = SocketOpen[ioPubString, "ZMQ_PUB"]; 323 | controlSocket = SocketOpen[controlString, "ZMQ_ROUTER"]; 324 | inputSocket = SocketOpen[inputString, "ZMQ_ROUTER"]; 325 | shellSocket = SocketOpen[shellString, "ZMQ_ROUTER"]; 326 | 327 | (* check for any problems *) 328 | If[FailureQ[ioPubSocket] || FailureQ[controlSocket] || FailureQ[inputSocket] || FailureQ[shellSocket], 329 | Quit[]; 330 | ]; 331 | 332 | (************************************ 333 | spin off a new kernel 334 | that nullifies Jupyter's 335 | requirement for looping 336 | back arrving "heartbeats" 337 | *************************************) 338 | 339 | (* start heartbeat thread *) 340 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#heartbeat-for-kernels *) 341 | heldLocalSubmit = 342 | Replace[ 343 | Hold[ 344 | (* submit a task for the new kernel *) 345 | LocalSubmit[ 346 | (* get required ZMQ utilities in the new kernel *) 347 | Get["ZeroMQLink`"]; 348 | (* open the heartbeat socket -- inserted with Replace and a placeholder *) 349 | heartbeatSocket = SocketOpen[placeholder1, "ZMQ_REP"]; 350 | (* check for any problems *) 351 | If[ 352 | FailureQ[heartbeatSocket], 353 | Quit[]; 354 | ]; 355 | (* do this "forever" *) 356 | While[ 357 | True, 358 | (* wait for new data on the heartbeat socket *) 359 | SocketWaitNext[{heartbeatSocket}]; 360 | (* receive the data *) 361 | heartbeatRecv = SocketReadMessage[heartbeatSocket]; 362 | (* check for any problems *) 363 | If[ 364 | FailureQ[heartbeatRecv], 365 | Continue[]; 366 | ]; 367 | (* and loop the data back to Jupyter *) 368 | socketWriteFunction[ 369 | heartbeatSocket, 370 | heartbeatRecv, 371 | "Multipart" -> False 372 | ]; 373 | ];, 374 | HandlerFunctions-> Association["TaskFinished" -> Quit] 375 | ] 376 | ], 377 | (* see above *) 378 | placeholder1 -> heartbeatString, 379 | Infinity 380 | ]; 381 | (* start the heartbeat thread *) 382 | (* Quiet[ReleaseHold[heldLocalSubmit]]; *) 383 | 384 | (* end the private context for WolframLanguageForJupyter *) 385 | End[]; (* `Private`` *) 386 | 387 | (************************************ 388 | Get[] guard 389 | *************************************) 390 | 391 | ] (* WolframLanguageForJupyter`Private`$GotInitialization *) 392 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Resources/KernelForWolframLanguageForJupyter.wl: -------------------------------------------------------------------------------- 1 | (************************************************ 2 | KernelForWolframLanguage- 3 | ForJupyter.wl 4 | ************************************************* 5 | Description: 6 | Entry point for WolframLanguageForJupyter 7 | kernels started by Jupyter 8 | Symbols defined: 9 | loop 10 | *************************************************) 11 | 12 | (************************************ 13 | begin the 14 | WolframLanguageForJupyter 15 | package 16 | *************************************) 17 | 18 | (* begin the WolframLanguageForJupyter package *) 19 | BeginPackage["WolframLanguageForJupyter`"]; 20 | 21 | (************************************ 22 | get required paclets 23 | *************************************) 24 | 25 | (* obtain ZMQ utilities *) 26 | Needs["ZeroMQLink`"]; (* SocketReadMessage *) 27 | 28 | (************************************ 29 | load required 30 | WolframLanguageForJupyter 31 | files 32 | *************************************) 33 | 34 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* initialize WolframLanguageForJupyter; loopState, bannerWarning, shellSocket, controlSocket, ioPubSocket *) 35 | 36 | Get[FileNameJoin[{DirectoryName[$InputFileName], "SocketUtilities.wl"}]]; (* sendFrame *) 37 | Get[FileNameJoin[{DirectoryName[$InputFileName], "MessagingUtilities.wl"}]]; (* getFrameAssoc, createReplyFrame *) 38 | 39 | Get[FileNameJoin[{DirectoryName[$InputFileName], "RequestHandlers.wl"}]]; (* isCompleteRequestHandler, executeRequestHandler, completeRequestHandler *) 40 | 41 | (************************************ 42 | private symbols 43 | *************************************) 44 | 45 | (* begin the private context for WolframLanguageForJupyter *) 46 | Begin["`Private`"]; 47 | 48 | (* define the evaluation loop *) 49 | loop[] := 50 | Module[ 51 | { 52 | (* the socket that has become ready *) 53 | readySocket, 54 | 55 | (* the raw byte array frame received through SocketReadMessage *) 56 | rawFrame, 57 | 58 | (* a frame for sending status updates on the IO Publish socket *) 59 | statusReplyFrame, 60 | 61 | (* a frame for sending replies on a socket *) 62 | replyFrame 63 | }, 64 | While[ 65 | True, 66 | Switch[ 67 | (* poll sockets until one is ready *) 68 | readySocket = First[SocketWaitNext[{shellSocket, controlSocket}]], 69 | (* if the shell socket or control socket is ready, ... *) 70 | shellSocket | controlSocket, 71 | (* receive a frame *) 72 | rawFrame = SocketReadMessage[readySocket, "Multipart" -> True]; 73 | (* check for any problems *) 74 | If[FailureQ[rawFrame], 75 | Quit[]; 76 | ]; 77 | (* convert the frame into an Association *) 78 | loopState["frameAssoc"] = getFrameAssoc[rawFrame]; 79 | (* handle this frame based on the type of request *) 80 | Switch[ 81 | loopState["frameAssoc"]["header"]["msg_type"], 82 | (* if asking for information about the kernel, ... *) 83 | "kernel_info_request", 84 | (* set the appropriate reply type *) 85 | loopState["replyMsgType"] = "kernel_info_reply"; 86 | (* provide the information *) 87 | loopState["replyContent"] = 88 | StringJoin[ 89 | "{\"protocol_version\": \"5.3.0\",\"implementation\": \"WolframLanguageForJupyter\",\"implementation_version\": \"0.0.1\",\"language_info\": {\"name\": \"Wolfram Language\",\"version\": \"12.0\",\"mimetype\": \"application/vnd.wolfram.m\",\"file_extension\": \".m\",\"pygments_lexer\": \"mathematica\",\"codemirror_mode\": \"mathematica\"},\"banner\" : \"Wolfram Language/Wolfram Engine Copyright 2019", 90 | bannerWarning, 91 | "\"}" 92 | ];, 93 | (* if asking if the input is complete (relevant for jupyter-console), respond appropriately *) 94 | "is_complete_request", 95 | (* isCompleteRequestHandler will read and update loopState *) 96 | isCompleteRequestHandler[];, 97 | (* if asking the kernel to execute something, use executeRequestHandler *) 98 | "execute_request", 99 | (* executeRequestHandler will read and update loopState *) 100 | executeRequestHandler[];, 101 | (* use the tab-completion functionality to rewrite named character names *) 102 | "complete_request", 103 | (* completeRequestHandler will read and update loopState *) 104 | completeRequestHandler[];, 105 | (* if asking the kernel to shutdown, set doShutdown to True *) 106 | "shutdown_request", 107 | loopState["replyMsgType"] = "shutdown_reply"; 108 | loopState["replyContent"] = "{\"restart\":false}"; 109 | loopState["doShutdown"] = True;, 110 | _, 111 | Continue[]; 112 | ]; 113 | 114 | (* create a message frame to send on the IO Publish socket to mark the kernel's status as "busy" *) 115 | statusReplyFrame = 116 | createReplyFrame[ 117 | (* use the current source frame *) 118 | loopState["frameAssoc"], 119 | (* the status message type *) 120 | "status", 121 | (* the status message content *) 122 | "{\"execution_state\":\"busy\"}", 123 | (* do not branch off *) 124 | False 125 | ]; 126 | (* send the frame *) 127 | sendFrame[ioPubSocket, statusReplyFrame]; 128 | 129 | (* create a message frame to send a reply on the socket that became ready *) 130 | replyFrame = 131 | createReplyFrame[ 132 | (* use the current source frame *) 133 | loopState["frameAssoc"], 134 | (* the reply message type *) 135 | loopState["replyMsgType"], 136 | (* the reply message content *) 137 | loopState["replyContent"], 138 | (* do not branch off *) 139 | False 140 | ]; 141 | (* send the frame *) 142 | sendFrame[readySocket, replyFrame]; 143 | 144 | (* if an ioPubReplyFrame was created, send it on the IO Publish socket *) 145 | If[ 146 | loopState["ioPubReplyFrame"] =!= Association[], 147 | sendFrame[ioPubSocket, loopState["ioPubReplyFrame"]]; 148 | (* -- also, reset ioPubReplyFrame *) 149 | loopState["ioPubReplyFrame"] = Association[]; 150 | ]; 151 | 152 | (* send a message frame on the IO Publish socket that marks the kernel's status as "idle" *) 153 | sendFrame[ 154 | ioPubSocket, 155 | createReplyFrame[ 156 | (* use the current source frame *) 157 | loopState["frameAssoc"], 158 | (* the status message type *) 159 | "status", 160 | (* the status message content *) 161 | "{\"execution_state\":\"idle\"}", 162 | (* do not branch off *) 163 | False 164 | ] 165 | ]; 166 | 167 | (* if the doShutdown flag is True, shut down *) 168 | If[ 169 | loopState["doShutdown"], 170 | Block[{$inQuit = True}, Quit[]]; 171 | ]; 172 | , 173 | _, 174 | Continue[]; 175 | ] 176 | ]; 177 | ]; 178 | 179 | (* end the private context for WolframLanguageForJupyter *) 180 | End[]; (* `Private` *) 181 | 182 | (************************************ 183 | end the 184 | WolframLanguageForJupyter 185 | package 186 | *************************************) 187 | 188 | (* end the WolframLanguageForJupyter package *) 189 | EndPackage[]; (* WolframLanguageForJupyter` *) 190 | (* $ContextPath = DeleteCases[$ContextPath, "WolframLanguageForJupyter`"]; *) 191 | 192 | (************************************ 193 | evaluate loop[] 194 | *************************************) 195 | 196 | (* start the loop *) 197 | WolframLanguageForJupyter`Private`loop[]; 198 | 199 | (* This setup does not preclude dynamics or widgets. *) 200 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Resources/MessagingUtilities.wl: -------------------------------------------------------------------------------- 1 | (************************************************ 2 | MessagingUtilities.wl 3 | ************************************************* 4 | Description: 5 | Higher-level utilities for sending 6 | and receiving messages from Jupyter 7 | Symbols defined: 8 | getFrameAssoc, 9 | createReplyFrame 10 | *************************************************) 11 | 12 | (************************************ 13 | Get[] guard 14 | *************************************) 15 | 16 | If[ 17 | !TrueQ[WolframLanguageForJupyter`Private`$GotMessagingUtilities], 18 | 19 | WolframLanguageForJupyter`Private`$GotMessagingUtilities = True; 20 | 21 | (************************************ 22 | load required 23 | WolframLanguageForJupyter 24 | files 25 | *************************************) 26 | 27 | Get[FileNameJoin[{DirectoryName[$InputFileName], "SocketUtilities.wl"}]]; (* hmac *) 28 | 29 | (************************************ 30 | private symbols 31 | *************************************) 32 | 33 | (* begin the private context for WolframLanguageForJupyter *) 34 | Begin["`Private`"]; 35 | 36 | (************************************ 37 | utilities for reading in, and 38 | writing out, message 39 | frames 40 | *************************************) 41 | 42 | (* transform received frame into a structured Association *) 43 | getFrameAssoc[baFrame_ByteArray] := 44 | Module[ 45 | { 46 | (* string form of the byte array *) 47 | frameStr, 48 | (* storage for the various value fields of the received frame *) 49 | identLen, 50 | header, 51 | pheader, 52 | metadata, 53 | content 54 | }, 55 | 56 | (* set frameStr to the string form of the byte array of the received frame *) 57 | frameStr = Quiet[ByteArrayToString[baFrame]]; 58 | 59 | (* get the values (of key-value pairs) from the string frame *) 60 | {identLen, header, pheader, metadata, content} = 61 | First[ 62 | (* pick out the values using the expected form of the frame *) 63 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html *) 64 | StringCases[ 65 | frameStr, 66 | Shortest[ident1___] ~~ "" ~~ Shortest[___] ~~ 67 | "{" ~~ Shortest[json2___] ~~ "}" ~~ 68 | "{" ~~ Shortest[json3___] ~~ "}" ~~ 69 | "{" ~~ Shortest[json4___] ~~ "}" ~~ 70 | "{" ~~ Shortest[json5___] ~~ "}" ~~ 71 | EndOfString :> 72 | Prepend[ 73 | (* add back in the brackets *) 74 | ( 75 | Association[ 76 | ImportByteArray[ 77 | StringToByteArray[ 78 | StringJoin["{", #1, "}"] 79 | ], 80 | "JSON" 81 | ] 82 | ] & 83 | ) /@ {json2,json3,json4,json5}, 84 | (* use the length of ident1 *) 85 | StringLength[ident1] 86 | ] 87 | ] 88 | ]; 89 | 90 | (* return an association with: 91 | * an ident key with a byte array value 92 | * the header of the original frame imported as JSON 93 | * the content of the original frame imported as JSON 94 | *) 95 | Return[ 96 | Association[ 97 | "ident" -> baFrame[[;;identLen]], 98 | "header" -> header, 99 | "content" -> content 100 | ] 101 | ]; 102 | ]; 103 | 104 | (* generate a reply message frame from using a source message frame, replyType, and replyContent *) 105 | createReplyFrame[ 106 | (* the source frame to use, after it has been ran through getFrameAssoc *) 107 | sourceFrame_Association, 108 | (* the message type to be used for the reply message frame *) 109 | replyType_String, 110 | (* the content to be used for the reply message frame *) 111 | replyContent : (_String | _ByteArray), 112 | (* whether to list sourceFrame as a parent for the reply message frame *) 113 | branchOff:(True|False) 114 | ] := 115 | Module[ 116 | { 117 | (* for storing the header and content of the source message frame *) 118 | header, content, 119 | 120 | (* the association for the generated reply message frame *) 121 | result 122 | }, 123 | 124 | (* save the header and content of the source message frame *) 125 | header = sourceFrame["header"]; 126 | content = sourceFrame["content"]; 127 | 128 | (* build reply message *) 129 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html for why the following are set as they are *) 130 | result = Association[ 131 | "ident" -> If[KeyExistsQ[sourceFrame, "ident"], sourceFrame["ident"], ByteArray[{0, 0, 0, 0, 0}]], 132 | "idsmsg" -> "", 133 | "header" -> ExportString[ 134 | Append[ 135 | header, 136 | {"date" -> DateString["ISODateTime"], "msg_type" -> replyType, "msg_id" -> StringInsert[StringReplace[CreateUUID[], "-" -> ""], "-", 9]} 137 | ], 138 | "JSON", 139 | "Compact" -> True 140 | ], 141 | "pheader" -> If[branchOff, "{}", ExportString[header, "JSON", "Compact" -> True]], 142 | "metadata" -> ExportString[ 143 | {"text/html" -> {}}, 144 | "JSON", 145 | "Compact" -> True 146 | ], 147 | "content" -> replyContent 148 | ]; 149 | 150 | (* generate the signature of the reply message *) 151 | AssociateTo[ 152 | result, 153 | "signature" -> 154 | hmac[ 155 | keyString, 156 | StringJoin[ 157 | result["header"], 158 | result["pheader"], 159 | result["metadata"], 160 | If[StringQ[result["content"]], result["content"], ByteArrayToString[result["content"]]] 161 | ] 162 | ] 163 | ]; 164 | 165 | (* return the built reply message frame *) 166 | Return[result]; 167 | ]; 168 | 169 | (* end the private context for WolframLanguageForJupyter *) 170 | End[]; (* `Private` *) 171 | 172 | (************************************ 173 | Get[] guard 174 | *************************************) 175 | 176 | ] (* WolframLanguageForJupyter`Private`$GotMessagingUtilities *) 177 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Resources/OutputHandlingUtilities.wl: -------------------------------------------------------------------------------- 1 | (************************************************ 2 | OutputHandlingUtilities.wl 3 | ************************************************* 4 | Description: 5 | Utilities for handling the result 6 | of Wolfram Language expressions 7 | so that, as outputs, they are 8 | reasonably displayed in Jupyter 9 | notebooks 10 | Symbols defined: 11 | textQ, 12 | toText, 13 | toOutTextHTML, 14 | toImageData, 15 | toOutImageHTML 16 | *************************************************) 17 | 18 | (************************************ 19 | Get[] guard 20 | *************************************) 21 | 22 | If[ 23 | !TrueQ[WolframLanguageForJupyter`Private`$GotOutputHandlingUtilities], 24 | 25 | WolframLanguageForJupyter`Private`$GotOutputHandlingUtilities = True; 26 | 27 | (************************************ 28 | load required 29 | WolframLanguageForJupyter 30 | files 31 | *************************************) 32 | 33 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* $canUseFrontEnd, $outputSetToTeXForm, 34 | $outputSetToTraditionalForm, 35 | $trueFormatType, $truePageWidth, 36 | failedInBase64 *) 37 | 38 | (************************************ 39 | private symbols 40 | *************************************) 41 | 42 | (* begin the private context for WolframLanguageForJupyter *) 43 | Begin["`Private`"]; 44 | 45 | (************************************ 46 | helper utility for converting 47 | an expression into a 48 | textual form 49 | *************************************) 50 | 51 | (* convert an expression into a textual form, 52 | using as much of the options already set for $Output as possible for ToString *) 53 | (* NOTE: toOutTextHTML used to call toStringUsingOutput *) 54 | toStringUsingOutput[expr_] := 55 | ToString[ 56 | expr, 57 | Sequence @@ 58 | Cases[ 59 | Options[$Output], 60 | Verbatim[Rule][opt_, val_] /; 61 | MemberQ[ 62 | Keys[Options[ToString]], 63 | opt 64 | ] 65 | ] 66 | ]; 67 | 68 | (************************************ 69 | helper utility for determining 70 | if a result should be 71 | displayed as text or an image 72 | *************************************) 73 | 74 | (* check if a string contains any private use area characters *) 75 | containsPUAQ[str_] := 76 | AnyTrue[ 77 | ToCharacterCode[str, "Unicode"], 78 | (57344 <= #1 <= 63743 || 983040 <= #1 <= 1048575 || 1048576 <= #1 <= 1114111) & 79 | ]; 80 | 81 | (************************************ 82 | utility for determining if a 83 | result should be displayed 84 | as text or an image 85 | *************************************) 86 | 87 | (* determine if a result does not depend on any Wolfram Language frontend functionality, 88 | such that it should be displayed as text *) 89 | textQ[expr_] := Module[ 90 | { 91 | (* the head of expr *) 92 | exprHead, 93 | 94 | (* pattern objects *) 95 | pObjects 96 | }, 97 | 98 | (* if we cannot use the frontend, use text *) 99 | If[ 100 | !$canUseFrontEnd, 101 | Return[True]; 102 | ]; 103 | 104 | (* save the head of the expression *) 105 | exprHead = Head[expr]; 106 | 107 | (* if the expression is wrapped with InputForm or OutputForm, 108 | automatically format as text *) 109 | If[exprHead === InputForm || exprHead === OutputForm, 110 | Return[True] 111 | ]; 112 | 113 | (* if the FormatType of $Output is set to TeXForm, or if the expression is wrapped with TeXForm, 114 | and the expression has an acceptable textual form, format as text *) 115 | If[($outputSetToTeXForm || exprHead == TeXForm) && !containsPUAQ[ToString[expr]], 116 | Return[True]; 117 | ]; 118 | 119 | (* if the FormatType of $Output is set to TraditionalForm, 120 | or if the expression is wrapped with TraditionalForm, 121 | do not use text *) 122 | If[$outputSetToTraditionalForm || exprHead === TraditionalForm, 123 | Return[False] 124 | ]; 125 | 126 | (* breakdown expr into atomic objects organized by their Head *) 127 | pObjects = 128 | GroupBy[ 129 | Complement[ 130 | Quiet[Cases[ 131 | expr, 132 | elem_ /; (Depth[Unevaluated[elem]] == 1) -> Hold[elem], 133 | {0, Infinity}, 134 | Heads -> True 135 | ]], 136 | (* these symbols are fine *) 137 | {Hold[List], Hold[Association]} 138 | ], 139 | ( 140 | Replace[ 141 | #1, 142 | Hold[elem_] :> Head[Unevaluated[elem]] 143 | ] 144 | ) & 145 | ]; 146 | 147 | (* if expr just contains atomic objects of the types listed above, return True *) 148 | If[ 149 | ContainsOnly[Keys[pObjects], {Integer, Real}], 150 | Return[True]; 151 | ]; 152 | 153 | (* if expr just contains atomic objects of the types listed above, along with some symbols, 154 | return True only if the symbols have no attached rules *) 155 | If[ 156 | ContainsOnly[Keys[pObjects], {Integer, Real, String, Symbol}], 157 | Return[ 158 | AllTrue[ 159 | Lookup[pObjects, String, {}], 160 | (!containsPUAQ[ReleaseHold[#1]]) & 161 | ] && 162 | AllTrue[ 163 | Lookup[pObjects, Symbol, {}], 164 | ( 165 | Replace[ 166 | #1, 167 | Hold[elem_] :> ToString[Definition[elem]] 168 | ] === "Null" 169 | ) & 170 | ] 171 | ]; 172 | ]; 173 | 174 | (* otherwise, no, the result should not be displayed as text *) 175 | Return[False]; 176 | ]; 177 | 178 | (************************************ 179 | utilities for generating 180 | HTML for displaying 181 | results as text and images 182 | *************************************) 183 | 184 | (* generate the textual form of a result using a given page width *) 185 | (* NOTE: the OutputForm (which ToString uses) of any expressions wrapped with, say, InputForm should 186 | be identical to the string result of an InputForm-wrapped expression itself *) 187 | toText[result_, pageWidth_] := 188 | ToString[ 189 | (* make sure to apply $trueFormatType to the result if the result is not already headed by TeXForm *) 190 | If[ 191 | Head[result] === TeXForm, 192 | result, 193 | $trueFormatType[result] 194 | ], 195 | (* also, use the given page width *) 196 | PageWidth -> pageWidth 197 | ]; 198 | (* generate the textual form of a result using the current PageWidth setting for $Output *) 199 | toText[result_] := toText[result, $truePageWidth]; 200 | 201 | (* generate HTML for the textual form of a result *) 202 | toOutTextHTML[result_] := 203 | Module[ 204 | { 205 | (* if the result should be marked as TeX *) 206 | isTeX 207 | }, 208 | (* check if the result should be marked as TeX *) 209 | isTeX = ((Head[result] === TeXForm) || ($outputSetToTeXForm)); 210 | Return[ 211 | StringJoin[ 212 | 213 | (* mark this result as preformatted only if it isn't TeX *) 214 | If[ 215 | !isTeX, 216 | { 217 | (* preformatted *) 218 | "
"
222 | 						},
223 | 						{}
224 | 					],
225 | 
226 | 					(* mark the text as TeX, if is TeX *)
227 | 					If[isTeX, "$$", ""],
228 | 
229 | 					(* the textual form of the result *)
230 | 					({"&#", ToString[#1], ";"} & /@ 
231 | 						ToCharacterCode[
232 | 							If[
233 | 								isTeX,
234 | 								(* if the result is TeX, do not allow line breaks *)
235 | 								toText[result, Infinity],
236 | 								(* otherwise, just call toText *)
237 | 								toText[result]
238 | 							],
239 | 							"Unicode"
240 | 						]),
241 | 
242 | 					(* mark the text as TeX, if is TeX *)
243 | 					If[isTeX, "$$", ""],
244 | 
245 | 					(* mark this result as preformatted only if it isn't TeX *)
246 | 					If[
247 | 						!isTeX,
248 | 						{
249 | 							(* end the element *)
250 | 							"
" 251 | }, 252 | {} 253 | ] 254 | ] 255 | ]; 256 | ]; 257 | 258 | (* generate a byte array of image data for the rasterized form of a result *) 259 | toImageData[result_] := 260 | Module[ 261 | { 262 | (* the preprocessed form of a result *) 263 | preprocessedForm 264 | }, 265 | (* preprocess the result *) 266 | If[ 267 | Head[result] === Manipulate, 268 | preprocessedForm = result; 269 | , 270 | preprocessedForm = Rasterize[result]; 271 | ]; 272 | (* if the preprocessing failed, return $Failed *) 273 | If[ 274 | FailureQ[preprocessedForm], 275 | Return[$Failed]; 276 | ]; 277 | (* now return preprocessedForm as a byte array corresponding to the PNG format *) 278 | Return[ 279 | ExportByteArray[ 280 | preprocessedForm, 281 | "PNG" 282 | ] 283 | ]; 284 | ]; 285 | 286 | (* generate HTML for the rasterized form of a result *) 287 | toOutImageHTML[result_] := 288 | Module[ 289 | { 290 | (* the rasterization of result *) 291 | imageData, 292 | (* the rasterization of result in base 64 *) 293 | imageDataInBase64 294 | }, 295 | 296 | (* rasterize the result *) 297 | imageData = 298 | toImageData[ 299 | $trueFormatType[result] 300 | ]; 301 | If[ 302 | !FailureQ[imageData], 303 | (* if the rasterization did not fail, convert it to base 64 *) 304 | imageInBase64 = BaseEncode[imageData]; 305 | , 306 | (* if the rasterization did fail, try to rasterize result with Shallow *) 307 | imageData = 308 | toImageData[ 309 | $trueFormatType[Shallow[result]] 310 | ]; 311 | If[ 312 | !FailureQ[imageData], 313 | (* if the rasterization did not fail, convert it to base 64 *) 314 | imageInBase64 = BaseEncode[imageData]; 315 | , 316 | (* if the rasterization did fail, try to rasterize $Failed *) 317 | imageData = 318 | toImageData[ 319 | $trueFormatType[$Failed] 320 | ]; 321 | If[ 322 | !FailureQ[imageData], 323 | (* if the rasterization did not fail, convert it to base 64 *) 324 | imageInBase64 = BaseEncode[imageData]; 325 | , 326 | (* if the rasterization did fail, use a hard-coded base64 rasterization of $Failed *) 327 | imageInBase64 = failedInBase64; 328 | ]; 329 | ]; 330 | ]; 331 | 332 | (* return HTML for the rasterized form of result *) 333 | Return[ 334 | StringJoin[ 335 | (* display a inlined PNG image encoded in base64 *) 336 | "\"Output\"" 341 | ] 342 | ] 343 | ]; 344 | 345 | (* end the private context for WolframLanguageForJupyter *) 346 | End[]; (* `Private` *) 347 | 348 | (************************************ 349 | Get[] guard 350 | *************************************) 351 | 352 | ] (* WolframLanguageForJupyter`Private`$GotOutputHandlingUtilities *) 353 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Resources/RequestHandlers.wl: -------------------------------------------------------------------------------- 1 | (************************************************ 2 | RequestHandlers.wl 3 | ************************************************* 4 | Description: 5 | Handlers for message frames of the form 6 | "*_request" arriving from Jupyter 7 | Symbols defined: 8 | isCompleteRequestHandler, 9 | executeRequestHandler, 10 | completeRequestHandler 11 | *************************************************) 12 | 13 | (************************************ 14 | Get[] guard 15 | *************************************) 16 | 17 | If[ 18 | !TrueQ[WolframLanguageForJupyter`Private`$GotRequestHandlers], 19 | 20 | WolframLanguageForJupyter`Private`$GotRequestHandlers = True; 21 | 22 | (************************************ 23 | load required 24 | WolframLanguageForJupyter 25 | files 26 | *************************************) 27 | 28 | Get[FileNameJoin[{DirectoryName[$InputFileName], "Initialization.wl"}]]; (* loopState *) 29 | 30 | Get[FileNameJoin[{DirectoryName[$InputFileName], "SocketUtilities.wl"}]]; (* sendFrame *) 31 | Get[FileNameJoin[{DirectoryName[$InputFileName], "MessagingUtilities.wl"}]]; (* createReplyFrame *) 32 | 33 | Get[FileNameJoin[{DirectoryName[$InputFileName], "EvaluationUtilities.wl"}]]; (* redirectPrint, redirectMessages, simulatedEvaluate *) 34 | 35 | Get[FileNameJoin[{DirectoryName[$InputFileName], "OutputHandlingUtilities.wl"}]]; (* textQ, toOutTextHTML, toOutImageHTML, 36 | toText, containsPUAQ *) 37 | 38 | Get[FileNameJoin[{DirectoryName[$InputFileName], "CompletionUtilities.wl"}]]; (* rewriteNamedCharacters *) 39 | 40 | (************************************ 41 | private symbols 42 | *************************************) 43 | 44 | (* begin the private context for WolframLanguageForJupyter *) 45 | Begin["`Private`"]; 46 | 47 | (************************************ 48 | handler for is_complete_requests 49 | *************************************) 50 | 51 | (* handle is_complete_request message frames received on the shell socket *) 52 | isCompleteRequestHandler[] := 53 | Module[ 54 | { 55 | (* the length of the code string to check completeness for *) 56 | stringLength, 57 | (* the value returned by SyntaxLength[] on the code string to check completeness for *) 58 | syntaxLength 59 | }, 60 | 61 | (* mark loopState["isCompleteRequestSent"] as True *) 62 | loopState["isCompleteRequestSent"] = True; 63 | 64 | (* set the appropriate reply type *) 65 | loopState["replyMsgType"] = "is_complete_reply"; 66 | 67 | (* determine the length of the code string *) 68 | stringLength = StringLength[loopState["frameAssoc"]["content"]["code"]]; 69 | (* determine the SyntaxLength[] value for the code string *) 70 | syntaxLength = SyntaxLength[loopState["frameAssoc"]["content"]["code"]]; 71 | 72 | (* test the value of syntaxLength to determine the completeness of the code string, 73 | setting the content of the reply appropriately *) 74 | Which[ 75 | (* if the above values could not be correctly determined, 76 | the completeness status of the code string is unknown *) 77 | !IntegerQ[stringLength] || !IntegerQ[syntaxLength], 78 | loopState["replyContent"] = "{\"status\":\"unknown\"}";, 79 | (* if the SyntaxLength[] value for a code string is greater than its actual length, 80 | the code string is incomplete *) 81 | syntaxLength > stringLength, 82 | loopState["replyContent"] = "{\"status\":\"incomplete\"}";, 83 | (* if the SyntaxLength[] value for a code string is less than its actual length, 84 | the code string contains a syntax error (or is "invalid") *) 85 | syntaxLength < stringLength, 86 | loopState["replyContent"] = "{\"status\":\"invalid\"}";, 87 | (* if the SyntaxLength[] value for a code string is equal to its actual length, 88 | the code string is complete and correct *) 89 | syntaxLength == stringLength, 90 | loopState["replyContent"] = "{\"status\":\"complete\"}"; 91 | ]; 92 | ]; 93 | 94 | (************************************ 95 | handler for execute_requests 96 | *************************************) 97 | 98 | (* handle execute_request message frames received on the shell socket *) 99 | executeRequestHandler[] := 100 | Module[ 101 | { 102 | (* message formatter function *) 103 | messageFormatter, 104 | 105 | (* content of the desired frame to send on the IO Publish socket *) 106 | ioPubReplyContent, 107 | 108 | (* the HTML form for any generated message *) 109 | errorMessage, 110 | 111 | (* the total result of the evaluation: 112 | an association containing 113 | the result of evaluation ("EvaluationResult"), 114 | indices of the output lines of the result ("EvaluationResultOutputLineIndices"), 115 | the total number of indices consumed by this evaluation ("ConsumedIndices"), 116 | generated messages ("GeneratedMessages") 117 | *) 118 | totalResult, 119 | 120 | (* flag for if there are any unreported error messages after execution of the input *) 121 | unreportedErrorMessages 122 | }, 123 | 124 | (* if an is_complete_request has been sent, assume jupyter-console is running the kernel, 125 | redirect messages, and handle any "Quit", "Exit", "quit" or "exit" inputs *) 126 | If[ 127 | loopState["isCompleteRequestSent"], 128 | loopState["redirectMessages"] = True; 129 | If[ 130 | StringMatchQ[ 131 | loopState["frameAssoc"]["content"]["code"], 132 | "Quit" | "Exit" | "quit" | "exit" 133 | ], 134 | loopState["replyMsgType"] = "execute_reply"; 135 | (* NOTE: uses payloads *) 136 | loopState["replyContent"] = ExportString[Association["status" -> "ok", "execution_count" -> loopState["executionCount"], "user_expressions" -> {}, "payload" -> {Association["source" -> "ask_exit", "keepkernel" -> False]}], "JSON", "Compact" -> True]; 137 | Return[]; 138 | ]; 139 | ]; 140 | 141 | (* redirect Print so that it prints in the Jupyter notebook *) 142 | loopState["printFunction"] = (redirectPrint[loopState["frameAssoc"], #1] &); 143 | 144 | (* if loopState["redirectMessages"] is True, 145 | update Jupyter explicitly with any errors that occur DURING the execution of the input *) 146 | If[ 147 | loopState["redirectMessages"], 148 | messageFormatter[messageName_, messageText_] := 149 | redirectMessages[ 150 | loopState["frameAssoc"], 151 | messageName, 152 | messageText, 153 | (* add a newline if loopState["isCompleteRequestSent"] *) 154 | loopState["isCompleteRequestSent"] 155 | ]; 156 | SetAttributes[messageFormatter, HoldAll]; 157 | Internal`$MessageFormatter = messageFormatter; 158 | ]; 159 | 160 | (* evaluate the input, and store the total result in totalResult *) 161 | totalResult = simulatedEvaluate[loopState["frameAssoc"]["content"]["code"]]; 162 | 163 | (* restore printFunction to False *) 164 | loopState["printFunction"] = False; 165 | 166 | (* unset messageFormatter and Internal`$MessageFormatter *) 167 | Unset[messageFormatter]; 168 | Unset[Internal`$MessageFormatter]; 169 | 170 | (* set the appropriate reply type *) 171 | loopState["replyMsgType"] = "execute_reply"; 172 | 173 | (* set the content of the reply to information about WolframLanguageForJupyter's execution of the input *) 174 | loopState["replyContent"] = 175 | ExportString[ 176 | Association[ 177 | "status" -> "ok", 178 | "execution_count" -> loopState["executionCount"], 179 | "user_expressions" -> {}, 180 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html#payloads-deprecated *) 181 | (* if the "askExit" flag is True, add an "ask_exit" payload *) 182 | (* NOTE: uses payloads *) 183 | "payload" -> If[loopState["askExit"], {Association["source" -> "ask_exit", "keepkernel" -> False]}, {}] 184 | ], 185 | "JSON", 186 | "Compact" -> True 187 | ]; 188 | 189 | (* check if there are any unreported error messages *) 190 | unreportedErrorMessages = 191 | ( 192 | (* ... because messages are not being redirected *) 193 | (!loopState["redirectMessages"]) && 194 | (* ... and because at least one message was generated *) 195 | (StringLength[totalResult["GeneratedMessages"]] > 0) 196 | ); 197 | 198 | (* if there are no results, or if the "askExit" flag is True, 199 | do not send anything on the IO Publish socket and return *) 200 | If[ 201 | (Length[totalResult["EvaluationResultOutputLineIndices"]] == 0) || 202 | (loopState["askExit"]), 203 | (* set the "askExit" flag to False *) 204 | loopState["askExit"] = False; 205 | (* send any unreported error messages *) 206 | If[unreportedErrorMessages, 207 | redirectMessages[ 208 | loopState["frameAssoc"], 209 | "", 210 | totalResult["GeneratedMessages"], 211 | (* do not add a newline *) 212 | False, 213 | (* drop message name *) 214 | True 215 | ]; 216 | ]; 217 | (* increment loopState["executionCount"] as needed *) 218 | loopState["executionCount"] += totalResult["ConsumedIndices"]; 219 | Return[]; 220 | ]; 221 | 222 | (* generate an HTML form of the message text *) 223 | errorMessage = 224 | If[ 225 | !unreportedErrorMessages, 226 | (* if there are no unreported error messages, there is no need to format them *) 227 | {}, 228 | (* build the HTML form of the message text *) 229 | { 230 | (* preformatted *) 231 | "
",
236 | 						(* the generated messages  *)
237 | 						StringJoin[{"&#", ToString[#1], ";"} & /@ ToCharacterCode[totalResult["GeneratedMessages"], "UTF-8"]],
238 | 						(* end the element *)
239 | 						"
" 240 | } 241 | ]; 242 | 243 | (* format output as purely text, image, or cloud interface *) 244 | If[ 245 | (* check if the input was wrapped with Interact, 246 | which is used when the output should be displayed as an embedded cloud object *) 247 | TrueQ[totalResult["InteractStatus"]] && 248 | (* check if we are logged into the Cloud *) 249 | $CloudConnected, 250 | (* prepare the content for a reply message frame to be sent on the IO Publish socket *) 251 | ioPubReplyContent = 252 | ExportString[ 253 | Association[ 254 | (* the first output index *) 255 | "execution_count" -> First[totalResult["EvaluationResultOutputLineIndices"]], 256 | (* HTML code to embed output uploaded to the Cloud in the Jupyter notebook *) 257 | "data" -> 258 | { 259 | "text/html" -> 260 | StringJoin[ 261 | (* display any generated messages as inlined PNG images encoded in base64 *) 262 | "
\"\"", 267 | (* embed the cloud object *) 268 | EmbedCode[CloudDeploy[totalResult["EvaluationResult"]], "HTML"][[1]]["CodeSection"]["Content"], 269 | (* end the whole element *) 270 | "
" 271 | ], 272 | "text/plain" -> "" 273 | }, 274 | (* no metadata *) 275 | "metadata" -> {"text/html" -> {}, "text/plain" -> {}} 276 | ], 277 | "JSON", 278 | "Compact" -> True 279 | ]; 280 | , 281 | (* if every output line can be formatted as text, use a function that converts the output to text *) 282 | (* otherwise, use a function that converts the output to an image *) 283 | (* TODO: allow for mixing text and image results *) 284 | If[AllTrue[totalResult["EvaluationResult"], textQ], 285 | toOut = toOutTextHTML, 286 | toOut = toOutImageHTML 287 | ]; 288 | (* prepare the content for a reply message frame to be sent on the IO Publish socket *) 289 | ioPubReplyContent = ExportByteArray[ 290 | Association[ 291 | (* the first output index *) 292 | "execution_count" -> First[totalResult["EvaluationResultOutputLineIndices"]], 293 | (* the data representing the results and messages *) 294 | "data" -> 295 | { 296 | (* generate HTML for the results and messages *) 297 | "text/html" -> 298 | If[ 299 | loopState["isCompleteRequestSent"], 300 | (* if an is_complete_request has been sent, assume jupyter-console is running the kernel, 301 | and do not generate HTML *) 302 | "", 303 | (* otherwise, output the results in a grid *) 304 | If[ 305 | Length[totalResult["EvaluationResult"]] > 1, 306 | StringJoin[ 307 | (* add grid style *) 308 | " 314 | 315 |
", 316 | (* display error message *) 317 | errorMessage, 318 | (* start the grid *) 319 | "
", 320 | (* display the output lines *) 321 | Table[ 322 | { 323 | (* start the grid item *) 324 | "
", 325 | (* show the output line *) 326 | toOut[totalResult["EvaluationResult"][[outIndex]]], 327 | (* end the grid item *) 328 | "
" 329 | }, 330 | {outIndex, 1, Length[totalResult["EvaluationResult"]]} 331 | ], 332 | (* end the element *) 333 | "
" 334 | ], 335 | StringJoin[ 336 | (* start the element *) 337 | "
", 338 | (* display error message *) 339 | errorMessage, 340 | (* if there are messages, but no results, do not display a result *) 341 | If[ 342 | Length[totalResult["EvaluationResult"]] == 0, 343 | "", 344 | (* otherwise, display a result *) 345 | toOut[First[totalResult["EvaluationResult"]]] 346 | ], 347 | (* end the element *) 348 | "
" 349 | ] 350 | ] 351 | ], 352 | (* provide, as a backup, plain text for the results *) 353 | "text/plain" -> 354 | StringJoin[ 355 | Table[ 356 | { 357 | toText[totalResult["EvaluationResult"][[outIndex]]], 358 | (* -- also, suppress newline if this is the last result *) 359 | If[outIndex != Length[totalResult["EvaluationResult"]], "\n", ""] 360 | }, 361 | {outIndex, 1, Length[totalResult["EvaluationResult"]]} 362 | ] 363 | ] 364 | }, 365 | (* no metadata *) 366 | "metadata" -> {"text/html" -> {}, "text/plain" -> {}} 367 | ], 368 | "JSON", 369 | "Compact" -> True 370 | ]; 371 | ]; 372 | 373 | (* create frame from ioPubReplyContent *) 374 | loopState["ioPubReplyFrame"] = 375 | createReplyFrame[ 376 | (* use the current source frame *) 377 | loopState["frameAssoc"], 378 | (* the reply message type *) 379 | "execute_result", 380 | (* the reply message content *) 381 | ioPubReplyContent, 382 | (* do not branch off *) 383 | False 384 | ]; 385 | 386 | (* increment loopState["executionCount"] as needed *) 387 | loopState["executionCount"] += totalResult["ConsumedIndices"]; 388 | ]; 389 | 390 | (************************************ 391 | handler for complete_requests 392 | *************************************) 393 | 394 | (* handle complete_request message frames received on the shell socket *) 395 | completeRequestHandler[] := 396 | Module[ 397 | { 398 | (* for storing the code string to offer completion suggestions on *) 399 | codeStr 400 | }, 401 | (* get the code string to rewrite the named characters of, ending at the cursor *) 402 | codeStr = 403 | StringTake[ 404 | loopState["frameAssoc"]["content"]["code"], 405 | { 406 | 1, 407 | loopState["frameAssoc"]["content"]["cursor_pos"] 408 | } 409 | ]; 410 | (* set the appropriate reply type *) 411 | loopState["replyMsgType"] = "complete_reply"; 412 | (* set the content of the reply to a list of rewrites for any named characters in the code string *) 413 | loopState["replyContent"] = 414 | ByteArrayToString[ 415 | ExportByteArray[ 416 | Association[ 417 | "matches" -> 418 | DeleteDuplicates[ 419 | Prepend[ 420 | Select[ 421 | rewriteNamedCharacters[codeStr], 422 | (!containsPUAQ[#1])& 423 | ], 424 | codeStr 425 | ] 426 | ], 427 | "cursor_start" -> 0, 428 | "cursor_end" -> StringLength[codeStr], 429 | "metadata" -> {}, 430 | "status" -> "ok" 431 | ], 432 | "JSON", 433 | "Compact" -> True 434 | ] 435 | ]; 436 | ]; 437 | 438 | (* end the private context for WolframLanguageForJupyter *) 439 | End[]; (* `Private` *) 440 | 441 | (************************************ 442 | Get[] guard 443 | *************************************) 444 | 445 | ] (* WolframLanguageForJupyter`Private`$GotRequestHandlers *) 446 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/Resources/SocketUtilities.wl: -------------------------------------------------------------------------------- 1 | (************************************************ 2 | SocketUtilities.wl 3 | ************************************************* 4 | Description: 5 | Low-level utilities for writing to 6 | sockets 7 | Symbols defined: 8 | socketWriteFunction, 9 | sendFrame, 10 | hmac 11 | *************************************************) 12 | 13 | (************************************ 14 | Get[] guard 15 | *************************************) 16 | 17 | If[ 18 | !TrueQ[WolframLanguageForJupyter`Private`$GotSocketUtilities], 19 | 20 | WolframLanguageForJupyter`Private`$GotSocketUtilities = True; 21 | 22 | (************************************ 23 | get required paclets 24 | *************************************) 25 | 26 | (* obtain ZMQ utilities *) 27 | Needs["ZeroMQLink`"]; (* socketWriteFunction, ZeroMQLink`Private`ZMQWriteInternal, 28 | ZeroMQLink`ZMQSocketWriteMessage *) 29 | 30 | (************************************ 31 | private symbols 32 | *************************************) 33 | 34 | (* begin the private context for WolframLanguageForJupyter *) 35 | Begin["`Private`"]; 36 | 37 | (************************************ 38 | utility for writing a part 39 | of a message frame to a 40 | socket 41 | *************************************) 42 | 43 | (* write a part of a message frame to a socket *) 44 | (* adjust for differences in Wolfram Engine version *) 45 | If[TrueQ[$VersionNumber < 12.0], 46 | Options[socketWriteFunction] = {"Asynchronous"->False,"Multipart"->False}; 47 | socketWriteFunction[sock_, data_List, opts:OptionsPattern[]] := ZeroMQLink`Private`ZMQWriteInternal[sock, data, opts]; 48 | socketWriteFunction[sock_, data_ByteArray, rest___]:= socketWriteFunction[sock, Normal[data], rest] 49 | , 50 | socketWriteFunction = ZeroMQLink`ZMQSocketWriteMessage 51 | ]; 52 | 53 | (************************************ 54 | utility for writing a message 55 | frame to a socket 56 | *************************************) 57 | 58 | (* write a message frame that matches Jupyter's messaging protocols to a socket *) 59 | sendFrame[socket_, frame_Association] := Module[{}, 60 | 61 | (* see https://jupyter-client.readthedocs.io/en/stable/messaging.html for an explanation of the below *) 62 | 63 | socketWriteFunction[ 64 | socket, 65 | frame["ident"], 66 | "Multipart" -> True 67 | ]; 68 | 69 | socketWriteFunction[ 70 | socket, 71 | StringToByteArray[#1], 72 | "Multipart" -> True 73 | ]& /@ Lookup[frame, {"idsmsg", "signature", "header", "pheader", "metadata"}]; 74 | 75 | socketWriteFunction[ 76 | socket, 77 | If[ByteArrayQ[frame["content"]], frame["content"], StringToByteArray[frame["content"]]], 78 | "Multipart" -> False 79 | ]; 80 | ]; 81 | 82 | (************************************ 83 | utility for determining the 84 | HMAC signature of a 85 | message frame 86 | *************************************) 87 | 88 | (* determine the HMAC signature of a message frame *) 89 | hmac[key_String, message_String] := 90 | Module[ 91 | { 92 | method, blockSize, outputSize, 93 | baKey, baMessage, 94 | baKeyPrime, 95 | keyPrime, 96 | baOPadded, baIPadded 97 | }, 98 | 99 | (* adapted from wikipedia article on HMAC's definition *) 100 | 101 | method = "SHA256"; 102 | blockSize = 64; 103 | outputSize = 32; 104 | 105 | baKey = StringToByteArray[key]; 106 | baMessage = StringToByteArray[message]; 107 | 108 | If[Length[baKey] > blockSize, 109 | baKeyPrime = Hash[baKey, method, "ByteArray"]; 110 | ]; 111 | 112 | If[Length[baKey] < blockSize, 113 | baKeyPrime = Join[ 114 | baKey, 115 | ByteArray[ 116 | Table[0, {blockSize - Length[baKey]}] 117 | ] 118 | ]; 119 | ]; 120 | 121 | keyPrime = Normal[baKeyPrime]; 122 | 123 | baOPadded = ByteArray[BitXor[#1, 92] & /@ Normal[keyPrime]]; 124 | baIPadded = ByteArray[BitXor[#1, 54] & /@ Normal[keyPrime]]; 125 | 126 | Hash[ 127 | Join[ 128 | baOPadded, 129 | Hash[ 130 | Join[ 131 | baIPadded, 132 | baMessage 133 | ], 134 | method, 135 | "ByteArray" 136 | ] 137 | ], 138 | method, 139 | "HexString" 140 | ] 141 | ]; 142 | 143 | (* end the private context for WolframLanguageForJupyter *) 144 | End[]; (* `Private` *) 145 | 146 | (************************************ 147 | Get[] guard 148 | *************************************) 149 | 150 | ] (* WolframLanguageForJupyter`Private`$GotSocketUtilities *) 151 | -------------------------------------------------------------------------------- /WolframLanguageForJupyter/WolframLanguageForJupyter.m: -------------------------------------------------------------------------------- 1 | BeginPackage["WolframLanguageForJupyter`"]; 2 | 3 | ConfigureJupyter::subcommand = "The first argument to ConfigureJupyter is the subcommand: either \"add\", \"remove\", or \"clear\"."; 4 | ConfigureJupyter::argx = "ConfigureJupyter called with `1` arguments; 1 argument is expected."; 5 | 6 | ConfigureJupyter::notfound = "Jupyter installation on Environment[\"PATH\"] not found."; 7 | ConfigureJupyter::isdir = "Provided `1` binary path is a directory. Please provide the path to the `1` binary."; 8 | ConfigureJupyter::nobin = "Provided `1` binary path does not exist."; 9 | 10 | ConfigureJupyter::notadded = "An error has occurred. The desired Wolfram Engine is not in \"jupyter kernelspec list.\" See WolframLanguageForJupyter`.`Errors`.`$ConfigureError for the message that Jupyter returned when attempting to add the Wolfram Engine."; 11 | ConfigureJupyter::notremoved = "An error has occurred: Wolfram Engine(s) still in \"jupyter kernelspec list.\" See WolframLanguageForJupyter`.`Errors`.`$ConfigureError for the message that Jupyter returned when attempting to remove the Wolfram Engine."; 12 | 13 | ConfigureJupyter::addconflict = "An error has occurred. A Wolfram Engine with the same $VersionNumber of the target Wolfram Engine is in \"jupyter kernelspec list.\" Attempting to overwrite ..."; 14 | (* ConfigureJupyter::removeconflict = "An error has occurred. The Wolfram Engine(s) to be removed is/are not in \"jupyter kernelspec list.\""; *) 15 | 16 | ConfigureJupyter::nolink = "An error has occurred: Communication with provided Wolfram Engine binary could not be established."; 17 | 18 | ConfigureJupyter::usage = 19 | "ConfigureJupyter[subcommand:\"add\"|\"remove\"|\"clear\"] evaluates the action associated with subcommand, relying on the current Wolfram Engine binary path and the first Jupyter installation on Environment[\"PATH\"] when relevant. 20 | ConfigureJupyter[subcommand:\"add\"|\"remove\"|\"clear\", opts] evaluates the action associated with subcommand, using specified paths for \"WolframEngineBinary\" and \"JupyterInstallation\" when given as options."; 21 | 22 | Begin["`Private`"]; 23 | 24 | (* 25 | Dictionary: 26 | mathBin/mathBinSession = WolframKernel binary 27 | kernelspec = Kernel Specification; term used by Jupyter 28 | notProvidedQ = was a Wolfram Engine Binary explicitly specified? 29 | *) 30 | 31 | (* START: Helper symbols *) 32 | 33 | projectHome = DirectoryName[$InputFileName]; 34 | 35 | (* establishes link with Wolfram Engine at mathBin and evaluates $Version/$VersionNumber *) 36 | (* returns string form *) 37 | getVersionFromKernel[mathBin_String] := 38 | Module[{link, res}, 39 | link = 40 | LinkLaunch[ 41 | StringJoin[ 42 | { 43 | "\"", 44 | mathBin, 45 | "\" -wstp" 46 | } 47 | ] 48 | ]; 49 | If[FailureQ[link], 50 | Return[$Failed]; 51 | ]; 52 | (* bleed link *) 53 | While[LinkReadyQ[link, 0.5], LinkRead[link];]; 54 | LinkWrite[link, Unevaluated[$VersionNumber]]; 55 | res = StringTrim[ToString[LinkRead[link]], "ReturnPacket[" | "]"]; 56 | LinkClose[link]; 57 | If[!StringContainsQ[res, "[" | "]"], 58 | Return[res];, 59 | Return[$Failed]; 60 | ]; 61 | ]; 62 | 63 | (* determine display name for Jupyter installation from Wolfram Engine $Version/$VersionNumber *) 64 | (* returns {Kernel ID, Display Name} *) 65 | getNames[mathBin_String, notProvidedQ_?BooleanQ] := 66 | Module[{version, installDir, (* names, hashedKernelUUID *) versionStr}, 67 | (* if Wolfram Engine binary not provided, just evaluate $Version in the current session *) 68 | (* otherwise, use MathLink to obtain $Version *) 69 | If[ 70 | notProvidedQ, 71 | version = ToString[$VersionNumber]; 72 | installDir = $InstallationDirectory; 73 | , 74 | version = Quiet[getVersionFromKernel[mathBin]]; 75 | If[ 76 | FailureQ[version], 77 | Return[$Failed]; 78 | ]; 79 | installDir = mathBin; 80 | ]; 81 | 82 | versionStr = StringTrim[version, "."]; 83 | Return[ 84 | { 85 | (* Kernel ID *) 86 | StringJoin["wolframlanguage", versionStr], 87 | (* Display Name *) 88 | StringJoin["Wolfram Language ", versionStr] 89 | } 90 | ]; 91 | ]; 92 | 93 | (* determine symbols related to finding Wolfram Engine and Jupyter installations *) 94 | (* mathBinSession: WolframKernel location for the current session *) 95 | (* fileExt: file extension for executables *) 96 | (* pathSeperator: delimiter for directories on PATH *) 97 | defineGlobalVars[] := 98 | Switch[ 99 | $OperatingSystem, 100 | "Windows", 101 | mathBinSession = FileNameJoin[{$InstallationDirectory, "wolfram.exe"}]; 102 | fileExt = ".exe"; 103 | pathSeperator = ";";, 104 | "MacOSX", 105 | mathBinSession = FileNameJoin[{$InstallationDirectory, "MacOS", "WolframKernel"}]; 106 | fileExt = ""; 107 | pathSeperator = ":";, 108 | "Unix", 109 | mathBinSession = FileNameJoin[{$InstallationDirectory, "Executables", "WolframKernel"}]; 110 | fileExt = ""; 111 | pathSeperator = ":"; 112 | ]; 113 | 114 | mathBinSession := (defineGlobalVars[]; mathBinSession); 115 | fileExt := (defineGlobalVars[]; fileExt); 116 | pathSeperator := (defineGlobalVars[]; pathSeperator); 117 | 118 | (* a list of directories in PATH *) 119 | splitPath := 120 | StringSplit[ 121 | (* restore PATH, if due to a bug, it becomes essentially empty; this is relevant to finding the Jupyter installation *) 122 | (* otherwise, just use PATH directly *) 123 | If[ 124 | $OperatingSystem === "MacOSX" && FileType["~/.profile"] === File, 125 | StringTrim[ 126 | RunProcess[ 127 | $SystemShell, 128 | "StandardOutput", 129 | StringJoin[Import["~/.profile", "String"], "\necho $PATH"], 130 | ProcessEnvironment -> {} 131 | ], 132 | "\n" 133 | ] 134 | , 135 | Environment["PATH"] 136 | ], 137 | pathSeperator]; 138 | 139 | 140 | (* find Jupyter installation path *) 141 | (* returns above *) 142 | findJupyterPath[] := 143 | SelectFirst[ 144 | splitPath, 145 | (* check every directory in PATH to see if a Jupyter binary is a member *) 146 | (FileType[FileNameJoin[{#1, StringJoin["jupyter", fileExt]}]] === File)& 147 | ]; 148 | 149 | (* get information about installed kernels in Jupyter *) 150 | (* returns kernel IDs in Jupyter *) 151 | getKernels[jupyterPath_String, processEnvironment_] := 152 | Module[{json, kernelspecAssoc}, 153 | (* obtain information about "jupyter kernelspec list" in JSON *) 154 | json = Quiet[ImportString[RunProcess[{jupyterPath, "kernelspec", "list", "--json"}, "StandardOutput", ProcessEnvironment -> processEnvironment], "JSON"]]; 155 | (* transform that JSON information into an Association *) 156 | kernelspecAssoc = 157 | If[ 158 | FailureQ[json], 159 | Association[], 160 | Replace[ 161 | json, 162 | part_List /; AllTrue[part, Head[#1] === Rule &] -> Association @ part, 163 | {0, Infinity} 164 | ] 165 | ]; 166 | Return[ 167 | (* if the above process worked, just return the kernel IDs of all the kernelspecs *) 168 | (* otherwise, return an empty list *) 169 | If[ 170 | KeyExistsQ[kernelspecAssoc, "kernelspecs"], 171 | Keys[kernelspecAssoc["kernelspecs"]], 172 | {} 173 | ] 174 | ]; 175 | ]; 176 | 177 | 178 | (* END: Helper symbols *) 179 | 180 | (* main install command *) 181 | (* specs: options \"WolframEngineBinary\" and \"JupyterInstallation\" in an Association, when provided *) 182 | (* removeQ: remove a Jupyter installation or not *) 183 | (* removeAllQ: clear all Jupyter installations or not *) 184 | (* removeQ first, removeAllQ second: "add" is False, False; "remove" is True, False, and "clear" is True, True *) 185 | (* returns action success status *) 186 | configureJupyter[specs_Association, removeQ_?BooleanQ, removeAllQ_?BooleanQ] := 187 | Module[ 188 | { 189 | retrievedNames, kernelID, displayName, 190 | notProvidedQ, 191 | jupyterPath, mathBin, 192 | fileType, 193 | processEnvironment, 194 | baseDir, tempDir, 195 | wlKernels, (* wlKernelsL(owerCase) *) wlKernelsL, 196 | commandArgs, 197 | exitInfo, kernelspecAssoc, kernelspecs 198 | }, 199 | 200 | (* just check that the REPL script is there *) 201 | If[ 202 | !( 203 | FileType[ 204 | FileNameJoin[{projectHome, "Resources", "KernelForWolframLanguageForJupyter.wl"}] 205 | ] === File 206 | ), 207 | Return[$Failed]; 208 | ]; 209 | 210 | jupyterPath = specs["JupyterInstallation"]; 211 | (* if no Jupyter installation path provided, determine it from PATH *) 212 | If[ 213 | MissingQ[jupyterPath], 214 | jupyterPath = findJupyterPath[]; 215 | (* if Jupyter not on PATH, message *) 216 | If[MissingQ[jupyterPath], 217 | Message[ConfigureJupyter::notfound, "Jupyter"]; 218 | Return[$Failed]; 219 | ]; 220 | jupyterPath = FileNameJoin[{jupyterPath, StringJoin["jupyter", fileExt]}]; 221 | ]; 222 | 223 | mathBin = 224 | Lookup[ 225 | specs, 226 | "WolframEngineBinary", 227 | (* if no "WolframEngineBinary" provided, use the session Wolfram Kernel location and set notProvidedQ to True *) 228 | (notProvidedQ = True; mathBinSession) 229 | ]; 230 | 231 | (* check that the Jupyter installation path is a file, and message appropriately *) 232 | If[ 233 | !((fileType = FileType[jupyterPath]) === File), 234 | Switch[ 235 | fileType, 236 | Directory, 237 | Message[ConfigureJupyter::isdir, "Jupyter"];, 238 | None, 239 | Message[ConfigureJupyter::nobin, "Jupyter"]; 240 | ]; 241 | Return[$Failed]; 242 | ]; 243 | 244 | {kernelID, displayName} = {"", ""}; 245 | (* if not clearing, check that the Wolfram Engine installation path is a file, and message appropriately *) 246 | If[ 247 | !(removeQ && removeAllQ), 248 | If[ 249 | (fileType = FileType[mathBin]) === File, 250 | (* get the "Kernel ID" and "Display Name" for the new Jupyter kernel *) 251 | retrievedNames = getNames[mathBin, TrueQ[notProvidedQ]]; 252 | If[FailureQ[retrievedNames], Message[ConfigureJupyter::nolink]; Return[$Failed]]; 253 | {kernelID, displayName} = retrievedNames; 254 | , 255 | Switch[ 256 | fileType, 257 | Directory, 258 | Message[ConfigureJupyter::isdir, "Wolfram Engine"];, 259 | None, 260 | Message[ConfigureJupyter::nobin, "Wolfram Engine"]; 261 | ]; 262 | Return[$Failed]; 263 | ]; 264 | ]; 265 | 266 | (* as an association for 11.3 compatibility *) 267 | processEnvironment = Association[GetEnvironment[]]; 268 | processEnvironment["PATH"] = StringJoin[Riffle[Append[splitPath, DirectoryName[jupyterPath]], pathSeperator]]; 269 | 270 | (* list of kernels in Jupyter to perform an action on *) 271 | wlKernels = {kernelID}; 272 | tempDir = ""; 273 | (* if adding, ...*) 274 | (* otherwise, when removing or clearing, ...*) 275 | If[ 276 | !removeQ, 277 | 278 | (* create staging directory for files needed to register a kernel with Jupyter *) 279 | tempDir = CreateDirectory[ 280 | FileNameJoin[{ 281 | projectHome, 282 | CreateUUID[], 283 | kernelID 284 | }], CreateIntermediateDirectories -> True 285 | ]; 286 | 287 | (* export a JSON file to the staging directory that contains all the relevant information on how to run the kernel *) 288 | Export[ 289 | FileNameJoin[{tempDir, "kernel.json"}], 290 | Association[ 291 | "argv" -> { 292 | mathBin, 293 | (* TODO: automatically find the kernel script 294 | (only) if the Wolfram Engine being installed is the same as the one used to execute this command *) 295 | "-script", 296 | FileNameJoin[{projectHome, "Resources", "KernelForWolframLanguageForJupyter.wl"}], 297 | "{connection_file}" 298 | (* , "-noprompt" *) 299 | }, 300 | "display_name" -> displayName, 301 | "language" -> "Wolfram Language" 302 | ] 303 | ]; 304 | 305 | (* create a list of arguments that directs Jupyter to install from the staging directory *) 306 | commandArgs = {jupyterPath, "kernelspec", "install", "--user", tempDir};, 307 | (* create a list of arguments that directs Jupyter to remove ... *) 308 | commandArgs = {jupyterPath, "kernelspec", "remove", "-f", 309 | If[ 310 | !removeAllQ, 311 | (* just the specified kernel *) 312 | kernelID, 313 | (* all Wolfram Language Jupyter kernels *) 314 | (* select from all kernel IDs in Jupyter those that match the form used by this install *) 315 | Sequence @@ (wlKernels = Select[getKernels[jupyterPath, processEnvironment], StringMatchQ[#1, (* ("WolframLanguage-" | "wl-") *) "WolframLanguage" ~~ ___, IgnoreCase -> True] &]) 316 | ] 317 | } 318 | ]; 319 | (* if no kernels to act on, quit *) 320 | If[Length[wlKernels] == 0, Return[];]; 321 | wlKernelsL = ToLowerCase /@ wlKernels; 322 | 323 | (* for error detection, get a snapshot of kernels before the action is performed *) 324 | kernelspecs = getKernels[jupyterPath, processEnvironment]; 325 | (* when adding, if there is a kernel with the same id already in Jupyter, it will be replaced; thus, message, but continue *) 326 | If[SubsetQ[kernelspecs, wlKernelsL] && !removeQ, Message[ConfigureJupyter::addconflict]]; 327 | 328 | (* perform the action *) 329 | exitInfo = RunProcess[commandArgs, All, ProcessEnvironment -> processEnvironment]; 330 | (* remove temporary directory if it was created *) 331 | If[StringLength[tempDir] > 0, DeleteDirectory[DirectoryName[tempDir], DeleteContents -> True]]; 332 | 333 | (* get list of kernels after the action was performed *) 334 | kernelspecs = getKernels[jupyterPath, processEnvironment]; 335 | (* message about success with respect to the action that was performed *) 336 | If[ 337 | !Xor[removeQ, SubsetQ[kernelspecs, wlKernelsL]], 338 | WolframLanguageForJupyter`Errors`$ConfigureError = exitInfo["StandardError"]; 339 | Print[WolframLanguageForJupyter`Errors`$ConfigureError]; 340 | If[!removeQ, Message[ConfigureJupyter::notadded];, Message[ConfigureJupyter::notremoved];]; 341 | Return[$Failed]; 342 | ]; 343 | ]; 344 | 345 | (* convert options to an Association *) 346 | ConfigureJupyter[ 347 | args___, 348 | opts:OptionsPattern[] /; Length[{opts}] > 0 349 | ] := ConfigureJupyter[args, Association[opts]]; 350 | 351 | (* mold ConfigureJupyter arguments to what is expected by the main install function, configureJupyter ... *) 352 | ConfigureJupyter["Add", args___] := ConfigureJupyter["add", args]; 353 | ConfigureJupyter["add"] := ConfigureJupyter["add", Association[]]; 354 | ConfigureJupyter["add", assoc_Association] := configureJupyter[assoc, False, False]; 355 | 356 | ConfigureJupyter["Remove", args___] := ConfigureJupyter["remove", args]; 357 | ConfigureJupyter["remove"] := ConfigureJupyter["remove", Association[]]; 358 | ConfigureJupyter["remove", assoc_Association] := configureJupyter[assoc, True, False]; 359 | 360 | ConfigureJupyter["Clear", args___] := ConfigureJupyter["clear", args]; 361 | ConfigureJupyter["clear"] := ConfigureJupyter["clear", Association[]]; 362 | ConfigureJupyter["clear", assoc_Association] := configureJupyter[assoc, True, True]; 363 | 364 | ConfigureJupyter[sc_String, ___] /; !StringMatchQ[sc, "add" | "remove" | "clear" | "Add" | "Remove" | "Clear"] := Message[ConfigureJupyter::subcommand]; 365 | ConfigureJupyter[Except[_String], ___] := Message[ConfigureJupyter::subcommand]; 366 | ConfigureJupyter[args___] := Message[ConfigureJupyter::argx, Length[{args}]]; 367 | 368 | End[]; 369 | EndPackage[]; 370 | -------------------------------------------------------------------------------- /configure-jupyter.wls: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env wolframscript 2 | 3 | Begin["WolframLanguageForJupyter`Private`"]; 4 | 5 | notfound = "configure-jupyter.wls: Jupyter installation on Environment[\"PATH\"] not found."; 6 | isdir = "configure-jupyter.wls: Provided Jupyter binary path is a directory. Please provide the path to the Jupyter binary." 7 | nobin = "configure-jupyter.wls: Provided Jupyter binary path does not exist."; 8 | isdirMath = "configure-jupyter.wls: Provided Wolfram Engine binary path is a directory. Please provide the path to the Wolfram Engine binary." 9 | nobinMath = "configure-jupyter.wls: Provided Wolfram Engine binary path does not exist."; 10 | notadded = "configure-jupyter.wls: An error has occurred. The desired Wolfram Engine is not in \"jupyter kernelspec list.\""; 11 | notremoved = "configure-jupyter.wls: An error has occurred: Wolfram Engine(s) still in \"jupyter kernelspec list.\""; 12 | addconflict = "configure-jupyter.wls: An error has occurred. A Wolfram Engine with the same $VersionNumber of the target Wolfram Engine is in \"jupyter kernelspec list.\" Attempting to overwrite ..."; 13 | (* removeconflict = "configure-jupyter.wls: An error has occurred. The Wolfram Engine(s) to be removed is/are not in \"jupyter kernelspec list.\""; *) 14 | removeconflict = ""; 15 | nopaclet = "configure-jupyter.wls: WolframLanguageForJupyter paclet source not detected. Are you running the script in the root project directory?"; 16 | nolink = "configure-jupyter.wls: Communication with provided Wolfram Engine binary could not be established."; 17 | 18 | (* 19 | Dictionary: 20 | mathBin/mathBinSession = WolframKernel binary 21 | kernelspec = Kernel Specification; term used by Jupyter 22 | notProvidedQ = was a Wolfram Engine Binary explicitly specified? 23 | *) 24 | 25 | (* START: Helper symbols *) 26 | 27 | projectHome = If[StringQ[$InputFileName] && $InputFileName != "", DirectoryName[$InputFileName], Directory[]]; 28 | 29 | (* establishes link with Wolfram Engine at mathBin and evaluates $Version/$VersionNumber *) 30 | (* returns string form *) 31 | getVersionFromKernel[mathBin_String] := 32 | Module[{link, res}, 33 | link = 34 | LinkLaunch[ 35 | StringJoin[ 36 | { 37 | "\"", 38 | mathBin, 39 | "\" -wstp" 40 | } 41 | ] 42 | ]; 43 | If[FailureQ[link], 44 | Return[$Failed]; 45 | ]; 46 | (* bleed link *) 47 | While[LinkReadyQ[link, 0.5], LinkRead[link];]; 48 | LinkWrite[link, Unevaluated[$VersionNumber]]; 49 | res = StringTrim[ToString[LinkRead[link]], "ReturnPacket[" | "]"]; 50 | LinkClose[link]; 51 | If[!StringContainsQ[res, "[" | "]"], 52 | Return[res];, 53 | Return[$Failed]; 54 | ]; 55 | ]; 56 | 57 | (* determine display name for Jupyter installation from Wolfram Engine $Version/$VersionNumber *) 58 | (* returns {Kernel ID, Display Name} *) 59 | getNames[mathBin_String, notProvidedQ_?BooleanQ] := 60 | Module[{version, installDir, (* names, hashedKernelUUID *) versionStr}, 61 | (* if Wolfram Engine binary not provided, just evaluate $Version in the current session *) 62 | (* otherwise, use MathLink to obtain $Version *) 63 | If[ 64 | notProvidedQ, 65 | version = ToString[$VersionNumber]; 66 | installDir = $InstallationDirectory; 67 | , 68 | version = Quiet[getVersionFromKernel[mathBin]]; 69 | If[ 70 | FailureQ[version], 71 | Return[$Failed]; 72 | ]; 73 | installDir = mathBin; 74 | ]; 75 | 76 | (* 77 | 78 | hashedKernelUUID = StringJoin["wl-script-", Hash[installDir, "SHA", "HexString"]]; 79 | 80 | names = StringCases[version, name___ ~~ " for " ~~ ("Mac" | "Microsoft" | "Windows" | "Linux") -> name]; 81 | Return[ 82 | If[Length[names] > 0, 83 | { 84 | ToLowerCase[StringJoin[ 85 | "WolframLanguage-script-", 86 | StringReplace[First[names], Whitespace -> "-"] 87 | ]], 88 | StringJoin[ 89 | "Wolfram Language (", 90 | Capitalize[ 91 | First[names], 92 | "AllWords" 93 | ], 94 | ") | Script Install" 95 | ] 96 | } 97 | , 98 | {hashedKernelUUID, "Wolfram Language | Script Install"} 99 | ] 100 | ]; 101 | 102 | *) 103 | 104 | versionStr = StringTrim[version, "."]; 105 | Return[ 106 | { 107 | (* Kernel ID *) 108 | StringJoin["wolframlanguage", versionStr], 109 | (* Display Name *) 110 | StringJoin["Wolfram Language ", versionStr] 111 | } 112 | ]; 113 | ]; 114 | 115 | (* determine symbols related to finding Wolfram Engine and Jupyter installations *) 116 | (* mathBinSession: WolframKernel location for the current session *) 117 | (* fileExt: file extension for executables *) 118 | (* pathSeperator: delimiter for directories on PATH *) 119 | defineGlobalVars[] := 120 | Switch[ 121 | $OperatingSystem, 122 | "Windows", 123 | mathBinSession = FileNameJoin[{$InstallationDirectory, "wolfram.exe"}]; 124 | fileExt = ".exe"; 125 | pathSeperator = ";";, 126 | "MacOSX", 127 | mathBinSession = FileNameJoin[{$InstallationDirectory, "MacOS", "WolframKernel"}]; 128 | fileExt = ""; 129 | pathSeperator = ":";, 130 | "Unix", 131 | mathBinSession = FileNameJoin[{$InstallationDirectory, "Executables", "WolframKernel"}]; 132 | fileExt = ""; 133 | pathSeperator = ":"; 134 | ]; 135 | 136 | mathBinSession := (defineGlobalVars[]; mathBinSession); 137 | fileExt := (defineGlobalVars[]; fileExt); 138 | pathSeperator := (defineGlobalVars[]; pathSeperator); 139 | 140 | (* a list of directories in PATH *) 141 | splitPath := StringSplit[Environment["PATH"], pathSeperator]; 142 | 143 | (* restore PATH, if due to a bug, it becomes essentially empty; this is relevant to finding the Jupyter installation *) 144 | (* returns above *) 145 | attemptPathRegeneration[] := If[ 146 | $OperatingSystem === "MacOSX" && FileType["~/.profile"] === File, 147 | Print["install.wls: Warning: Regenerating PATH ..."]; 148 | SetEnvironment[ 149 | "PATH" -> StringTrim[ 150 | RunProcess[ 151 | $SystemShell, 152 | "StandardOutput", 153 | StringJoin[Import["~/.profile", "String"], "\necho $PATH"], 154 | ProcessEnvironment -> {} 155 | ], 156 | "\n" 157 | ] 158 | ]; 159 | ]; 160 | 161 | (* find Jupyter installation path *) 162 | (* returns kernel IDs in Jupyter *) 163 | findJupyterPath[] := 164 | SelectFirst[ 165 | splitPath, 166 | (* check every directory in PATH to see if a Jupyter binary is a member *) 167 | (FileType[FileNameJoin[{#1, StringJoin["jupyter", fileExt]}]] === File)& 168 | ]; 169 | 170 | (* get information about installed kernels in Jupyter *) 171 | (* returns kernel IDs in Jupyter *) 172 | getKernels[jupyterPath_String, processEnvironment_] := 173 | Module[{json, kernelspecAssoc}, 174 | (* obtain information about "jupyter kernelspec list" in JSON *) 175 | json = Quiet[ImportString[RunProcess[{jupyterPath, "kernelspec", "list", "--json"}, "StandardOutput", ProcessEnvironment -> processEnvironment], "JSON"]]; 176 | (* transform that JSON information into an Association *) 177 | kernelspecAssoc = 178 | If[ 179 | FailureQ[json], 180 | Association[], 181 | Replace[ 182 | json, 183 | part_List /; AllTrue[part, Head[#1] === Rule &] -> Association @ part, 184 | {0, Infinity} 185 | ] 186 | ]; 187 | Return[ 188 | (* if the above process worked, just return the kernel IDs of all the kernelspecs *) 189 | (* otherwise, return an empty list *) 190 | If[ 191 | KeyExistsQ[kernelspecAssoc, "kernelspecs"], 192 | Keys[kernelspecAssoc["kernelspecs"]], 193 | {} 194 | ] 195 | ]; 196 | ]; 197 | 198 | 199 | (* END: Helper symbols *) 200 | 201 | (* main install command *) 202 | (* specs: options \"WolframEngineBinary\" and \"JupyterInstallation\" in an Association, when provided *) 203 | (* removeQ: remove a Jupyter installation or not *) 204 | (* removeAllQ: clear all Jupyter installations or not *) 205 | (* removeQ first, removeAllQ second: "add" is False, False; "remove" is True, False, and "clear" is True, True *) 206 | configureJupyter[specs_Association, removeQ_?BooleanQ, removeAllQ_?BooleanQ] := 207 | Module[ 208 | { 209 | kernelScript, 210 | retrievedNames, kernelID, displayName, 211 | notProvidedQ, 212 | jupyterPath, mathBin, 213 | fileType, 214 | processEnvironment, 215 | baseDir, tempDir, 216 | wlKernels, (* wlKernelsL(owerCase) *) wlKernelsL, 217 | commandArgs, 218 | exitInfo, kernelspecAssoc, kernelspecs, 219 | conflictMessage, failureMessage 220 | }, 221 | 222 | kernelScript = FileNameJoin[{projectHome, "WolframLanguageForJupyter", "Resources", "KernelForWolframLanguageForJupyter.wl"}]; 223 | (* just check that the REPL script is there *) 224 | If[ 225 | !(FileType[kernelScript] === File), 226 | Print[nopaclet]; 227 | Return[$Failed]; 228 | ]; 229 | 230 | jupyterPath = specs["JupyterInstallation"]; 231 | (* if no Jupyter installation path provided, determine it from PATH *) 232 | If[ 233 | MissingQ[jupyterPath], 234 | jupyterPath = findJupyterPath[]; 235 | (* if Jupyter not on PATH, message *) 236 | If[MissingQ[jupyterPath], 237 | Print[notfound]; 238 | Return[$Failed]; 239 | ]; 240 | jupyterPath = FileNameJoin[{jupyterPath, StringJoin["jupyter", fileExt]}]; 241 | ]; 242 | 243 | mathBin = 244 | Lookup[ 245 | specs, 246 | "WolframEngineBinary", 247 | (* if no "WolframEngineBinary" provided, use the session Wolfram Kernel location and set notProvidedQ to True *) 248 | (notProvidedQ = True; mathBinSession) 249 | ]; 250 | 251 | (* check that the Jupyter installation path is a file *) 252 | If[ 253 | !((fileType = FileType[jupyterPath]) === File), 254 | Switch[ 255 | fileType, 256 | Directory, 257 | Print[isdir];, 258 | None, 259 | Print[nobin]; 260 | ]; 261 | Return[$Failed]; 262 | ]; 263 | 264 | {kernelID, displayName} = {"", ""}; 265 | (* if not clearing, check that the Wolfram Engine installation path is a file, and message appropriately *) 266 | If[ 267 | !(removeQ && removeAllQ), 268 | If[ 269 | (fileType = FileType[mathBin]) === File, 270 | (* get the "Kernel ID" and "Display Name" for the new Jupyter kernel *) 271 | retrievedNames = getNames[mathBin, TrueQ[notProvidedQ]]; 272 | If[FailureQ[retrievedNames], Print[nolink]; Return[$Failed]]; 273 | {kernelID, displayName} = retrievedNames;, 274 | Switch[ 275 | fileType, 276 | Directory, 277 | Print[isdirMath];, 278 | None, 279 | Print[nobinMath]; 280 | ]; 281 | Return[$Failed]; 282 | ]; 283 | ]; 284 | 285 | (* as an association for 11.3 compatibility *) 286 | processEnvironment = Association[GetEnvironment[]]; 287 | processEnvironment["PATH"] = StringJoin[Environment["PATH"], pathSeperator, DirectoryName[jupyterPath]]; 288 | 289 | (* list of kernels in Jupyter to perform an action on *) 290 | wlKernels = {kernelID}; 291 | tempDir = ""; 292 | (* if adding, ...*) 293 | (* otherwise, when removing or clearing, ...*) 294 | If[ 295 | !removeQ, 296 | failureMessage = notadded; 297 | conflictMessage = addconflict; 298 | 299 | (* create staging directory for files needed to register a kernel with Jupyter *) 300 | tempDir = CreateDirectory[ 301 | FileNameJoin[{ 302 | projectHome, 303 | CreateUUID[], 304 | (* removing this would cause every evalution of addKernelToJupyter adds a new kernel with a different uuid *) 305 | kernelID 306 | }], CreateIntermediateDirectories -> True 307 | ]; 308 | 309 | (* export a JSON file to the staging directory that contains all the relevant information on how to run the kernel *) 310 | Export[ 311 | FileNameJoin[{tempDir, "kernel.json"}], 312 | Association[ 313 | "argv" -> {mathBin, "-script", kernelScript, "{connection_file}", "ScriptInstall" (* , "-noprompt" *)}, 314 | "display_name" -> displayName, 315 | "language" -> "Wolfram Language" 316 | ] 317 | ]; 318 | 319 | (* create a list of arguments that directs Jupyter to install from the staging directory *) 320 | commandArgs = {jupyterPath, "kernelspec", "install", "--user", tempDir};, 321 | failureMessage = notremoved; 322 | conflictMessage = removeconflict; 323 | (* create a list of arguments that directs Jupyter to remove ... *) 324 | commandArgs = {jupyterPath, "kernelspec", "remove", "-f", 325 | If[ 326 | !removeAllQ, 327 | (* just the specified kernel *) 328 | kernelID, 329 | (* all Wolfram Language Jupyter kernels *) 330 | (* select from all kernel IDs in Jupyter those that match the form used by this install *) 331 | Sequence @@ (wlKernels = Select[getKernels[jupyterPath, processEnvironment], StringMatchQ[#1, (* ("WolframLanguage-" | "wl-") *) "WolframLanguage" ~~ ___, IgnoreCase -> True] &]) 332 | ] 333 | } 334 | ]; 335 | (* if no kernels to act on, quit *) 336 | If[Length[wlKernels] == 0, Return[];]; 337 | wlKernelsL = ToLowerCase /@ wlKernels; 338 | 339 | (* for error detection, get a snapshot of kernels before the action is performed *) 340 | kernelspecs = getKernels[jupyterPath, processEnvironment]; 341 | (* when adding, if there is a kernel with the same id already in Jupyter, it will be replaced; thus, message, but continue *) 342 | If[Xor[removeQ, SubsetQ[kernelspecs, wlKernelsL]], Print[conflictMessage];]; 343 | 344 | (* perform the action *) 345 | exitInfo = RunProcess[commandArgs, All, ProcessEnvironment -> processEnvironment]; 346 | (* remove temporary directory if it was created *) 347 | If[StringLength[tempDir] > 0, DeleteDirectory[DirectoryName[tempDir], DeleteContents -> True]]; 348 | 349 | (* get list of kernels after the action was performed *) 350 | kernelspecs = getKernels[jupyterPath, processEnvironment]; 351 | (* message about success with respect to the action that was performed *) 352 | If[ 353 | !Xor[removeQ, SubsetQ[kernelspecs, wlKernelsL]], 354 | Print[failureMessage]; 355 | Print["configure-jupyter.wls: See below for the message that Jupyter returned when attempting to add the Wolfram Engine."]; 356 | Print[StringTrim[exitInfo["StandardError"], Whitespace]]; 357 | Return[$Failed]; 358 | ]; 359 | ]; 360 | 361 | (* checking RunProcess ..., and messaging appropriately *) 362 | If[ 363 | FailureQ[RunProcess[$SystemShell, All, ""]], 364 | (* maybe remove *) 365 | If[ 366 | MemberQ[$CommandLine, "-script"], 367 | Print["configure-jupyter.wls: Please use -file instead of -script in WolframScript."]; 368 | Quit[]; 369 | , 370 | Print["configure-jupyter.wls: An unknown error has occurred."]; 371 | attemptPathRegeneration[]; 372 | If[FailureQ[RunProcess[$SystemShell, All, ""]], Quit[]]; 373 | ]; 374 | ]; 375 | 376 | defineGlobalVars[]; 377 | 378 | (* maybe remove *) 379 | (* checking PATH ..., and messaging appropriately *) 380 | If[ 381 | Length[splitPath] == 1, 382 | Print["configure-jupyter.wls: Warning: This script has encountered a very small PATH environment variable."]; 383 | Print["configure-jupyter.wls: Warning: This can occur due to a possible WolframScript bug."]; 384 | attemptPathRegeneration[]; 385 | ]; 386 | 387 | 388 | (* START: Building usage message *) 389 | 390 | templateJupyterPath = StringJoin["\"", FileNameJoin[{"path", "to", "Jupyter-binary"}], "\""]; 391 | templateWLPath = StringJoin["\"", FileNameJoin[{"", "absolute", "path", "to", "Wolfram-Engine-binary--not-wolframscript"}], "\""]; 392 | 393 | (* helpMessage = StringJoin[ 394 | "configure-jupyter.wls add [", templateJupyterPath, "]\n", 395 | "configure-jupyter.wls adds a Wolfram Engine to a Jupyter binary on PATH, or optional provided Jupyter binary path\n", 396 | "configure-jupyter.wls add ", templateJupyterPath, " ", templateWLPath, "\n", 397 | "\tadds the provided absolute Wolfram Engine binary path to the provided Jupyter binary path\n", 398 | "configure-jupyter.wls remove [", templateJupyterPath ,"]\n", 399 | "\tremoves any Wolfram Engines found on a Jupyter binary on PATH, or optional provided Jupyter binary path" 400 | ]; *) 401 | 402 | helpMessage = StringJoin[ 403 | "configure-jupyter.wls add [", templateWLPath, "]\n", 404 | "\tadds a Wolfram Engine, either attached to the current invocation, or at the provided absolute Wolfram Engine binary path, to a Jupyter binary on PATH\n", 405 | "configure-jupyter.wls add ", templateWLPath, " ", templateJupyterPath, "\n", 406 | "\tadds the provided absolute Wolfram Engine binary path to the provided Jupyter binary path\n", 407 | "configure-jupyter.wls remove [", templateWLPath ,"]\n", 408 | "\tremoves the Wolfram Engine, either attached to the current invocation, or at the provided absolute Wolfram Engine binary path, from a Jupyter binary on PATH\n", 409 | "configure-jupyter.wls remove ", templateWLPath, " ", templateJupyterPath, "\n", 410 | "\tremoves the provided absolute Wolfram Engine binary path from the provided Jupyter binary path\n", 411 | "configure-jupyter.wls clear [", templateJupyterPath ,"]\n", 412 | "\tremoves all Wolfram Engines found on a Jupyter binary on PATH, or optional provided Jupyter binary path\n", 413 | "configure-jupyter.wls build\n", 414 | "\tbuilds the WolframLanguageForJupyter paclet in the project directory" 415 | ]; 416 | 417 | (* END: Building usage message *) 418 | 419 | 420 | (* based off of the script invocation, use configureJupyter or PackPaclet; or display help message *) 421 | If[ 422 | Length[$ScriptCommandLine] < 2 || 423 | Length[$ScriptCommandLine] > 4 || 424 | $ScriptCommandLine[[2]] === "help", 425 | Print[helpMessage]; 426 | , 427 | Switch[ 428 | $ScriptCommandLine[[2]], 429 | "add" | "Add", 430 | command = {False, False};, 431 | "remove" | "Remove", 432 | command = {True, False};, 433 | "clear" | "Clear", 434 | command = {True, True};, 435 | "build", 436 | PackPaclet["WolframLanguageForJupyter"]; 437 | Quit[]; 438 | , 439 | _, 440 | Print[helpMessage]; 441 | ]; 442 | 443 | configureJupyter[ 444 | Switch[ 445 | Length[$ScriptCommandLine], 446 | 4, 447 | Association[ 448 | "WolframEngineBinary" -> $ScriptCommandLine[[3]], 449 | "JupyterInstallation" -> $ScriptCommandLine[[4]] 450 | ], 451 | 3, 452 | If[command === {True, True}, 453 | Association["JupyterInstallation" -> $ScriptCommandLine[[3]]], 454 | Association["WolframEngineBinary" -> $ScriptCommandLine[[3]]] 455 | ], 456 | 2, 457 | Association[] 458 | ], 459 | Sequence @@ command 460 | ]; 461 | ]; 462 | 463 | End[]; 464 | -------------------------------------------------------------------------------- /extras/custom.js: -------------------------------------------------------------------------------- 1 | /* (adapted) from https://stackoverflow.com/a/19961519 by Erik Aigner */ 2 | HTMLTextAreaElement.prototype.insertAtCaret = function (text) { 3 | text = text || ''; 4 | if(document.selection) { 5 | // IE 6 | this.focus(); 7 | var sel = document.selection.createRange(); 8 | sel.text = text; 9 | } 10 | else if(this.selectionStart || this.selectionStart === 0) { 11 | // Others 12 | var startPos = this.selectionStart; 13 | var endPos = this.selectionEnd; 14 | this.value = this.value.substring(0, startPos) + text + this.value.substring(endPos, this.value.length); 15 | this.selectionStart = startPos + text.length; 16 | this.selectionEnd = startPos + text.length; 17 | } 18 | else { 19 | this.value += text; 20 | } 21 | }; 22 | 23 | /* (adapted) from https://stackoverflow.com/a/51114347 by bambam */ 24 | function redirectEsc(event) { 25 | if(event.which == 27) 26 | { 27 | event.target.insertAtCaret( 28 | /* the vertical ellipsis character */ 29 | String.fromCharCode(8942) 30 | ); 31 | event.stopImmediatePropagation(); 32 | } 33 | } 34 | 35 | /* (adapted) from https://stackoverflow.com/a/51114347 by bambam */ 36 | var observer = new MutationObserver(function(mutations) { 37 | 38 | Array.from( 39 | document.querySelectorAll('.input_area') 40 | ).forEach( 41 | textarea => 42 | { 43 | textarea.removeEventListener('keydown', redirectEsc); 44 | textarea.addEventListener('keydown', redirectEsc); 45 | } 46 | ); 47 | 48 | }); 49 | 50 | observer.observe(document, {childList:true, subtree:true}); -------------------------------------------------------------------------------- /images/in-out-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframResearch/WolframLanguageForJupyter/9a26ac78743cc47084c9c99ff75c5aee2657a409/images/in-out-01.png -------------------------------------------------------------------------------- /images/in-out-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframResearch/WolframLanguageForJupyter/9a26ac78743cc47084c9c99ff75c5aee2657a409/images/in-out-02.png -------------------------------------------------------------------------------- /images/in-out-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframResearch/WolframLanguageForJupyter/9a26ac78743cc47084c9c99ff75c5aee2657a409/images/in-out-03.png -------------------------------------------------------------------------------- /images/menu-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframResearch/WolframLanguageForJupyter/9a26ac78743cc47084c9c99ff75c5aee2657a409/images/menu-01.png --------------------------------------------------------------------------------