├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── backlog.md ├── dist └── frontend │ ├── frontend.4dd03dfa.css │ ├── frontend.e3b05c9d.js │ ├── index.html │ └── worker.2bdd663b.js ├── go.mod ├── go.sum ├── main.go ├── src ├── core │ ├── api.go │ ├── baseservices.go │ ├── cmdcli.go │ ├── cmdweb.go │ ├── httphandler │ │ ├── apinewnotebook.go │ │ ├── apinotebookexec.go │ │ ├── apinotebooksetcontent.go │ │ ├── apinotenookstop.go │ │ ├── apirenamenotebook.go │ │ ├── crsf.go │ │ ├── handlers.go │ │ ├── homepage.go │ │ └── notebook.go │ └── shared │ │ ├── docker.go │ │ ├── recipe │ │ ├── helper │ │ │ ├── defaultinitnotebook.go │ │ │ ├── stdexec.go │ │ │ └── stdrecipe.go │ │ ├── recipe_c.go │ │ ├── recipe_clojure.go │ │ ├── recipe_cpp.go │ │ ├── recipe_csharp.go │ │ ├── recipe_elixir.go │ │ ├── recipe_fsharp.go │ │ ├── recipe_go.go │ │ ├── recipe_haskell.go │ │ ├── recipe_java.go │ │ ├── recipe_lua.go │ │ ├── recipe_nodejs.go │ │ ├── recipe_ocaml.go │ │ ├── recipe_php.go │ │ ├── recipe_python3.go │ │ ├── recipe_r.go │ │ ├── recipe_ruby.go │ │ ├── recipe_rust.go │ │ ├── recipe_swift.go │ │ ├── recipe_typescript.go │ │ └── registry.go │ │ ├── service │ │ ├── csrf.go │ │ ├── notebookregistry.go │ │ ├── notebookwatcher.go │ │ ├── reciperegistry.go │ │ ├── routes.go │ │ └── sanitize.go │ │ └── types │ │ ├── callbackexechandler.go │ │ ├── container.go │ │ ├── exechandler.go │ │ ├── notebook.go │ │ ├── parameters.go │ │ ├── process.go │ │ ├── recipe.go │ │ └── streamwriter.go ├── frontend │ ├── ApiClient │ │ └── index.ts │ ├── Components │ │ ├── App │ │ │ └── index.tsx │ │ ├── Home │ │ │ └── index.tsx │ │ └── Notebook │ │ │ ├── index.tsx │ │ │ ├── style.scss │ │ │ └── worker.ts │ ├── index.html │ ├── index.tsx │ ├── package-lock.json │ ├── package.json │ ├── tsconfig.json │ └── types.ts └── recipes │ ├── c │ └── defaultcontent │ │ └── main.c │ ├── clojure │ └── defaultcontent │ │ └── index.clj │ ├── cpp │ └── defaultcontent │ │ └── main.cpp │ ├── csharp │ └── defaultcontent │ │ ├── Program.cs │ │ └── app.csproj │ ├── elixir │ └── defaultcontent │ │ └── main.ex │ ├── fsharp │ └── defaultcontent │ │ ├── Program.fs │ │ └── app.fsproj │ ├── go │ └── defaultcontent │ │ └── main.go │ ├── haskell │ └── defaultcontent │ │ └── main.hs │ ├── java │ └── defaultcontent │ │ └── main.java │ ├── lua │ └── defaultcontent │ │ └── main.lua │ ├── nodejs │ └── defaultcontent │ │ ├── .gitignore │ │ └── index.js │ ├── ocaml │ └── defaultcontent │ │ └── index.ml │ ├── php │ └── defaultcontent │ │ └── main.php │ ├── plaintext │ ├── defaultcontent │ │ └── main.txt │ └── index.ts │ ├── python3 │ └── defaultcontent │ │ └── main.py │ ├── r │ └── defaultcontent │ │ └── main.r │ ├── ruby │ └── defaultcontent │ │ └── main.rb │ ├── rust │ └── defaultcontent │ │ └── main.rs │ ├── swift │ └── defaultcontent │ │ └── main.swift │ └── typescript │ └── defaultcontent │ ├── .gitignore │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json └── test └── fixtures └── notebooks ├── C++ └── main.cpp ├── C └── main.c ├── Elixir └── main.ex ├── Go └── main.go ├── Haskell └── main.hs ├── Java └── main.java ├── Javascript └── index.js ├── Lua └── main.lua ├── PHP └── main.php ├── Python └── main.py ├── R └── main.r ├── Ruby └── main.rb ├── Rust └── main.rs ├── Swift └── main.swift └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | /dist/frontend/*.map 4 | .DS_Store 5 | src/core/core 6 | dist/nbk 7 | pkged.go 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020 Jérôme Schneider 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: deps build install deps-frontend build-frontend embed-frontend 2 | 3 | deps: 4 | go get github.com/markbates/pkger/cmd/pkger 5 | 6 | embed-frontend: 7 | pkger 8 | 9 | build: embed-frontend 10 | go build -o dist/nodebook . 11 | 12 | install: embed-frontend 13 | go install . 14 | @echo "nodebook built and installed." 15 | 16 | deps-frontend: 17 | cd src/frontend && npm i 18 | 19 | build-frontend: 20 | cd src/frontend && npm run build 21 | rm -Rf dist/frontend/*.map 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodebook 2 | 3 | Nodebook - Multi-Language REPL with Web UI + CLI code runner 4 | 5 | Useful to practice algorithms and datastructures for coding interviews. 6 | 7 | ## What is it? 8 | 9 | Nodebook is an in-browser REPL supporting many programming languages. Code's on the left, Console's on the right. Click "Run" or press Ctrl+Enter or Cmd+Enter to run your code. 10 | Code is automatically persisted on the file system. 11 | 12 | **You can also use Nodebook directly on the command line**, running your notebooks upon change. 13 | 14 | ![nodebook](https://user-images.githubusercontent.com/4974818/45320903-2cdec280-b544-11e8-9b2e-067b646de751.png) 15 | 16 | A notebook is a folder containing an `{index|main}.{js,py,c,cpp,...}` file. The homepage lists all of the available notebooks. 17 | 18 | ![home](https://user-images.githubusercontent.com/4974818/45383977-fde05380-b60c-11e8-91cc-06548dd4fae8.png) 19 | 20 | ## Supported environments 21 | 22 | * C11 `(.c)` 23 | * C++14 `(.cpp)` 24 | * C# `(.cs)` 25 | * Clojure `(.clj)` 26 | * Elixir `(.ex)` 27 | * Fsharp `(.fs)` 28 | * Go `(.go)` 29 | * Haskell `(.hs)` 30 | * Java `(.java)` 31 | * NodeJS `(.js)` 32 | * Lua `(.lua)` 33 | * OCaml `(.ml)` 34 | * PHP `(.php)` 35 | * Python 3 `(.py)` 36 | * R `(.r, .R)` 37 | * Ruby `(.rb)` 38 | * Rust `(.rs)` — Uses `cargo run` if `Cargo.toml` is present, and `rustc` otherwise 39 | * Swift `(.swift)` 40 | * TypeScript `(.ts)` 41 | 42 | If `--docker` is set on the command line, each of these environments will run inside a specific docker container. 43 | 44 | Otherwise, the local toolchains will be used. 45 | 46 | ## Install from release 47 | 48 | Head to [Releases](https://github.com/netgusto/nodebook/releases/latest) and download the binary built for your system (mac, linux). 49 | 50 | Rename it to `nodebook` and place it in your path. 51 | 52 | ## Install from source 53 | 54 | Building requires go. 55 | 56 | ```bash 57 | $ make deps 58 | $ make install 59 | # nodebook should be available under $GOPATH/bin/nodebook or $GOBIN/nodebook 60 | ``` 61 | 62 | ## Run with Web UI 63 | 64 | ``` 65 | # With dockerized toolchains 66 | $ nodebook --docker path/to/notebooks 67 | 68 | # With local toolchains 69 | $ nodebook path/to/notebooks 70 | ``` 71 | 72 | # Run on CLI (watch and run mode) 73 | 74 | ``` 75 | $ nodebook cli --docker path/to/notebooks 76 | # Or 77 | $ nodebook cli path/to/notebooks 78 | ``` 79 | 80 | ## Usage 81 | 82 | ### Create a Notebook (Web UI) 83 | 84 | Click on the **+ Notebook** button on the Home page, then select the language of the notebook to be created. 85 | 86 | Once on the notebook edition page, you can rename the notebook by clicking on it's name. 87 | 88 | Notebooks are created in the directory specified by the parameter `--notebooks`. 89 | 90 | ### Create a Notebook manually (WebUI, CLI) 91 | 92 | In the directory where you want your notebooks to be stored, simply create a folder containing a file named `{index|main}.{js,py,c,cpp,...}`. 93 | 94 | The notebook's name will be the name of the folder. The notebook language is determined automatically. 95 | 96 | ### Command line options 97 | 98 | * **--docker**: Execute code in disposable docker containers instead of local system; defaults to `false` 99 | 100 | **Web UI only**: 101 | 102 | * **--bindaddress**: IP address the http server should bind to; defaults to `127.0.0.1` 103 | * **--port**: Port used by the application; defaults to `8000` 104 | 105 | ### Notebook environment 106 | 107 | If your notebook dir contains a `.env` file, the corresponding environment will be set up during notebook execution. 108 | 109 | Exemple `.env`: 110 | 111 | ``` 112 | HELLO=World! 113 | ``` 114 | 115 | More information about the expected file format here: 116 | 117 | ## ⚠️ A bit of warning ⚠️ 118 | 119 | Do not run the Web UI on a port open to public traffic! Doing so would allow remote code execution on your machine. 120 | 121 | By default, the server binds to `127.0.0.1`, which allows connection from the localhost only. You can override the bind address using `--bindaddress`, but do it only if you know what you're doing. 122 | 123 | -------------------------------------------------------------------------------- /backlog.md: -------------------------------------------------------------------------------- 1 | * recipe: markdown 2 | * organize notebooks 3 | * packages autoinstall -------------------------------------------------------------------------------- /dist/frontend/frontend.4dd03dfa.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes scale{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}45%{-webkit-transform:scale(.1);transform:scale(.1);opacity:.7}80%{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.ball-pulse>div:nth-child(0){-webkit-animation:scale .75s cubic-bezier(.2,.68,.18,1.08) -.36s infinite;animation:scale .75s cubic-bezier(.2,.68,.18,1.08) -.36s infinite}.ball-pulse>div:first-child{-webkit-animation:scale .75s cubic-bezier(.2,.68,.18,1.08) -.24s infinite;animation:scale .75s cubic-bezier(.2,.68,.18,1.08) -.24s infinite}.ball-pulse>div:nth-child(2){-webkit-animation:scale .75s cubic-bezier(.2,.68,.18,1.08) -.12s infinite;animation:scale .75s cubic-bezier(.2,.68,.18,1.08) -.12s infinite}.ball-pulse>div:nth-child(3){-webkit-animation:scale .75s cubic-bezier(.2,.68,.18,1.08) 0s infinite;animation:scale .75s cubic-bezier(.2,.68,.18,1.08) 0s infinite}.ball-pulse>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block}@-webkit-keyframes ball-pulse-sync{33%{-webkit-transform:translateY(10px);transform:translateY(10px)}66%{-webkit-transform:translateY(-10px);transform:translateY(-10px)}to{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes ball-pulse-sync{33%{-webkit-transform:translateY(10px);transform:translateY(10px)}66%{-webkit-transform:translateY(-10px);transform:translateY(-10px)}to{-webkit-transform:translateY(0);transform:translateY(0)}}.ball-pulse-sync>div:nth-child(0){-webkit-animation:ball-pulse-sync .6s ease-in-out -.21s infinite;animation:ball-pulse-sync .6s ease-in-out -.21s infinite}.ball-pulse-sync>div:first-child{-webkit-animation:ball-pulse-sync .6s ease-in-out -.14s infinite;animation:ball-pulse-sync .6s ease-in-out -.14s infinite}.ball-pulse-sync>div:nth-child(2){-webkit-animation:ball-pulse-sync .6s ease-in-out -.07s infinite;animation:ball-pulse-sync .6s ease-in-out -.07s infinite}.ball-pulse-sync>div:nth-child(3){-webkit-animation:ball-pulse-sync .6s ease-in-out 0s infinite;animation:ball-pulse-sync .6s ease-in-out 0s infinite}.ball-pulse-sync>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block}@-webkit-keyframes ball-scale{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1);opacity:0}}@keyframes ball-scale{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1);opacity:0}}.ball-scale>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;height:60px;width:60px;-webkit-animation:ball-scale 1s ease-in-out 0s infinite;animation:ball-scale 1s ease-in-out 0s infinite}.ball-scale-random{width:37px;height:40px}.ball-scale-random>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;display:inline-block;height:30px;width:30px;-webkit-animation:ball-scale 1s ease-in-out 0s infinite;animation:ball-scale 1s ease-in-out 0s infinite}.ball-scale-random>div:first-child{margin-left:-7px;-webkit-animation:ball-scale 1s ease-in-out .2s infinite;animation:ball-scale 1s ease-in-out .2s infinite}.ball-scale-random>div:nth-child(3){margin-left:-2px;margin-top:9px;-webkit-animation:ball-scale 1s ease-in-out .5s infinite;animation:ball-scale 1s ease-in-out .5s infinite}@-webkit-keyframes rotate{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.ball-rotate,.ball-rotate>div{position:relative}.ball-rotate>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both}.ball-rotate>div:first-child{-webkit-animation:rotate 1s cubic-bezier(.7,-.13,.22,.86) 0s infinite;animation:rotate 1s cubic-bezier(.7,-.13,.22,.86) 0s infinite}.ball-rotate>div:after,.ball-rotate>div:before{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;content:"";position:absolute;opacity:.8}.ball-rotate>div:before{top:0;left:-28px}.ball-rotate>div:after{top:0;left:25px}.ball-clip-rotate>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;border:2px solid;border-color:#fff #fff transparent;height:25px;width:25px;background:transparent!important;display:inline-block;-webkit-animation:rotate .75s linear 0s infinite;animation:rotate .75s linear 0s infinite}@keyframes scale{30%{-webkit-transform:scale(.3);transform:scale(.3)}to{-webkit-transform:scale(1);transform:scale(1)}}.ball-clip-rotate-pulse{position:relative;-webkit-transform:translateY(-15px);-ms-transform:translateY(-15px);transform:translateY(-15px)}.ball-clip-rotate-pulse>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;top:0;left:0;border-radius:100%}.ball-clip-rotate-pulse>div:first-child{background:#fff;height:16px;width:16px;top:7px;left:-7px;-webkit-animation:scale 1s cubic-bezier(.09,.57,.49,.9) 0s infinite;animation:scale 1s cubic-bezier(.09,.57,.49,.9) 0s infinite}.ball-clip-rotate-pulse>div:last-child{position:absolute;width:30px;height:30px;left:-16px;top:-2px;background:transparent;border:2px solid;border-color:#fff transparent;border-style:solid;border-width:2px;-webkit-animation:rotate 1s cubic-bezier(.09,.57,.49,.9) 0s infinite;animation:rotate 1s cubic-bezier(.09,.57,.49,.9) 0s infinite;-webkit-animation-duration:1s;animation-duration:1s}@keyframes rotate{0%{-webkit-transform:rotate(0deg) scale(1);transform:rotate(0deg) scale(1)}50%{-webkit-transform:rotate(180deg) scale(.6);transform:rotate(180deg) scale(.6)}to{-webkit-transform:rotate(1turn) scale(1);transform:rotate(1turn) scale(1)}}.ball-clip-rotate-multiple{position:relative}.ball-clip-rotate-multiple>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;left:-20px;top:-20px;border-color:transparent #fff;border-style:solid;border-width:2px;border-radius:100%;height:35px;width:35px;-webkit-animation:rotate 1s ease-in-out 0s infinite;animation:rotate 1s ease-in-out 0s infinite}.ball-clip-rotate-multiple>div:last-child{display:inline-block;top:-10px;left:-10px;width:15px;height:15px;-webkit-animation-duration:.5s;animation-duration:.5s;border-color:#fff transparent;-webkit-animation-direction:reverse;animation-direction:reverse}@-webkit-keyframes ball-scale-ripple{0%{-webkit-transform:scale(.1);transform:scale(.1);opacity:1}70%{-webkit-transform:scale(1);transform:scale(1);opacity:.7}to{opacity:0}}@keyframes ball-scale-ripple{0%{-webkit-transform:scale(.1);transform:scale(.1);opacity:1}70%{-webkit-transform:scale(1);transform:scale(1);opacity:.7}to{opacity:0}}.ball-scale-ripple>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;height:50px;width:50px;border-radius:100%;border:2px solid #fff;-webkit-animation:ball-scale-ripple 1s cubic-bezier(.21,.53,.56,.8) 0s infinite;animation:ball-scale-ripple 1s cubic-bezier(.21,.53,.56,.8) 0s infinite}@-webkit-keyframes ball-scale-ripple-multiple{0%{-webkit-transform:scale(.1);transform:scale(.1);opacity:1}70%{-webkit-transform:scale(1);transform:scale(1);opacity:.7}to{opacity:0}}@keyframes ball-scale-ripple-multiple{0%{-webkit-transform:scale(.1);transform:scale(.1);opacity:1}70%{-webkit-transform:scale(1);transform:scale(1);opacity:.7}to{opacity:0}}.ball-scale-ripple-multiple{position:relative;-webkit-transform:translateY(-25px);-ms-transform:translateY(-25px);transform:translateY(-25px)}.ball-scale-ripple-multiple>div:nth-child(0){-webkit-animation-delay:-.8s;animation-delay:-.8s}.ball-scale-ripple-multiple>div:first-child{-webkit-animation-delay:-.6s;animation-delay:-.6s}.ball-scale-ripple-multiple>div:nth-child(2){-webkit-animation-delay:-.4s;animation-delay:-.4s}.ball-scale-ripple-multiple>div:nth-child(3){-webkit-animation-delay:-.2s;animation-delay:-.2s}.ball-scale-ripple-multiple>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;top:-2px;left:-26px;width:50px;height:50px;border-radius:100%;border:2px solid #fff;-webkit-animation:ball-scale-ripple-multiple 1.25s cubic-bezier(.21,.53,.56,.8) 0s infinite;animation:ball-scale-ripple-multiple 1.25s cubic-bezier(.21,.53,.56,.8) 0s infinite}@-webkit-keyframes ball-beat{50%{opacity:.2;-webkit-transform:scale(.75);transform:scale(.75)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes ball-beat{50%{opacity:.2;-webkit-transform:scale(.75);transform:scale(.75)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.ball-beat>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;-webkit-animation:ball-beat .7s linear 0s infinite;animation:ball-beat .7s linear 0s infinite}.ball-beat>div:nth-child(2n-1){-webkit-animation-delay:-.35s!important;animation-delay:-.35s!important}@-webkit-keyframes ball-scale-multiple{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}5%{opacity:1}to{-webkit-transform:scale(1);transform:scale(1);opacity:0}}@keyframes ball-scale-multiple{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}5%{opacity:1}to{-webkit-transform:scale(1);transform:scale(1);opacity:0}}.ball-scale-multiple{position:relative;-webkit-transform:translateY(-30px);-ms-transform:translateY(-30px);transform:translateY(-30px)}.ball-scale-multiple>div:nth-child(2){-webkit-animation-delay:-.4s;animation-delay:-.4s}.ball-scale-multiple>div:nth-child(3){-webkit-animation-delay:-.2s;animation-delay:-.2s}.ball-scale-multiple>div{background-color:#fff;width:15px;height:15px;border-radius:100%;-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;left:-30px;top:0;opacity:0;margin:0;width:60px;height:60px;-webkit-animation:ball-scale-multiple 1s linear 0s infinite;animation:ball-scale-multiple 1s linear 0s infinite}@-webkit-keyframes ball-triangle-path-1{33%{-webkit-transform:translate(25px,-50px);transform:translate(25px,-50px)}66%{-webkit-transform:translate(50px);transform:translate(50px)}to{-webkit-transform:translate(0);transform:translate(0)}}@keyframes ball-triangle-path-1{33%{-webkit-transform:translate(25px,-50px);transform:translate(25px,-50px)}66%{-webkit-transform:translate(50px);transform:translate(50px)}to{-webkit-transform:translate(0);transform:translate(0)}}@-webkit-keyframes ball-triangle-path-2{33%{-webkit-transform:translate(25px,50px);transform:translate(25px,50px)}66%{-webkit-transform:translate(-25px,50px);transform:translate(-25px,50px)}to{-webkit-transform:translate(0);transform:translate(0)}}@keyframes ball-triangle-path-2{33%{-webkit-transform:translate(25px,50px);transform:translate(25px,50px)}66%{-webkit-transform:translate(-25px,50px);transform:translate(-25px,50px)}to{-webkit-transform:translate(0);transform:translate(0)}}@-webkit-keyframes ball-triangle-path-3{33%{-webkit-transform:translate(-50px);transform:translate(-50px)}66%{-webkit-transform:translate(-25px,-50px);transform:translate(-25px,-50px)}to{-webkit-transform:translate(0);transform:translate(0)}}@keyframes ball-triangle-path-3{33%{-webkit-transform:translate(-50px);transform:translate(-50px)}66%{-webkit-transform:translate(-25px,-50px);transform:translate(-25px,-50px)}to{-webkit-transform:translate(0);transform:translate(0)}}.ball-triangle-path{position:relative;-webkit-transform:translate(-29.994px,-37.50938px);-ms-transform:translate(-29.994px,-37.50938px);transform:translate(-29.994px,-37.50938px)}.ball-triangle-path>div:first-child{-webkit-animation-name:ball-triangle-path-1;animation-name:ball-triangle-path-1;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.ball-triangle-path>div:first-child,.ball-triangle-path>div:nth-child(2){-webkit-animation-delay:0;animation-delay:0;-webkit-animation-duration:2s;animation-duration:2s}.ball-triangle-path>div:nth-child(2){-webkit-animation-name:ball-triangle-path-2;animation-name:ball-triangle-path-2;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.ball-triangle-path>div:nth-child(3){-webkit-animation-name:ball-triangle-path-3;animation-name:ball-triangle-path-3;-webkit-animation-delay:0;animation-delay:0;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.ball-triangle-path>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;width:10px;height:10px;border-radius:100%;border:1px solid #fff}.ball-triangle-path>div:first-of-type{top:50px}.ball-triangle-path>div:nth-of-type(2){left:25px}.ball-triangle-path>div:nth-of-type(3){top:50px;left:50px}@-webkit-keyframes ball-pulse-rise-even{0%{-webkit-transform:scale(1.1);transform:scale(1.1)}25%{-webkit-transform:translateY(-30px);transform:translateY(-30px)}50%{-webkit-transform:scale(.4);transform:scale(.4)}75%{-webkit-transform:translateY(30px);transform:translateY(30px)}to{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform:scale(1);transform:scale(1)}}@keyframes ball-pulse-rise-even{0%{-webkit-transform:scale(1.1);transform:scale(1.1)}25%{-webkit-transform:translateY(-30px);transform:translateY(-30px)}50%{-webkit-transform:scale(.4);transform:scale(.4)}75%{-webkit-transform:translateY(30px);transform:translateY(30px)}to{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes ball-pulse-rise-odd{0%{-webkit-transform:scale(.4);transform:scale(.4)}25%{-webkit-transform:translateY(30px);transform:translateY(30px)}50%{-webkit-transform:scale(1.1);transform:scale(1.1)}75%{-webkit-transform:translateY(-30px);transform:translateY(-30px)}to{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform:scale(.75);transform:scale(.75)}}@keyframes ball-pulse-rise-odd{0%{-webkit-transform:scale(.4);transform:scale(.4)}25%{-webkit-transform:translateY(30px);transform:translateY(30px)}50%{-webkit-transform:scale(1.1);transform:scale(1.1)}75%{-webkit-transform:translateY(-30px);transform:translateY(-30px)}to{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform:scale(.75);transform:scale(.75)}}.ball-pulse-rise>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-timing-function:cubic-bezier(.15,.46,.9,.6);animation-timing-function:cubic-bezier(.15,.46,.9,.6);-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-delay:0;animation-delay:0}.ball-pulse-rise>div:nth-child(2n){-webkit-animation-name:ball-pulse-rise-even;animation-name:ball-pulse-rise-even}.ball-pulse-rise>div:nth-child(2n-1){-webkit-animation-name:ball-pulse-rise-odd;animation-name:ball-pulse-rise-odd}@-webkit-keyframes ball-grid-beat{50%{opacity:.7}to{opacity:1}}@keyframes ball-grid-beat{50%{opacity:.7}to{opacity:1}}.ball-grid-beat{width:57px}.ball-grid-beat>div:first-child{-webkit-animation-delay:.44s;animation-delay:.44s;-webkit-animation-duration:1.27s;animation-duration:1.27s}.ball-grid-beat>div:nth-child(2){-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-duration:1.52s;animation-duration:1.52s}.ball-grid-beat>div:nth-child(3){-webkit-animation-delay:.14s;animation-delay:.14s;-webkit-animation-duration:.61s;animation-duration:.61s}.ball-grid-beat>div:nth-child(4){-webkit-animation-delay:.15s;animation-delay:.15s;-webkit-animation-duration:.82s;animation-duration:.82s}.ball-grid-beat>div:nth-child(5){-webkit-animation-delay:-.01s;animation-delay:-.01s;-webkit-animation-duration:1.24s;animation-duration:1.24s}.ball-grid-beat>div:nth-child(6){-webkit-animation-delay:-.07s;animation-delay:-.07s;-webkit-animation-duration:1.35s;animation-duration:1.35s}.ball-grid-beat>div:nth-child(7){-webkit-animation-delay:.29s;animation-delay:.29s;-webkit-animation-duration:1.44s;animation-duration:1.44s}.ball-grid-beat>div:nth-child(8){-webkit-animation-delay:.63s;animation-delay:.63s;-webkit-animation-duration:1.19s;animation-duration:1.19s}.ball-grid-beat>div:nth-child(9){-webkit-animation-delay:-.18s;animation-delay:-.18s;-webkit-animation-duration:1.48s;animation-duration:1.48s}.ball-grid-beat>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;float:left;-webkit-animation-name:ball-grid-beat;animation-name:ball-grid-beat;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-delay:0;animation-delay:0}@-webkit-keyframes ball-grid-pulse{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(.5);transform:scale(.5);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes ball-grid-pulse{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(.5);transform:scale(.5);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.ball-grid-pulse{width:57px}.ball-grid-pulse>div:first-child{-webkit-animation-delay:.58s;animation-delay:.58s;-webkit-animation-duration:.9s;animation-duration:.9s}.ball-grid-pulse>div:nth-child(2){-webkit-animation-delay:.01s;animation-delay:.01s;-webkit-animation-duration:.94s;animation-duration:.94s}.ball-grid-pulse>div:nth-child(3){-webkit-animation-delay:.25s;animation-delay:.25s;-webkit-animation-duration:1.43s;animation-duration:1.43s}.ball-grid-pulse>div:nth-child(4){-webkit-animation-delay:-.03s;animation-delay:-.03s;-webkit-animation-duration:.74s;animation-duration:.74s}.ball-grid-pulse>div:nth-child(5){-webkit-animation-delay:.21s;animation-delay:.21s;-webkit-animation-duration:.68s;animation-duration:.68s}.ball-grid-pulse>div:nth-child(6){-webkit-animation-delay:.25s;animation-delay:.25s;-webkit-animation-duration:1.17s;animation-duration:1.17s}.ball-grid-pulse>div:nth-child(7){-webkit-animation-delay:.46s;animation-delay:.46s;-webkit-animation-duration:1.41s;animation-duration:1.41s}.ball-grid-pulse>div:nth-child(8){-webkit-animation-delay:.02s;animation-delay:.02s;-webkit-animation-duration:1.56s;animation-duration:1.56s}.ball-grid-pulse>div:nth-child(9){-webkit-animation-delay:.13s;animation-delay:.13s;-webkit-animation-duration:.78s;animation-duration:.78s}.ball-grid-pulse>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;float:left;-webkit-animation-name:ball-grid-pulse;animation-name:ball-grid-pulse;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-delay:0;animation-delay:0}@-webkit-keyframes ball-spin-fade-loader{50%{opacity:.3;-webkit-transform:scale(.4);transform:scale(.4)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes ball-spin-fade-loader{50%{opacity:.3;-webkit-transform:scale(.4);transform:scale(.4)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.ball-spin-fade-loader{position:relative;top:-10px;left:-10px}.ball-spin-fade-loader>div:first-child{top:25px;left:0;-webkit-animation:ball-spin-fade-loader 1s linear -.96s infinite;animation:ball-spin-fade-loader 1s linear -.96s infinite}.ball-spin-fade-loader>div:nth-child(2){top:17.04545px;left:17.04545px;-webkit-animation:ball-spin-fade-loader 1s linear -.84s infinite;animation:ball-spin-fade-loader 1s linear -.84s infinite}.ball-spin-fade-loader>div:nth-child(3){top:0;left:25px;-webkit-animation:ball-spin-fade-loader 1s linear -.72s infinite;animation:ball-spin-fade-loader 1s linear -.72s infinite}.ball-spin-fade-loader>div:nth-child(4){top:-17.04545px;left:17.04545px;-webkit-animation:ball-spin-fade-loader 1s linear -.6s infinite;animation:ball-spin-fade-loader 1s linear -.6s infinite}.ball-spin-fade-loader>div:nth-child(5){top:-25px;left:0;-webkit-animation:ball-spin-fade-loader 1s linear -.48s infinite;animation:ball-spin-fade-loader 1s linear -.48s infinite}.ball-spin-fade-loader>div:nth-child(6){top:-17.04545px;left:-17.04545px;-webkit-animation:ball-spin-fade-loader 1s linear -.36s infinite;animation:ball-spin-fade-loader 1s linear -.36s infinite}.ball-spin-fade-loader>div:nth-child(7){top:0;left:-25px;-webkit-animation:ball-spin-fade-loader 1s linear -.24s infinite;animation:ball-spin-fade-loader 1s linear -.24s infinite}.ball-spin-fade-loader>div:nth-child(8){top:17.04545px;left:-17.04545px;-webkit-animation:ball-spin-fade-loader 1s linear -.12s infinite;animation:ball-spin-fade-loader 1s linear -.12s infinite}.ball-spin-fade-loader>div{background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute}@-webkit-keyframes ball-spin-loader{75%{opacity:.2}to{opacity:1}}@keyframes ball-spin-loader{75%{opacity:.2}to{opacity:1}}.ball-spin-loader{position:relative}.ball-spin-loader>span:first-child{top:45px;left:0;-webkit-animation:ball-spin-loader 2s linear .9s infinite;animation:ball-spin-loader 2s linear .9s infinite}.ball-spin-loader>span:nth-child(2){top:30.68182px;left:30.68182px;-webkit-animation:ball-spin-loader 2s linear 1.8s infinite;animation:ball-spin-loader 2s linear 1.8s infinite}.ball-spin-loader>span:nth-child(3){top:0;left:45px;-webkit-animation:ball-spin-loader 2s linear 2.7s infinite;animation:ball-spin-loader 2s linear 2.7s infinite}.ball-spin-loader>span:nth-child(4){top:-30.68182px;left:30.68182px;-webkit-animation:ball-spin-loader 2s linear 3.6s infinite;animation:ball-spin-loader 2s linear 3.6s infinite}.ball-spin-loader>span:nth-child(5){top:-45px;left:0;-webkit-animation:ball-spin-loader 2s linear 4.5s infinite;animation:ball-spin-loader 2s linear 4.5s infinite}.ball-spin-loader>span:nth-child(6){top:-30.68182px;left:-30.68182px;-webkit-animation:ball-spin-loader 2s linear 5.4s infinite;animation:ball-spin-loader 2s linear 5.4s infinite}.ball-spin-loader>span:nth-child(7){top:0;left:-45px;-webkit-animation:ball-spin-loader 2s linear 6.3s infinite;animation:ball-spin-loader 2s linear 6.3s infinite}.ball-spin-loader>span:nth-child(8){top:30.68182px;left:-30.68182px;-webkit-animation:ball-spin-loader 2s linear 7.2s infinite;animation:ball-spin-loader 2s linear 7.2s infinite}.ball-spin-loader>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;width:15px;height:15px;border-radius:100%;background:green}@-webkit-keyframes ball-zig{33%{-webkit-transform:translate(-15px,-30px);transform:translate(-15px,-30px)}66%{-webkit-transform:translate(15px,-30px);transform:translate(15px,-30px)}to{-webkit-transform:translate(0);transform:translate(0)}}@keyframes ball-zig{33%{-webkit-transform:translate(-15px,-30px);transform:translate(-15px,-30px)}66%{-webkit-transform:translate(15px,-30px);transform:translate(15px,-30px)}to{-webkit-transform:translate(0);transform:translate(0)}}@-webkit-keyframes ball-zag{33%{-webkit-transform:translate(15px,30px);transform:translate(15px,30px)}66%{-webkit-transform:translate(-15px,30px);transform:translate(-15px,30px)}to{-webkit-transform:translate(0);transform:translate(0)}}@keyframes ball-zag{33%{-webkit-transform:translate(15px,30px);transform:translate(15px,30px)}66%{-webkit-transform:translate(-15px,30px);transform:translate(-15px,30px)}to{-webkit-transform:translate(0);transform:translate(0)}}.ball-zig-zag{position:relative;-webkit-transform:translate(-15px,-15px);-ms-transform:translate(-15px,-15px);transform:translate(-15px,-15px)}.ball-zig-zag>div{background-color:#fff;width:15px;height:15px;border-radius:100%;-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;margin:2px 2px 2px 15px;top:4px;left:-7px}.ball-zig-zag>div:first-child{-webkit-animation:ball-zig .7s linear 0s infinite;animation:ball-zig .7s linear 0s infinite}.ball-zig-zag>div:last-child{-webkit-animation:ball-zag .7s linear 0s infinite;animation:ball-zag .7s linear 0s infinite}@-webkit-keyframes ball-zig-deflect{17%{-webkit-transform:translate(-15px,-30px);transform:translate(-15px,-30px)}34%{-webkit-transform:translate(15px,-30px);transform:translate(15px,-30px)}50%{-webkit-transform:translate(0);transform:translate(0)}67%{-webkit-transform:translate(15px,-30px);transform:translate(15px,-30px)}84%{-webkit-transform:translate(-15px,-30px);transform:translate(-15px,-30px)}to{-webkit-transform:translate(0);transform:translate(0)}}@keyframes ball-zig-deflect{17%{-webkit-transform:translate(-15px,-30px);transform:translate(-15px,-30px)}34%{-webkit-transform:translate(15px,-30px);transform:translate(15px,-30px)}50%{-webkit-transform:translate(0);transform:translate(0)}67%{-webkit-transform:translate(15px,-30px);transform:translate(15px,-30px)}84%{-webkit-transform:translate(-15px,-30px);transform:translate(-15px,-30px)}to{-webkit-transform:translate(0);transform:translate(0)}}@-webkit-keyframes ball-zag-deflect{17%{-webkit-transform:translate(15px,30px);transform:translate(15px,30px)}34%{-webkit-transform:translate(-15px,30px);transform:translate(-15px,30px)}50%{-webkit-transform:translate(0);transform:translate(0)}67%{-webkit-transform:translate(-15px,30px);transform:translate(-15px,30px)}84%{-webkit-transform:translate(15px,30px);transform:translate(15px,30px)}to{-webkit-transform:translate(0);transform:translate(0)}}@keyframes ball-zag-deflect{17%{-webkit-transform:translate(15px,30px);transform:translate(15px,30px)}34%{-webkit-transform:translate(-15px,30px);transform:translate(-15px,30px)}50%{-webkit-transform:translate(0);transform:translate(0)}67%{-webkit-transform:translate(-15px,30px);transform:translate(-15px,30px)}84%{-webkit-transform:translate(15px,30px);transform:translate(15px,30px)}to{-webkit-transform:translate(0);transform:translate(0)}}.ball-zig-zag-deflect{position:relative;-webkit-transform:translate(-15px,-15px);-ms-transform:translate(-15px,-15px);transform:translate(-15px,-15px)}.ball-zig-zag-deflect>div{background-color:#fff;width:15px;height:15px;border-radius:100%;-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;margin:2px 2px 2px 15px;top:4px;left:-7px}.ball-zig-zag-deflect>div:first-child{-webkit-animation:ball-zig-deflect 1.5s linear 0s infinite;animation:ball-zig-deflect 1.5s linear 0s infinite}.ball-zig-zag-deflect>div:last-child{-webkit-animation:ball-zag-deflect 1.5s linear 0s infinite;animation:ball-zag-deflect 1.5s linear 0s infinite}@-webkit-keyframes line-scale{0%{-webkit-transform:scaley(1);transform:scaley(1)}50%{-webkit-transform:scaley(.4);transform:scaley(.4)}to{-webkit-transform:scaley(1);transform:scaley(1)}}@keyframes line-scale{0%{-webkit-transform:scaley(1);transform:scaley(1)}50%{-webkit-transform:scaley(.4);transform:scaley(.4)}to{-webkit-transform:scaley(1);transform:scaley(1)}}.line-scale>div:first-child{-webkit-animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) -.4s infinite;animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) -.4s infinite}.line-scale>div:nth-child(2){-webkit-animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) -.3s infinite;animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) -.3s infinite}.line-scale>div:nth-child(3){-webkit-animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) -.2s infinite;animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) -.2s infinite}.line-scale>div:nth-child(4){-webkit-animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) -.1s infinite;animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) -.1s infinite}.line-scale>div:nth-child(5){-webkit-animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) 0s infinite;animation:line-scale 1s cubic-bezier(.2,.68,.18,1.08) 0s infinite}.line-scale>div{background-color:#fff;width:4px;height:35px;border-radius:2px;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block}@-webkit-keyframes line-scale-party{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(.5);transform:scale(.5)}to{-webkit-transform:scale(1);transform:scale(1)}}@keyframes line-scale-party{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(.5);transform:scale(.5)}to{-webkit-transform:scale(1);transform:scale(1)}}.line-scale-party>div:first-child{-webkit-animation-delay:-.09s;animation-delay:-.09s;-webkit-animation-duration:.83s;animation-duration:.83s}.line-scale-party>div:nth-child(2){-webkit-animation-delay:.33s;animation-delay:.33s;-webkit-animation-duration:.64s;animation-duration:.64s}.line-scale-party>div:nth-child(3){-webkit-animation-delay:.32s;animation-delay:.32s;-webkit-animation-duration:.39s;animation-duration:.39s}.line-scale-party>div:nth-child(4){-webkit-animation-delay:.47s;animation-delay:.47s;-webkit-animation-duration:.52s;animation-duration:.52s}.line-scale-party>div{background-color:#fff;width:4px;height:35px;border-radius:2px;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;-webkit-animation-name:line-scale-party;animation-name:line-scale-party;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-delay:0;animation-delay:0}@-webkit-keyframes line-scale-pulse-out{0%{-webkit-transform:scaley(1);transform:scaley(1)}50%{-webkit-transform:scaley(.4);transform:scaley(.4)}to{-webkit-transform:scaley(1);transform:scaley(1)}}@keyframes line-scale-pulse-out{0%{-webkit-transform:scaley(1);transform:scaley(1)}50%{-webkit-transform:scaley(.4);transform:scaley(.4)}to{-webkit-transform:scaley(1);transform:scaley(1)}}.line-scale-pulse-out>div{background-color:#fff;width:4px;height:35px;border-radius:2px;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;-webkit-animation:line-scale-pulse-out .9s cubic-bezier(.85,.25,.37,.85) -.6s infinite;animation:line-scale-pulse-out .9s cubic-bezier(.85,.25,.37,.85) -.6s infinite}.line-scale-pulse-out>div:nth-child(2),.line-scale-pulse-out>div:nth-child(4){-webkit-animation-delay:-.4s!important;animation-delay:-.4s!important}.line-scale-pulse-out>div:first-child,.line-scale-pulse-out>div:nth-child(5){-webkit-animation-delay:-.2s!important;animation-delay:-.2s!important}@-webkit-keyframes line-scale-pulse-out-rapid{0%{-webkit-transform:scaley(1);transform:scaley(1)}80%{-webkit-transform:scaley(.3);transform:scaley(.3)}90%{-webkit-transform:scaley(1);transform:scaley(1)}}@keyframes line-scale-pulse-out-rapid{0%{-webkit-transform:scaley(1);transform:scaley(1)}80%{-webkit-transform:scaley(.3);transform:scaley(.3)}90%{-webkit-transform:scaley(1);transform:scaley(1)}}.line-scale-pulse-out-rapid>div{background-color:#fff;width:4px;height:35px;border-radius:2px;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;display:inline-block;-webkit-animation:line-scale-pulse-out-rapid .9s cubic-bezier(.11,.49,.38,.78) -.5s infinite;animation:line-scale-pulse-out-rapid .9s cubic-bezier(.11,.49,.38,.78) -.5s infinite}.line-scale-pulse-out-rapid>div:nth-child(2),.line-scale-pulse-out-rapid>div:nth-child(4){-webkit-animation-delay:-.25s!important;animation-delay:-.25s!important}.line-scale-pulse-out-rapid>div:first-child,.line-scale-pulse-out-rapid>div:nth-child(5){-webkit-animation-delay:0s!important;animation-delay:0s!important}@-webkit-keyframes line-spin-fade-loader{50%{opacity:.3}to{opacity:1}}@keyframes line-spin-fade-loader{50%{opacity:.3}to{opacity:1}}.line-spin-fade-loader{position:relative;top:-10px;left:-4px}.line-spin-fade-loader>div:first-child{top:20px;left:0;-webkit-animation:line-spin-fade-loader 1.2s ease-in-out -.84s infinite;animation:line-spin-fade-loader 1.2s ease-in-out -.84s infinite}.line-spin-fade-loader>div:nth-child(2){top:13.63636px;left:13.63636px;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-animation:line-spin-fade-loader 1.2s ease-in-out -.72s infinite;animation:line-spin-fade-loader 1.2s ease-in-out -.72s infinite}.line-spin-fade-loader>div:nth-child(3){top:0;left:20px;-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-animation:line-spin-fade-loader 1.2s ease-in-out -.6s infinite;animation:line-spin-fade-loader 1.2s ease-in-out -.6s infinite}.line-spin-fade-loader>div:nth-child(4){top:-13.63636px;left:13.63636px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg);-webkit-animation:line-spin-fade-loader 1.2s ease-in-out -.48s infinite;animation:line-spin-fade-loader 1.2s ease-in-out -.48s infinite}.line-spin-fade-loader>div:nth-child(5){top:-20px;left:0;-webkit-animation:line-spin-fade-loader 1.2s ease-in-out -.36s infinite;animation:line-spin-fade-loader 1.2s ease-in-out -.36s infinite}.line-spin-fade-loader>div:nth-child(6){top:-13.63636px;left:-13.63636px;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-animation:line-spin-fade-loader 1.2s ease-in-out -.24s infinite;animation:line-spin-fade-loader 1.2s ease-in-out -.24s infinite}.line-spin-fade-loader>div:nth-child(7){top:0;left:-20px;-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-animation:line-spin-fade-loader 1.2s ease-in-out -.12s infinite;animation:line-spin-fade-loader 1.2s ease-in-out -.12s infinite}.line-spin-fade-loader>div:nth-child(8){top:13.63636px;left:-13.63636px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg);-webkit-animation:line-spin-fade-loader 1.2s ease-in-out 0s infinite;animation:line-spin-fade-loader 1.2s ease-in-out 0s infinite}.line-spin-fade-loader>div{background-color:#fff;width:4px;height:35px;border-radius:2px;margin:2px;-webkit-animation-fill-mode:both;animation-fill-mode:both;position:absolute;width:5px;height:15px}@-webkit-keyframes triangle-skew-spin{25%{-webkit-transform:perspective(100px) rotateX(180deg) rotateY(0);transform:perspective(100px) rotateX(180deg) rotateY(0)}50%{-webkit-transform:perspective(100px) rotateX(180deg) rotateY(180deg);transform:perspective(100px) rotateX(180deg) rotateY(180deg)}75%{-webkit-transform:perspective(100px) rotateX(0) rotateY(180deg);transform:perspective(100px) rotateX(0) rotateY(180deg)}to{-webkit-transform:perspective(100px) rotateX(0) rotateY(0);transform:perspective(100px) rotateX(0) rotateY(0)}}@keyframes triangle-skew-spin{25%{-webkit-transform:perspective(100px) rotateX(180deg) rotateY(0);transform:perspective(100px) rotateX(180deg) rotateY(0)}50%{-webkit-transform:perspective(100px) rotateX(180deg) rotateY(180deg);transform:perspective(100px) rotateX(180deg) rotateY(180deg)}75%{-webkit-transform:perspective(100px) rotateX(0) rotateY(180deg);transform:perspective(100px) rotateX(0) rotateY(180deg)}to{-webkit-transform:perspective(100px) rotateX(0) rotateY(0);transform:perspective(100px) rotateX(0) rotateY(0)}}.triangle-skew-spin>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;width:0;height:0;border-left:20px solid transparent;border-right:20px solid transparent;border-bottom:20px solid #fff;-webkit-animation:triangle-skew-spin 3s cubic-bezier(.09,.57,.49,.9) 0s infinite;animation:triangle-skew-spin 3s cubic-bezier(.09,.57,.49,.9) 0s infinite}@-webkit-keyframes square-spin{25%{-webkit-transform:perspective(100px) rotateX(180deg) rotateY(0);transform:perspective(100px) rotateX(180deg) rotateY(0)}50%{-webkit-transform:perspective(100px) rotateX(180deg) rotateY(180deg);transform:perspective(100px) rotateX(180deg) rotateY(180deg)}75%{-webkit-transform:perspective(100px) rotateX(0) rotateY(180deg);transform:perspective(100px) rotateX(0) rotateY(180deg)}to{-webkit-transform:perspective(100px) rotateX(0) rotateY(0);transform:perspective(100px) rotateX(0) rotateY(0)}}@keyframes square-spin{25%{-webkit-transform:perspective(100px) rotateX(180deg) rotateY(0);transform:perspective(100px) rotateX(180deg) rotateY(0)}50%{-webkit-transform:perspective(100px) rotateX(180deg) rotateY(180deg);transform:perspective(100px) rotateX(180deg) rotateY(180deg)}75%{-webkit-transform:perspective(100px) rotateX(0) rotateY(180deg);transform:perspective(100px) rotateX(0) rotateY(180deg)}to{-webkit-transform:perspective(100px) rotateX(0) rotateY(0);transform:perspective(100px) rotateX(0) rotateY(0)}}.square-spin>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;width:50px;height:50px;background:#fff;border:1px solid red;-webkit-animation:square-spin 3s cubic-bezier(.09,.57,.49,.9) 0s infinite;animation:square-spin 3s cubic-bezier(.09,.57,.49,.9) 0s infinite}@-webkit-keyframes rotate_pacman_half_up{0%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}50%{-webkit-transform:rotate(1turn);transform:rotate(1turn)}to{-webkit-transform:rotate(270deg);transform:rotate(270deg)}}@keyframes rotate_pacman_half_up{0%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}50%{-webkit-transform:rotate(1turn);transform:rotate(1turn)}to{-webkit-transform:rotate(270deg);transform:rotate(270deg)}}@-webkit-keyframes rotate_pacman_half_down{0%{-webkit-transform:rotate(90deg);transform:rotate(90deg)}50%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(90deg);transform:rotate(90deg)}}@keyframes rotate_pacman_half_down{0%{-webkit-transform:rotate(90deg);transform:rotate(90deg)}50%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(90deg);transform:rotate(90deg)}}@-webkit-keyframes pacman-balls{75%{opacity:.7}to{-webkit-transform:translate(-100px,-6.25px);transform:translate(-100px,-6.25px)}}@keyframes pacman-balls{75%{opacity:.7}to{-webkit-transform:translate(-100px,-6.25px);transform:translate(-100px,-6.25px)}}.pacman{position:relative}.pacman>div:nth-child(2){-webkit-animation:pacman-balls 1s linear -.99s infinite;animation:pacman-balls 1s linear -.99s infinite}.pacman>div:nth-child(3){-webkit-animation:pacman-balls 1s linear -.66s infinite;animation:pacman-balls 1s linear -.66s infinite}.pacman>div:nth-child(4){-webkit-animation:pacman-balls 1s linear -.33s infinite;animation:pacman-balls 1s linear -.33s infinite}.pacman>div:nth-child(5){-webkit-animation:pacman-balls 1s linear 0s infinite;animation:pacman-balls 1s linear 0s infinite}.pacman>div:first-of-type{-webkit-animation:rotate_pacman_half_up .5s 0s infinite;animation:rotate_pacman_half_up .5s 0s infinite}.pacman>div:first-of-type,.pacman>div:nth-child(2){width:0;height:0;border:25px solid #fff;border-right-color:transparent;border-radius:25px;position:relative;left:-30px}.pacman>div:nth-child(2){-webkit-animation:rotate_pacman_half_down .5s 0s infinite;animation:rotate_pacman_half_down .5s 0s infinite;margin-top:-50px}.pacman>div:nth-child(3),.pacman>div:nth-child(4),.pacman>div:nth-child(5),.pacman>div:nth-child(6){background-color:#fff;width:15px;height:15px;border-radius:100%;margin:2px;width:10px;height:10px;position:absolute;-webkit-transform:translateY(-6.25px);-ms-transform:translateY(-6.25px);transform:translateY(-6.25px);top:25px;left:70px}@-webkit-keyframes cube-transition{25%{-webkit-transform:translateX(50px) scale(.5) rotate(-90deg);transform:translateX(50px) scale(.5) rotate(-90deg)}50%{-webkit-transform:translate(50px,50px) rotate(-180deg);transform:translate(50px,50px) rotate(-180deg)}75%{-webkit-transform:translateY(50px) scale(.5) rotate(-270deg);transform:translateY(50px) scale(.5) rotate(-270deg)}to{-webkit-transform:rotate(-1turn);transform:rotate(-1turn)}}@keyframes cube-transition{25%{-webkit-transform:translateX(50px) scale(.5) rotate(-90deg);transform:translateX(50px) scale(.5) rotate(-90deg)}50%{-webkit-transform:translate(50px,50px) rotate(-180deg);transform:translate(50px,50px) rotate(-180deg)}75%{-webkit-transform:translateY(50px) scale(.5) rotate(-270deg);transform:translateY(50px) scale(.5) rotate(-270deg)}to{-webkit-transform:rotate(-1turn);transform:rotate(-1turn)}}.cube-transition{position:relative;-webkit-transform:translate(-25px,-25px);-ms-transform:translate(-25px,-25px);transform:translate(-25px,-25px)}.cube-transition>div{-webkit-animation-fill-mode:both;animation-fill-mode:both;width:10px;height:10px;position:absolute;top:-5px;left:-5px;background-color:#fff;-webkit-animation:cube-transition 1.6s ease-in-out 0s infinite;animation:cube-transition 1.6s ease-in-out 0s infinite}.cube-transition>div:last-child{-webkit-animation-delay:-.8s;animation-delay:-.8s}@-webkit-keyframes spin-rotate{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes spin-rotate{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.semi-circle-spin{position:relative;width:35px;height:35px;overflow:hidden}.semi-circle-spin>div{position:absolute;border-width:0;border-radius:100%;-webkit-animation:spin-rotate .6s linear 0s infinite;animation:spin-rotate .6s linear 0s infinite;background-image:-webkit-linear-gradient(transparent,transparent 70%,#fff 0,#fff);background-image:linear-gradient(transparent,transparent 70%,#fff 0,#fff);width:100%;height:100%}@-webkit-keyframes bar-progress{0%{-webkit-transform:scaleY(20%);transform:scaleY(20%);opacity:1}25%{-webkit-transform:translateX(6%) scaleY(10%);transform:translateX(6%) scaleY(10%);opacity:.7}50%{-webkit-transform:translateX(20%) scaleY(20%);transform:translateX(20%) scaleY(20%);opacity:1}75%{-webkit-transform:translateX(6%) scaleY(10%);transform:translateX(6%) scaleY(10%);opacity:.7}to{-webkit-transform:scaleY(20%);transform:scaleY(20%);opacity:1}}@keyframes bar-progress{0%{-webkit-transform:scaleY(20%);transform:scaleY(20%);opacity:1}25%{-webkit-transform:translateX(6%) scaleY(10%);transform:translateX(6%) scaleY(10%);opacity:.7}50%{-webkit-transform:translateX(20%) scaleY(20%);transform:translateX(20%) scaleY(20%);opacity:1}75%{-webkit-transform:translateX(6%) scaleY(10%);transform:translateX(6%) scaleY(10%);opacity:.7}to{-webkit-transform:scaleY(20%);transform:scaleY(20%);opacity:1}}.bar-progress{width:30%;height:12px}.bar-progress>div{position:relative;width:20%;height:12px;border-radius:10px;background-color:#fff;-webkit-animation:bar-progress 3s cubic-bezier(.57,.1,.44,.93) infinite;animation:bar-progress 3s cubic-bezier(.57,.1,.44,.93) infinite;opacity:1}@-webkit-keyframes bar-swing{0%{left:0}50%{left:70%}to{left:0}}@keyframes bar-swing{0%{left:0}50%{left:70%}to{left:0}}.bar-swing,.bar-swing>div{width:30%;height:8px}.bar-swing>div{position:relative;border-radius:10px;background-color:#fff;-webkit-animation:bar-swing 1.5s infinite;animation:bar-swing 1.5s infinite}@-webkit-keyframes bar-swing-container{0%{left:0;-webkit-transform:translateX(0);transform:translateX(0)}50%{left:70%;-webkit-transform:translateX(-4px);transform:translateX(-4px)}to{left:0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes bar-swing-container{0%{left:0;-webkit-transform:translateX(0);transform:translateX(0)}50%{left:70%;-webkit-transform:translateX(-4px);transform:translateX(-4px)}to{left:0;-webkit-transform:translateX(0);transform:translateX(0)}}.bar-swing-container{width:20%;height:8px;position:relative}.bar-swing-container div:first-child{position:absolute;width:100%;background-color:hsla(0,0%,100%,.2);height:12px;border-radius:10px}.bar-swing-container div:nth-child(2){position:absolute;width:30%;height:8px;border-radius:10px;background-color:#fff;-webkit-animation:bar-swing-container 2s cubic-bezier(.91,.35,.12,.6) infinite;animation:bar-swing-container 2s cubic-bezier(.91,.35,.12,.6) infinite;margin:2px 2px 0}.sk-spinner{color:#333}.sk-spinner>div{background-color:currentColor}.ball-scale-ripple-multiple>div,.ball-scale-ripple>div,.ball-triangle-path>div{background-color:initial;border-color:currentColor}.ball-clip-rotate>div{background-color:initial;border-top-color:currentColor;border-right-color:currentColor;border-left-color:currentColor}.ball-clip-rotate-pulse>div:first-child{background-color:currentColor}.ball-clip-rotate-pulse>div:last-child{background-color:initial;border-top-color:currentColor;border-bottom-color:currentColor}.ball-clip-rotate-multiple>div:first-child{background-color:initial;border-right-color:currentColor;border-left-color:currentColor}.ball-clip-rotate-multiple>div:last-child{border-top-color:currentColor}.ball-clip-rotate-multiple>div:last-child,.pacman>div:first-child,.pacman>div:nth-child(2),.triangle-skew-spin>div{background-color:initial;border-bottom-color:currentColor}.pacman>div:first-child,.pacman>div:nth-child(2){border-top-color:currentColor;border-left-color:currentColor}.pacman>div:nth-child(3),.pacman>div:nth-child(4),.pacman>div:nth-child(5){background-color:currentColor}@-webkit-keyframes sk-fade-in{0%{opacity:0}50%{opacity:0}to{opacity:1}}@-moz-keyframes sk-fade-in{0%{opacity:0}50%{opacity:0}to{opacity:1}}@-ms-keyframes sk-fade-in{0%{opacity:0}50%{opacity:0}to{opacity:1}}@keyframes sk-fade-in{0%{opacity:0}50%{opacity:0}to{opacity:1}}.sk-fade-in{-webkit-animation:sk-fade-in 2s;-moz-animation:sk-fade-in 2s;-o-animation:sk-fade-in 2s;-ms-animation:sk-fade-in 2s;animation:sk-fade-in 2s}.sk-fade-in-half-second{-webkit-animation:sk-fade-in 1s;-moz-animation:sk-fade-in 1s;-o-animation:sk-fade-in 1s;-ms-animation:sk-fade-in 1s;animation:sk-fade-in 1s}.sk-fade-in-quarter-second{-webkit-animation:sk-fade-in .5s;-moz-animation:sk-fade-in .5s;-o-animation:sk-fade-in .5s;-ms-animation:sk-fade-in .5s;animation:sk-fade-in .5s}.sk-chasing-dots{width:27px;height:27px;position:relative;-webkit-animation:sk-rotate 2s linear infinite;animation:sk-rotate 2s linear infinite}.sk-chasing-dots>div{width:60%;height:60%;display:inline-block;position:absolute;top:0;background-color:currentColor;border-radius:100%;-webkit-animation:sk-bounce 2s ease-in-out infinite;animation:sk-bounce 2s ease-in-out infinite}.sk-chasing-dots>div:last-child{top:auto;bottom:0;-webkit-animation-delay:-1s;animation-delay:-1s}@-webkit-keyframes sk-rotate{to{-webkit-transform:rotate(1turn)}}@keyframes sk-rotate{to{transform:rotate(1turn);-webkit-transform:rotate(1turn)}}@-webkit-keyframes sk-bounce{0%,to{-webkit-transform:scale(0)}50%{-webkit-transform:scale(1)}}@keyframes sk-bounce{0%,to{transform:scale(0);-webkit-transform:scale(0)}50%{transform:scale(1);-webkit-transform:scale(1)}}.sk-circle{width:22px;height:22px;position:relative}.sk-circle>div{background-color:initial;width:100%;height:100%;position:absolute;left:0;top:0}.sk-circle>div:before{content:"";display:block;margin:0 auto;width:20%;height:20%;background-color:currentColor;border-radius:100%;-webkit-animation:sk-bouncedelay 1.2s ease-in-out infinite;animation:sk-bouncedelay 1.2s ease-in-out infinite;-webkit-animation-fill-mode:both;animation-fill-mode:both}.sk-circle>div:nth-child(2){-webkit-transform:rotate(30deg);transform:rotate(30deg)}.sk-circle>div:nth-child(3){-webkit-transform:rotate(60deg);transform:rotate(60deg)}.sk-circle>div:nth-child(4){-webkit-transform:rotate(90deg);transform:rotate(90deg)}.sk-circle>div:nth-child(5){-webkit-transform:rotate(120deg);transform:rotate(120deg)}.sk-circle>div:nth-child(6){-webkit-transform:rotate(150deg);transform:rotate(150deg)}.sk-circle>div:nth-child(7){-webkit-transform:rotate(180deg);transform:rotate(180deg)}.sk-circle>div:nth-child(8){-webkit-transform:rotate(210deg);transform:rotate(210deg)}.sk-circle>div:nth-child(9){-webkit-transform:rotate(240deg);transform:rotate(240deg)}.sk-circle>div:nth-child(10){-webkit-transform:rotate(270deg);transform:rotate(270deg)}.sk-circle>div:nth-child(11){-webkit-transform:rotate(300deg);transform:rotate(300deg)}.sk-circle>div:nth-child(12){-webkit-transform:rotate(330deg);transform:rotate(330deg)}.sk-circle>div:nth-child(2):before{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.sk-circle>div:nth-child(3):before{-webkit-animation-delay:-1s;animation-delay:-1s}.sk-circle>div:nth-child(4):before{-webkit-animation-delay:-.9s;animation-delay:-.9s}.sk-circle>div:nth-child(5):before{-webkit-animation-delay:-.8s;animation-delay:-.8s}.sk-circle>div:nth-child(6):before{-webkit-animation-delay:-.7s;animation-delay:-.7s}.sk-circle>div:nth-child(7):before{-webkit-animation-delay:-.6s;animation-delay:-.6s}.sk-circle>div:nth-child(8):before{-webkit-animation-delay:-.5s;animation-delay:-.5s}.sk-circle>div:nth-child(9):before{-webkit-animation-delay:-.4s;animation-delay:-.4s}.sk-circle>div:nth-child(10):before{-webkit-animation-delay:-.3s;animation-delay:-.3s}.sk-circle>div:nth-child(11):before{-webkit-animation-delay:-.2s;animation-delay:-.2s}.sk-circle>div:nth-child(12):before{-webkit-animation-delay:-.1s;animation-delay:-.1s}@-webkit-keyframes sk-bouncedelay{0%,80%,to{-webkit-transform:scale(0)}40%{-webkit-transform:scale(1)}}@keyframes sk-bouncedelay{0%,80%,to{-webkit-transform:scale(0);transform:scale(0)}40%{-webkit-transform:scale(1);transform:scale(1)}}.sk-cube-grid{width:27px;height:27px}.sk-cube-grid>div{width:33%;height:33%;background-color:currentColor;float:left;-webkit-animation:sk-scaleDelay 1.3s ease-in-out infinite;animation:sk-scaleDelay 1.3s ease-in-out infinite}.sk-cube-grid>div:first-child{-webkit-animation-delay:.2s;animation-delay:.2s}.sk-cube-grid>div:nth-child(2){-webkit-animation-delay:.3s;animation-delay:.3s}.sk-cube-grid>div:nth-child(3){-webkit-animation-delay:.4s;animation-delay:.4s}.sk-cube-grid>div:nth-child(4){-webkit-animation-delay:.1s;animation-delay:.1s}.sk-cube-grid>div:nth-child(5){-webkit-animation-delay:.2s;animation-delay:.2s}.sk-cube-grid>div:nth-child(6){-webkit-animation-delay:.3s;animation-delay:.3s}.sk-cube-grid>div:nth-child(7){-webkit-animation-delay:0s;animation-delay:0s}.sk-cube-grid>div:nth-child(8){-webkit-animation-delay:.1s;animation-delay:.1s}.sk-cube-grid>div:nth-child(9){-webkit-animation-delay:.2s;animation-delay:.2s}@-webkit-keyframes sk-scaleDelay{0%,70%,to{-webkit-transform:scaleX(1)}35%{-webkit-transform:scale3D(0,0,1)}}@keyframes sk-scaleDelay{0%,70%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}35%{-webkit-transform:scaleX(1);transform:scale3D(0,0,1)}}.sk-double-bounce{width:27px;height:27px;position:relative}.sk-double-bounce>div{width:100%;height:100%;border-radius:50%;background-color:currentColor;opacity:.6;position:absolute;top:0;left:0;-webkit-animation:sk-bounce 2s ease-in-out infinite;animation:sk-bounce 2s ease-in-out infinite}.sk-double-bounce>div:last-child{-webkit-animation-delay:-1s;animation-delay:-1s}@-webkit-keyframes sk-bounce{0%,to{-webkit-transform:scale(0)}50%{-webkit-transform:scale(1)}}@keyframes sk-bounce{0%,to{transform:scale(0);-webkit-transform:scale(0)}50%{transform:scale(1);-webkit-transform:scale(1)}}.sk-folding-cube{width:27px;height:27px;position:relative;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.sk-folding-cube>div{background-color:initial;float:left;width:50%;height:50%;position:relative;-webkit-transform:scale(1.1);-ms-transform:scale(1.1);transform:scale(1.1)}.sk-folding-cube>div:before{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background-color:currentColor;-webkit-animation:sk-foldCubeAngle 2.4s linear infinite both;animation:sk-foldCubeAngle 2.4s linear infinite both;-webkit-transform-origin:100% 100%;-ms-transform-origin:100% 100%;transform-origin:100% 100%}.sk-folding-cube>div:nth-child(2){-webkit-transform:scale(1.1) rotate(90deg);transform:scale(1.1) rotate(90deg)}.sk-folding-cube>div:nth-child(4){-webkit-transform:scale(1.1) rotate(180deg);transform:scale(1.1) rotate(180deg)}.sk-folding-cube>div:nth-child(3){-webkit-transform:scale(1.1) rotate(270deg);transform:scale(1.1) rotate(270deg)}.sk-folding-cube>div:nth-child(2):before{-webkit-animation-delay:.3s;animation-delay:.3s}.sk-folding-cube>div:nth-child(4):before{-webkit-animation-delay:.6s;animation-delay:.6s}.sk-folding-cube>div:nth-child(3):before{-webkit-animation-delay:.9s;animation-delay:.9s}@-webkit-keyframes sk-foldCubeAngle{0%,10%{-webkit-transform:perspective(140px) rotateX(-180deg);transform:perspective(140px) rotateX(-180deg);opacity:0}25%,75%{-webkit-transform:perspective(140px) rotateX(0deg);transform:perspective(140px) rotateX(0deg);opacity:1}90%,to{-webkit-transform:perspective(140px) rotateY(180deg);transform:perspective(140px) rotateY(180deg);opacity:0}}@keyframes sk-foldCubeAngle{0%,10%{-webkit-transform:perspective(140px) rotateX(-180deg);transform:perspective(140px) rotateX(-180deg);opacity:0}25%,75%{-webkit-transform:perspective(140px) rotateX(0deg);transform:perspective(140px) rotateX(0deg);opacity:1}90%,to{-webkit-transform:perspective(140px) rotateY(180deg);transform:perspective(140px) rotateY(180deg);opacity:0}}.sk-pulse>div{width:27px;height:27px;background-color:currentColor;border-radius:100%;-webkit-animation:sk-scaleout 1s ease-in-out infinite;animation:sk-scaleout 1s ease-in-out infinite}@-webkit-keyframes sk-scaleout{0%{-webkit-transform:scale(0)}to{-webkit-transform:scale(1);opacity:0}}@keyframes sk-scaleout{0%{transform:scale(0);-webkit-transform:scale(0)}to{transform:scale(1);-webkit-transform:scale(1);opacity:0}}.sk-rotating-plane>div{width:27px;height:27px;background-color:currentColor;-webkit-animation:sk-rotateplane 1.2s ease-in-out infinite;animation:sk-rotateplane 1.2s ease-in-out infinite}@-webkit-keyframes sk-rotateplane{0%{-webkit-transform:perspective(120px)}50%{-webkit-transform:perspective(120px) rotateY(180deg)}to{-webkit-transform:perspective(120px) rotateY(180deg) rotateX(180deg)}}@keyframes sk-rotateplane{0%{transform:perspective(120px) rotateX(0deg) rotateY(0deg);-webkit-transform:perspective(120px) rotateX(0deg) rotateY(0deg)}50%{transform:perspective(120px) rotateX(-180.1deg) rotateY(0deg);-webkit-transform:perspective(120px) rotateX(-180.1deg) rotateY(0deg)}to{transform:perspective(120px) rotateX(-180deg) rotateY(-179.9deg);-webkit-transform:perspective(120px) rotateX(-180deg) rotateY(-179.9deg)}}.sk-three-bounce{height:18px}.sk-three-bounce>div{width:18px;height:18px;background-color:currentColor;border-radius:100%;display:inline-block;-webkit-animation:sk-bouncedelay 1.4s ease-in-out infinite;animation:sk-bouncedelay 1.4s ease-in-out infinite;-webkit-animation-fill-mode:both;animation-fill-mode:both}.sk-three-bounce>div:first-child{-webkit-animation-delay:-.32s;animation-delay:-.32s}.sk-three-bounce>div:nth-child(2){-webkit-animation-delay:-.16s;animation-delay:-.16s}@-webkit-keyframes sk-bouncedelay{0%,80%,to{-webkit-transform:scale(0)}40%{-webkit-transform:scale(1)}}@keyframes sk-bouncedelay{0%,80%,to{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}.sk-wandering-cubes{width:52px;height:52px;position:relative}.sk-wandering-cubes>div{background-color:currentColor;width:10px;height:10px;position:absolute;top:0;left:0;-webkit-animation:sk-cubemove 1.8s ease-in-out infinite;animation:sk-cubemove 1.8s ease-in-out infinite}.sk-wandering-cubes>div:last-child{-webkit-animation-delay:-.9s;animation-delay:-.9s}@-webkit-keyframes sk-cubemove{25%{-webkit-transform:translateX(42px) rotate(-90deg) scale(.5)}50%{-webkit-transform:translateX(42px) translateY(42px) rotate(-180deg)}75%{-webkit-transform:translateX(0) translateY(42px) rotate(-270deg) scale(.5)}to{-webkit-transform:rotate(-1turn)}}@keyframes sk-cubemove{25%{transform:translateX(42px) rotate(-90deg) scale(.5);-webkit-transform:translateX(42px) rotate(-90deg) scale(.5)}50%{transform:translateX(42px) translateY(42px) rotate(-179deg);-webkit-transform:translateX(42px) translateY(42px) rotate(-179deg)}50.1%{transform:translateX(42px) translateY(42px) rotate(-180deg);-webkit-transform:translateX(42px) translateY(42px) rotate(-180deg)}75%{transform:translateX(0) translateY(42px) rotate(-270deg) scale(.5);-webkit-transform:translateX(0) translateY(42px) rotate(-270deg) scale(.5)}to{transform:rotate(-1turn);-webkit-transform:rotate(-1turn)}}.sk-wave{width:30px;height:27px}.sk-wave>div{background-color:currentColor;height:100%;width:6px;display:inline-block;-webkit-animation:sk-stretchdelay 1.2s ease-in-out infinite;animation:sk-stretchdelay 1.2s ease-in-out infinite}.sk-wave>div:nth-child(2){-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.sk-wave>div:nth-child(3){-webkit-animation-delay:-1s;animation-delay:-1s}.sk-wave>div:nth-child(4){-webkit-animation-delay:-.9s;animation-delay:-.9s}.sk-wave>div:nth-child(5){-webkit-animation-delay:-.8s;animation-delay:-.8s}@-webkit-keyframes sk-stretchdelay{0%,40%,to{-webkit-transform:scaleY(.4)}20%{-webkit-transform:scaleY(1)}}@keyframes sk-stretchdelay{0%,40%,to{transform:scaleY(.4);-webkit-transform:scaleY(.4)}20%{transform:scaleY(1);-webkit-transform:scaleY(1)}}.sk-wordpress>div{width:27px;height:27px;background-color:currentColor;display:inline-block;border-radius:27px;position:relative;-webkit-animation:sk-inner-circle 1s linear infinite;animation:sk-inner-circle 1s linear infinite}.sk-wordpress>div:after{content:"";display:block;background-color:#fff;width:8px;height:8px;position:absolute;border-radius:8px;top:5px;left:5px}@-webkit-keyframes sk-inner-circle{0%{-webkit-transform:rotate(0)}to{-webkit-transform:rotate(1turn)}}@keyframes sk-inner-circle{0%{transform:rotate(0);-webkit-transform:rotate(0)}to{transform:rotate(1turn);-webkit-transform:rotate(1turn)}}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor-mark{background-color:rgba(20,255,20,.5)}.cm-animate-fat-cursor,.cm-fat-cursor-mark{-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;border:0;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:none;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:none}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:""}span.CodeMirror-selectedtext{background:none}.cm-s-monokai.CodeMirror{background:#272822;color:#f8f8f2}.cm-s-monokai div.CodeMirror-selected{background:#49483e}.cm-s-monokai .CodeMirror-line::selection,.cm-s-monokai .CodeMirror-line>span::selection,.cm-s-monokai .CodeMirror-line>span>span::selection{background:rgba(73,72,62,.99)}.cm-s-monokai .CodeMirror-line::-moz-selection,.cm-s-monokai .CodeMirror-line>span::-moz-selection,.cm-s-monokai .CodeMirror-line>span>span::-moz-selection{background:rgba(73,72,62,.99)}.cm-s-monokai .CodeMirror-gutters{background:#272822;border-right:0}.cm-s-monokai .CodeMirror-guttermarker{color:#fff}.cm-s-monokai .CodeMirror-guttermarker-subtle,.cm-s-monokai .CodeMirror-linenumber{color:#d0d0d0}.cm-s-monokai .CodeMirror-cursor{border-left:1px solid #f8f8f0}.cm-s-monokai span.cm-comment{color:#75715e}.cm-s-monokai span.cm-atom,.cm-s-monokai span.cm-number{color:#ae81ff}.cm-s-monokai span.cm-comment.cm-attribute{color:#97b757}.cm-s-monokai span.cm-comment.cm-def{color:#bc9262}.cm-s-monokai span.cm-comment.cm-tag{color:#bc6283}.cm-s-monokai span.cm-comment.cm-type{color:#5998a6}.cm-s-monokai span.cm-attribute,.cm-s-monokai span.cm-property{color:#a6e22e}.cm-s-monokai span.cm-keyword{color:#f92672}.cm-s-monokai span.cm-builtin{color:#66d9ef}.cm-s-monokai span.cm-string{color:#e6db74}.cm-s-monokai span.cm-variable{color:#f8f8f2}.cm-s-monokai span.cm-variable-2{color:#9effff}.cm-s-monokai span.cm-type,.cm-s-monokai span.cm-variable-3{color:#66d9ef}.cm-s-monokai span.cm-def{color:#fd971f}.cm-s-monokai span.cm-bracket{color:#f8f8f2}.cm-s-monokai span.cm-tag{color:#f92672}.cm-s-monokai span.cm-header,.cm-s-monokai span.cm-link{color:#ae81ff}.cm-s-monokai span.cm-error{background:#f92672;color:#f8f8f0}.cm-s-monokai .CodeMirror-activeline-background{background:#373831}.cm-s-monokai .CodeMirror-matchingbracket{text-decoration:underline;color:#fff!important}@charset "UTF-8";body,html{margin:0;height:100%}html.notebook,html.notebook body{overflow-y:hidden}body{margin:0;padding:0;background:#272822;font-family:Helvetica Neue,Helvetica,Arial,sans-serif}body .notebook-recipe{color:#ffc312}body .notebook-recipe:before{display:inline-block;content:"—";margin:0 10px}body .bigbutton{height:30px;color:#fff;border:none;border-radius:5px;font-size:1.1rem;font-weight:700;cursor:pointer}.home-app{padding:10px;font-size:18px;display:grid;grid-template-columns:auto 150px;grid-template-areas:"notebooklist tools"}.home-app,.home-app a{color:#fff}.home-app:visited,.home-app a:visited{color:#aaa}.home-app .list{grid-area:notebooklist}.home-app .list h2{margin-left:16px}.home-app .tools{grid-area:tools;text-align:left;padding-top:15px}.home-app .tools .btn-new{background-color:#00a8ff;text-align:right;width:113px;white-space:nowrap}.home-app .tools .btn-new.creating{text-align:center}.home-app .tools .btn-new.creating .sk-spinner{margin:0 auto}.home-app .tools .recipe-list{padding:10px}.home-app .tools .recipe-list .recipe-item{display:block;cursor:pointer;text-decoration:underline;padding-top:5px}.home-app .tools .recipe-list .recipe-item:hover{color:#ffc312}.notebook-app body{font-size:14px}.notebook-app .CodeMirror{width:100%;height:calc(100vh - 50px);font-family:Roboto Mono,Menlo,Ubuntu Mono,Monaco,Consolas,source-code-pro,monospace;line-height:1.414}.notebook-app .CodeMirror-linenumber{padding-right:20px}.notebook-app .cm-s-monokai .CodeMirror-linenumber{color:#777}.notebook-app #layout{display:grid;grid-template-rows:50px auto;grid-template-areas:"top top top" "code gutter console"}.notebook-app #top{grid-area:top;background-color:#333;padding:10px;vertical-align:middle;display:grid;grid-template-columns:100px auto auto 100px;grid-template-areas:"btn-run notebook-header console-options btn-home"}.notebook-app #top #btn-run{grid-area:btn-run}.notebook-app #top #notebook-header{grid-area:notebook-header;font-size:20px;line-height:30px;font-family:Helvetica Neue,Helvetica,Arial,sans-serif}.notebook-app #top #notebook-header .notebook-name{color:#fff;font-weight:700;cursor:text}.notebook-app #top #console-options{grid-area:console-options;text-align:right;color:#fff;line-height:30px;padding-right:30px}.notebook-app #top #btn-home{grid-area:btn-home;text-align:right}.notebook-app #top #btn-run .bigbutton.run{width:90px;background-color:#6bb7af}.notebook-app #top #btn-run .bigbutton.run.running{background-color:#f06}.notebook-app #top #btn-home .bigbutton{background-color:#9b59b6}.notebook-app #left{grid-area:code}.notebook-app #gutter{grid-area:gutter;background-color:#333;cursor:col-resize}.notebook-app #right{grid-area:console}.notebook-app #console{color:#f8f8f2;width:100%;height:calc(100vh - 50px);overflow:scroll;unicode-bidi:embed;font-family:Roboto Mono,Menlo,Ubuntu Mono,Monaco,Consolas,source-code-pro,monospace;white-space:pre;margin-bottom:20px}.notebook-app #console .stderr{color:red}.notebook-app #console .info{color:#888}.notebook-app #console .forcednl{background-color:#f06;color:#fff} 2 | /*# sourceMappingURL=/frontend.4dd03dfa.css.map */ -------------------------------------------------------------------------------- /dist/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | -------------------------------------------------------------------------------- /dist/frontend/worker.2bdd663b.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c 1 { 64 | plural = "s" 65 | } 66 | 67 | _, _ = stdInfo.Write([]byte(fmt.Sprintf("Nodebook started. %d notebook%s watched in %s\n", nbNotebooks, plural, notebookspath))) 68 | 69 | <-make(chan interface{}) 70 | } 71 | 72 | func notebookChangedHandler(useDocker bool, stdOut, stdErr, stdInfo io.Writer) func(notebook types.Notebook) { 73 | 74 | return func(notebook types.Notebook) { 75 | 76 | start := time.Now() 77 | 78 | label := fmt.Sprintf("%s (%s)", notebook.GetName(), notebook.GetRecipe().GetName()) 79 | 80 | _, _ = stdInfo.Write([]byte( 81 | fmt.Sprintf("Executing %s; interrupt with Ctrl+c\n", label), 82 | )) 83 | 84 | execHandler := notebook.GetRecipe().ExecNotebook( 85 | notebook, 86 | useDocker, 87 | stdOut, 88 | stdErr, 89 | stdInfo, 90 | nil, 91 | ) 92 | 93 | done := make(chan interface{}) 94 | go func() { 95 | execHandler.Start() 96 | done <- nil 97 | }() 98 | 99 | sigs := make(chan os.Signal, 1) 100 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 101 | defer signal.Reset(syscall.SIGINT, syscall.SIGTERM) 102 | 103 | select { 104 | case <-done: 105 | _, _ = stdInfo.Write([]byte(fmt.Sprintf( 106 | "Done; took %.1f ms\n", 107 | float64(time.Since(start).Microseconds())/1000.0, 108 | ))) 109 | case <-sigs: 110 | // Ctrl+c outputs ^C in some terminals 111 | // writing on a new line to avoid being shifted right by 2 chars 112 | _, _ = stdOut.Write([]byte("\n")) 113 | _, _ = stdInfo.Write([]byte("Interrupting...\n")) 114 | execHandler.Stop() 115 | _, _ = stdInfo.Write([]byte(fmt.Sprintf( 116 | "Interrupted %s\n", label, 117 | ))) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/core/cmdweb.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/netgusto/nodebook/src/core/shared" 11 | "github.com/netgusto/nodebook/src/core/shared/service" 12 | ) 13 | 14 | func WebRun(notebookspath string, docker bool, bindaddress string, port int) { 15 | if docker && !shared.IsDockerRunning() { 16 | fmt.Println("docker is not running on the host, but --docker requested.") 17 | os.Exit(1) 18 | } 19 | 20 | recipeRegistry, nbRegistry := baseServices(notebookspath) 21 | 22 | csrfService := service.NewCSRFService() 23 | routes := service.NewRoutes() 24 | 25 | api := makeAPI( 26 | nbRegistry, 27 | recipeRegistry, 28 | routes, 29 | csrfService, 30 | docker, 31 | ) 32 | 33 | fmt.Printf("nbk listening on %s:%d\n", bindaddress, port) 34 | srv := &http.Server{ 35 | Handler: api, 36 | Addr: fmt.Sprintf("%s:%d", bindaddress, port), 37 | ReadTimeout: 15 * time.Second, 38 | // no write timeout, we may need to stream responses for indefinite amounts of time 39 | } 40 | 41 | log.Fatal(srv.ListenAndServe()) 42 | } 43 | -------------------------------------------------------------------------------- /src/core/httphandler/apinewnotebook.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/Pallinder/sillyname-go" 8 | "github.com/netgusto/nodebook/src/core/shared/service" 9 | ) 10 | 11 | func ApiNewNotebookHandler( 12 | notebookRegistry *service.NotebookRegistry, 13 | recipeRegistry *service.RecipeRegistry, 14 | csrfService *service.CSRFService, 15 | routes *service.Routes, 16 | notebookspath string) HTTPHandler { 17 | return func(res http.ResponseWriter, req *http.Request) { 18 | 19 | decoder := json.NewDecoder(req.Body) 20 | var post struct { 21 | CSRFToken service.CSRFToken `json:"csrfToken"` 22 | RecipeKey string `json:"recipekey"` 23 | } 24 | err := decoder.Decode(&post) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | if !csrfService.IsValid(post.CSRFToken) { 30 | // TODO: log 31 | http.Error(res, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 32 | return 33 | } 34 | 35 | // find recipe 36 | recipe := recipeRegistry.GetRecipeByKey(post.RecipeKey) 37 | if recipe == nil { 38 | http.Error(res, "Recipe does not exist", http.StatusBadRequest) 39 | return 40 | } 41 | 42 | // Generate name 43 | var name string 44 | for { 45 | name, err = service.SanitizeNotebookName(sillyname.GenerateStupidName()) 46 | if err != nil { 47 | http.Error(res, "Could not generate notebook name", http.StatusInternalServerError) 48 | return 49 | } 50 | 51 | _, err = notebookRegistry.GetNotebookByName(name) 52 | if err != nil { 53 | // notebook doe not exist yet; found unused name 54 | break 55 | } 56 | } 57 | 58 | // Create notebook 59 | if err := (*recipe).InitNotebook(*recipe, notebookspath, name); err != nil { 60 | http.Error(res, "newNotebook: Could not initialize recipe for notebook", http.StatusInternalServerError) 61 | return 62 | } 63 | 64 | // update cache 65 | notebook, err := notebookRegistry.BuildNotebookDescriptor(name, *recipe) 66 | if err != nil { 67 | http.Error(res, "Could not build notebook descriptor for notebook", http.StatusInternalServerError) 68 | return 69 | } 70 | 71 | // Register notebook 72 | notebookRegistry.RegisterNotebook(notebook) 73 | 74 | payload, err := json.Marshal(extractFrontendNotebookSummary(notebook, routes)) 75 | if err != nil { 76 | http.Error(res, "Could not summarize notebook for frontend", http.StatusInternalServerError) 77 | panic(err) 78 | } 79 | 80 | res.Header().Add("Content-Type", "application/json") 81 | res.Header().Add("Cache-Control", "max-age=0") 82 | 83 | _, _ = res.Write(payload) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/core/httphandler/apinotebookexec.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "os" 8 | "path" 9 | "strings" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/joho/godotenv" 13 | "github.com/netgusto/nodebook/src/core/shared/service" 14 | "github.com/netgusto/nodebook/src/core/shared/types" 15 | 16 | pkgErrors "github.com/pkg/errors" 17 | ) 18 | 19 | var running map[string]func() = map[string]func(){} 20 | 21 | func ApiNotebookExecHandler(notebookRegistry *service.NotebookRegistry, csrfService *service.CSRFService, useDocker bool) HTTPHandler { 22 | return func(res http.ResponseWriter, req *http.Request) { 23 | 24 | decoder := json.NewDecoder(req.Body) 25 | var post struct { 26 | CSRFToken service.CSRFToken `json:"csrfToken"` 27 | } 28 | err := decoder.Decode(&post) 29 | if err != nil || !csrfService.IsValid(post.CSRFToken) { 30 | http.Error(res, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 31 | return 32 | } 33 | 34 | params := mux.Vars(req) 35 | name, found := params["name"] 36 | if !found || strings.TrimSpace(name) == "" { 37 | http.Error(res, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 38 | return 39 | } 40 | 41 | notebook, err := notebookRegistry.GetNotebookByName(name) 42 | if err != nil { 43 | http.Error(res, http.StatusText(http.StatusNotFound), http.StatusNotFound) 44 | return 45 | } 46 | 47 | res.Header().Add("Content-Type", "application/stream+json; charset=utf8") 48 | res.Header().Add("Cache-Control", "max-age=0") 49 | 50 | execHandler, err := execNotebook(notebook, useDocker, res) 51 | if err != nil { 52 | http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 53 | return 54 | } 55 | 56 | running[notebook.GetName()] = execHandler.Stop 57 | execHandler.Start() 58 | delete(running, notebook.GetName()) 59 | } 60 | } 61 | 62 | func execNotebook(notebook types.Notebook, docker bool, res io.Writer) (types.ExecHandler, error) { 63 | 64 | write := func(data string, outputChannel string) { 65 | 66 | dataJson, _ := json.Marshal(data) 67 | payload := map[string]interface{}{ 68 | "chan": outputChannel, 69 | "data": string(dataJson), 70 | } 71 | out, _ := json.Marshal(payload) 72 | _, _ = res.Write([]byte(string(out) + "\n")) 73 | if f, ok := res.(http.Flusher); ok { 74 | f.Flush() 75 | } 76 | } 77 | 78 | writeStdOut := types.NewStreamWriter(func(data string) { 79 | write(data, "stdout") 80 | }) 81 | 82 | writeStdErr := types.NewStreamWriter(func(data string) { 83 | write(data, "stderr") 84 | }) 85 | 86 | writeInfo := types.NewStreamWriter(func(data string) { 87 | write(data, "info") 88 | }) 89 | 90 | // extracting .env from notebook if defined 91 | env, err := getNotebookEnv(notebook) 92 | if err != nil { 93 | return nil, pkgErrors.Wrap(err, "execNotebook: Error while reading env for notebook") 94 | } 95 | 96 | return notebook.GetRecipe().ExecNotebook( 97 | notebook, 98 | docker, 99 | writeStdOut, 100 | writeStdErr, 101 | writeInfo, 102 | env, 103 | ), nil 104 | } 105 | 106 | func getNotebookEnv(notebook types.Notebook) (map[string]string, error) { 107 | 108 | env := map[string]string{} 109 | abspath := path.Join(notebook.GetAbsdir(), ".env") 110 | info, err := os.Stat(abspath) 111 | if err != nil || !info.Mode().IsRegular() { 112 | // no .env in the notebook, or it's not a file 113 | return env, nil 114 | } 115 | 116 | f, err := os.Open(abspath) 117 | if err != nil { 118 | return nil, pkgErrors.Wrapf(err, "getNotebookEnv: Could not open .env for read in notebook %s", notebook.GetName()) 119 | } 120 | 121 | env, err = godotenv.Parse(f) 122 | if err != nil { 123 | return nil, pkgErrors.Wrapf(err, "getNotebookEnv: Could not parse .env in notebook %s", notebook.GetName()) 124 | } 125 | 126 | return env, nil 127 | } 128 | -------------------------------------------------------------------------------- /src/core/httphandler/apinotebooksetcontent.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "strings" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/netgusto/nodebook/src/core/shared/service" 12 | "github.com/netgusto/nodebook/src/core/shared/types" 13 | pkgErrors "github.com/pkg/errors" 14 | ) 15 | 16 | func ApiNotebookSetContentHandler(notebookRegistry *service.NotebookRegistry, csrfService *service.CSRFService) HTTPHandler { 17 | return func(res http.ResponseWriter, req *http.Request) { 18 | res.Header().Add("Content-Type", "application/json; charset=utf8") 19 | res.Header().Add("Cache-Control", "max-age=0") 20 | decoder := json.NewDecoder(req.Body) 21 | var post struct { 22 | CSRFToken service.CSRFToken `json:"csrfToken"` 23 | Content string `json:"content"` 24 | } 25 | err := decoder.Decode(&post) 26 | if err != nil || !csrfService.IsValid(post.CSRFToken) { 27 | http.Error(res, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 28 | return 29 | } 30 | 31 | params := mux.Vars(req) 32 | name, found := params["name"] 33 | if !found || strings.TrimSpace(name) == "" { 34 | http.Error(res, http.StatusText(http.StatusBadRequest), http.StatusUnauthorized) 35 | return 36 | } 37 | 38 | notebook, err := notebookRegistry.GetNotebookByName(name) 39 | if err != nil { 40 | http.Error(res, http.StatusText(http.StatusNotFound), http.StatusNotFound) 41 | return 42 | } 43 | 44 | if err := updateNotebookContent(notebook, []byte(post.Content), notebookRegistry); err != nil { 45 | http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 46 | return 47 | } 48 | 49 | _, _ = res.Write([]byte("\"OK\"")) 50 | } 51 | } 52 | 53 | func updateNotebookContent(notebook types.Notebook, content []byte, notebookregistry *service.NotebookRegistry) error { 54 | 55 | mainfile := notebook.GetMainFileAbsPath() 56 | 57 | info, err := os.Stat(mainfile) 58 | if err != nil { 59 | return pkgErrors.Wrapf(err, "updateNotebookContent: could not stat %s", mainfile) 60 | } 61 | 62 | err = ioutil.WriteFile(mainfile, content, info.Mode()) 63 | if err != nil { 64 | return pkgErrors.Wrapf(err, "updateNotebookContent: could not write content to file %s", mainfile) 65 | } 66 | 67 | if _, err := notebookregistry.Refresh(notebook); err != nil { 68 | return pkgErrors.Wrapf(err, "updateNotebookContent: could not refresh descriptor for notebook %s", notebook.GetName()) 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /src/core/httphandler/apinotenookstop.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/netgusto/nodebook/src/core/shared/service" 10 | ) 11 | 12 | func ApiNotebookStopHandler(notebookRegistry *service.NotebookRegistry, csrfService *service.CSRFService) HTTPHandler { 13 | return func(res http.ResponseWriter, req *http.Request) { 14 | res.Header().Add("Content-Type", "application/json; charset=utf8") 15 | res.Header().Add("Cache-Control", "max-age=0") 16 | decoder := json.NewDecoder(req.Body) 17 | var post struct { 18 | CSRFToken service.CSRFToken `json:"csrfToken"` 19 | } 20 | err := decoder.Decode(&post) 21 | if err != nil || !csrfService.IsValid(post.CSRFToken) { 22 | http.Error(res, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 23 | return 24 | } 25 | 26 | params := mux.Vars(req) 27 | name, found := params["name"] 28 | if !found || strings.TrimSpace(name) == "" { 29 | http.Error(res, http.StatusText(http.StatusBadRequest), http.StatusUnauthorized) 30 | return 31 | } 32 | 33 | _, err = notebookRegistry.GetNotebookByName(name) 34 | if err != nil { 35 | http.Error(res, http.StatusText(http.StatusNotFound), http.StatusNotFound) 36 | return 37 | } 38 | 39 | for _, stop := range running { 40 | stop() 41 | } 42 | 43 | // empty running stop funcs 44 | running = map[string]func(){} 45 | 46 | _, _ = res.Write([]byte("\"OK\"")) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/httphandler/apirenamenotebook.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "path" 9 | "strings" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/netgusto/nodebook/src/core/shared/service" 13 | "github.com/netgusto/nodebook/src/core/shared/types" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func ApiNotebookRenameHandler(notebookRegistry *service.NotebookRegistry, csrfService *service.CSRFService, routes *service.Routes) HTTPHandler { 18 | return func(res http.ResponseWriter, req *http.Request) { 19 | decoder := json.NewDecoder(req.Body) 20 | var post struct { 21 | CSRFToken service.CSRFToken `json:"csrfToken"` 22 | NewName string `json:"newname"` 23 | } 24 | err := decoder.Decode(&post) 25 | if err != nil || !csrfService.IsValid(post.CSRFToken) { 26 | http.Error(res, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 27 | return 28 | } 29 | 30 | params := mux.Vars(req) 31 | name, found := params["name"] 32 | if !found || strings.TrimSpace(name) == "" { 33 | http.Error(res, http.StatusText(http.StatusBadRequest)+":1", http.StatusBadRequest) 34 | return 35 | } 36 | 37 | notebook, err := notebookRegistry.GetNotebookByName(name) 38 | if err != nil { 39 | http.Error(res, http.StatusText(http.StatusNotFound), http.StatusNotFound) 40 | return 41 | } 42 | 43 | sanitizedNewName, err := service.SanitizeNotebookName(post.NewName) 44 | if err != nil { 45 | http.Error(res, http.StatusText(http.StatusBadRequest)+":2", http.StatusBadRequest) 46 | return 47 | } 48 | 49 | renamedNotebook, err := renameNotebook(notebook, sanitizedNewName, notebookRegistry) 50 | if err != nil { 51 | http.Error(res, http.StatusText(http.StatusBadRequest)+":3"+err.Error(), http.StatusBadRequest) 52 | return 53 | } 54 | 55 | res.Header().Add("Content-Type", "application/stream+json; charset=utf8") 56 | res.Header().Add("Cache-Control", "max-age=0") 57 | 58 | payload, err := json.Marshal(extractFrontendNotebookSummary(renamedNotebook, routes)) 59 | if err != nil { 60 | http.Error(res, "Could not summarize notebook for frontend", http.StatusInternalServerError) 61 | return 62 | } 63 | 64 | _, _ = res.Write(payload) 65 | } 66 | } 67 | 68 | func renameNotebook(notebook types.Notebook, newname string, notebookregistry *service.NotebookRegistry) (types.Notebook, error) { 69 | 70 | newabsdir := path.Join(path.Dir(notebook.GetAbsdir()), newname) 71 | _, err := os.Stat(newabsdir) 72 | if err == nil { 73 | return nil, fmt.Errorf("Could not rename notebook %s: the new dir %s already exists", notebook.GetName(), newabsdir) 74 | } 75 | 76 | if err = os.Rename(notebook.GetAbsdir(), newabsdir); err != nil { 77 | return nil, errors.Wrapf(err, "Could not rename notebook %s to %s", notebook.GetName(), newname) 78 | } 79 | 80 | renamedNotebook, err := notebookregistry.Renamed(notebook, newname) 81 | if err != nil { 82 | return nil, errors.Wrapf(err, "Could not rename notebook %s to %s", notebook.GetName(), newname) 83 | } 84 | 85 | return renamedNotebook, nil 86 | } 87 | -------------------------------------------------------------------------------- /src/core/httphandler/crsf.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/netgusto/nodebook/src/core/shared/service" 8 | ) 9 | 10 | func CsrfHandler(csrf *service.CSRFService) HTTPHandler { 11 | return func(res http.ResponseWriter, req *http.Request) { 12 | token := csrf.NewToken() 13 | 14 | payload, err := json.Marshal(map[string]string{ 15 | "csrfToken": string(token), 16 | }) 17 | 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | res.Header().Set("Cache-Control", "max-age=0") 23 | res.Header().Set("Content-Type", "application/json; charset=utf-8") 24 | _, _ = res.Write(payload) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/core/httphandler/handlers.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/markbates/pkger" 10 | "github.com/netgusto/nodebook/src/core/shared/service" 11 | "github.com/netgusto/nodebook/src/core/shared/types" 12 | ) 13 | 14 | type HTTPHandler = func(res http.ResponseWriter, req *http.Request) 15 | 16 | func generatePageHtml(routename string, params map[string]interface{}) (string, error) { 17 | f, err := pkger.Open("/dist/frontend/index.html") 18 | if err != nil { 19 | panic(err) 20 | } 21 | content, err := ioutil.ReadAll(f) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | jsonParams, _ := json.Marshal(params) 27 | 28 | strContent := string(content) 29 | strContent = strings.Replace(strContent, "\"#route#\"", "\""+routename+"\"", -1) 30 | strContent = strings.Replace(strContent, "\"#params#\"", string(jsonParams), -1) 31 | 32 | return strContent, nil 33 | } 34 | 35 | type NotebookSummaryFrontend struct { 36 | Name string `json:"name"` 37 | Url string `json:"url"` 38 | Mtime string `json:"mtime"` 39 | Recipe RecipeSummaryFrontend `json:"recipe"` 40 | } 41 | 42 | type RecipeSummaryFrontend struct { 43 | Key string `json:"key"` 44 | Name string `json:"name"` 45 | Language string `json:"language"` 46 | Cmmode string `json:"cmmode"` 47 | } 48 | 49 | func extractFrontendNotebookSummary(notebook types.Notebook, routes *service.Routes) NotebookSummaryFrontend { 50 | return NotebookSummaryFrontend{ 51 | Name: notebook.GetName(), 52 | Url: routes.Notebook(notebook.GetName()), 53 | Mtime: notebook.GetMtime(), 54 | Recipe: extractFrontendRecipeSummary(notebook.GetRecipe()), 55 | } 56 | } 57 | 58 | func extractFrontendRecipeSummary(recipe types.Recipe) RecipeSummaryFrontend { 59 | return RecipeSummaryFrontend{ 60 | Key: recipe.GetKey(), 61 | Name: recipe.GetName(), 62 | Language: recipe.GetLanguage(), 63 | Cmmode: recipe.GetCmmode(), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/core/httphandler/homepage.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/netgusto/nodebook/src/core/shared/service" 9 | ) 10 | 11 | func HomePageHandler( 12 | notebookregistry *service.NotebookRegistry, 13 | reciperegistry *service.RecipeRegistry, 14 | routes *service.Routes, 15 | ) HTTPHandler { 16 | return func(res http.ResponseWriter, req *http.Request) { 17 | strContent, err := generatePageHtml("home", map[string]interface{}{ 18 | "newnotebookurl": routes.APINewNotebook(), 19 | "notebooks": listNotebooks(notebookregistry, routes), 20 | "recipes": reciperegistry.GetRecipes(), 21 | }) 22 | if err != nil { 23 | http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 24 | return 25 | } 26 | 27 | _, _ = res.Write([]byte(strContent)) 28 | } 29 | } 30 | 31 | func listNotebooks(notebookRegistry *service.NotebookRegistry, routes *service.Routes) []NotebookSummaryFrontend { 32 | 33 | notebooks := notebookRegistry.GetNotebooks() 34 | sort.Slice(notebooks, func(a, b int) bool { 35 | return strings.Compare( 36 | strings.ToLower(notebooks[a].GetAbsdir()), 37 | strings.ToLower(notebooks[b].GetAbsdir()), 38 | ) > -1 39 | }) 40 | 41 | summaries := make([]NotebookSummaryFrontend, len(notebooks)) 42 | for i, notebook := range notebooks { 43 | summaries[i] = extractFrontendNotebookSummary(notebook, routes) 44 | } 45 | 46 | return summaries 47 | } 48 | -------------------------------------------------------------------------------- /src/core/httphandler/notebook.go: -------------------------------------------------------------------------------- 1 | package httphandler 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "os" 7 | "strings" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/netgusto/nodebook/src/core/shared/service" 11 | pkgErrors "github.com/pkg/errors" 12 | ) 13 | 14 | func NotebookHandler(notebookRegistry *service.NotebookRegistry, routes *service.Routes) HTTPHandler { 15 | return func(res http.ResponseWriter, req *http.Request) { 16 | params := mux.Vars(req) 17 | name, found := params["name"] 18 | if !found || strings.TrimSpace(name) == "" { 19 | res.WriteHeader(http.StatusBadRequest) 20 | return 21 | } 22 | 23 | notebook, err := notebookRegistry.GetNotebookByName(name) 24 | if err != nil { 25 | http.Error(res, http.StatusText(http.StatusNotFound), http.StatusNotFound) 26 | return 27 | } 28 | 29 | content, err := GetFileContent(notebook.GetMainFileAbsPath()) 30 | if err != nil { 31 | http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 32 | return 33 | } 34 | 35 | strContent, err := generatePageHtml("notebook", map[string]interface{}{ 36 | "notebook": extractFrontendNotebookSummary(notebook, routes), 37 | "homeurl": routes.Home(), 38 | "renamenotebookurl": routes.APINotebookRename(notebook.GetName()), 39 | "execurl": routes.APINotebookExec(notebook.GetName()), 40 | "stopurl": routes.APINotebookStop(notebook.GetName()), 41 | "persisturl": routes.APINotebookSetContent(notebook.GetName()), 42 | "content": content, 43 | }) 44 | if err != nil { 45 | http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 46 | return 47 | } 48 | 49 | res.Header().Add("Content-Type", "text/html; charset=utf8") 50 | res.Header().Add("Cache-Control", "max-age=0") 51 | 52 | _, _ = res.Write([]byte(strContent)) 53 | } 54 | } 55 | 56 | func GetFileContent(abspath string) (string, error) { 57 | f, err := os.Open(abspath) 58 | if err != nil { 59 | return "", pkgErrors.Wrapf(err, "Could not open notebook main file %s; ", abspath) 60 | } 61 | 62 | content, err := ioutil.ReadAll(f) 63 | if err != nil { 64 | return "", pkgErrors.Wrapf(err, "Could not read nottebook main file %s; ", abspath) 65 | } 66 | 67 | return string(content), nil 68 | } 69 | -------------------------------------------------------------------------------- /src/core/shared/docker.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import "os/exec" 4 | 5 | func IsDockerRunning() bool { 6 | return exec.Command("docker", "ps").Run() == nil 7 | } 8 | -------------------------------------------------------------------------------- /src/core/shared/recipe/helper/defaultinitnotebook.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | 9 | "github.com/markbates/pkger" 10 | "github.com/netgusto/nodebook/src/core/shared/types" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func defaultInitNotebook(recipe types.Recipe, notebookspath, name string) error { 15 | 16 | dirPerms := os.FileMode(0755) 17 | filePerm := os.FileMode(0644) 18 | 19 | srcPath := path.Join(recipe.GetDir(), "defaultcontent") 20 | destPathRoot := path.Join(notebookspath, name) 21 | if err := os.MkdirAll(destPathRoot, dirPerms); err != nil { 22 | return errors.Wrap(err, "defaultInitNotebook: Could not create notebook directory "+destPathRoot) 23 | } 24 | 25 | return pkger.Walk(srcPath, func(pathStr string, info os.FileInfo, err error) error { 26 | 27 | pathParts, _ := pkger.Parse(pathStr) 28 | relPath := pathParts.Name[len(srcPath):] // make path relative to "defaultcontent" 29 | destPath := path.Join(destPathRoot, relPath) 30 | 31 | if info.IsDir() { 32 | if err := os.MkdirAll(destPath, dirPerms); err != nil { 33 | return errors.Wrap(err, "defaultInitNotebook: Could not create notebook directory "+destPath) 34 | } 35 | } else { 36 | dir := filepath.Dir(destPath) 37 | if err := os.MkdirAll(dir, dirPerms); err != nil { 38 | return errors.Wrap(err, "defaultInitNotebook: Could not create notebook directory "+dir) 39 | } 40 | 41 | source, err := pkger.Open(pathStr) 42 | if err != nil { 43 | return errors.Wrap(err, "defaultInitNotebook: Could not open notebook default content file "+pathStr) 44 | } 45 | defer source.Close() 46 | 47 | destination, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, filePerm) 48 | if err != nil { 49 | return errors.Wrap(err, "defaultInitNotebook: Could not create notebook file "+destPath) 50 | } 51 | defer destination.Close() 52 | 53 | _, err = io.Copy(destination, source) 54 | if err != nil { 55 | return errors.Wrap(err, "defaultInitNotebook: Could not copy notebook default content file "+pathStr) 56 | } 57 | } 58 | 59 | return nil 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /src/core/shared/recipe/helper/stdexec.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | 11 | dockerTypes "github.com/docker/docker/api/types" 12 | "github.com/netgusto/nodebook/src/core/shared/types" 13 | 14 | "github.com/docker/docker/api/types/container" 15 | "github.com/docker/docker/api/types/network" 16 | "github.com/docker/docker/client" 17 | "github.com/docker/docker/pkg/stdcopy" 18 | "github.com/pkg/errors" 19 | ) 20 | 21 | func stdExecDocker(ctnrinfo types.ContainerInfo, writeStdOut, writeStdErr, writeInfo io.Writer) types.ExecHandler { 22 | 23 | ctx := context.Background() 24 | cli, err := client.NewEnvClient() 25 | var cont container.ContainerCreateCreatedBody 26 | 27 | return types.CallbackExecHandler{ 28 | StartFunc: func() { 29 | 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | createContainer := func() (container.ContainerCreateCreatedBody, error) { 35 | return cli.ContainerCreate(ctx, &container.Config{ 36 | Image: ctnrinfo.Image, 37 | Cmd: ctnrinfo.Cmd, 38 | Tty: false, 39 | AttachStdout: true, 40 | AttachStderr: true, 41 | WorkingDir: ctnrinfo.Cwd, 42 | Env: serializeEnv(ctnrinfo.Env), 43 | }, &container.HostConfig{ 44 | Binds: mountsToDockerBinds(ctnrinfo.Mounts), 45 | }, &network.NetworkingConfig{}, "") 46 | } 47 | 48 | cont, err = createContainer() 49 | if err != nil { 50 | // Pulling image from registry 51 | events, err := cli.ImagePull(ctx, ctnrinfo.Image, dockerTypes.ImagePullOptions{}) 52 | if err != nil { 53 | panic(errors.Wrap(err, "Could not pull image from registry")) 54 | } 55 | 56 | d := json.NewDecoder(events) 57 | 58 | type Event struct { 59 | ID string `json:"id"` 60 | Status string `json:"status"` 61 | Error string `json:"error"` 62 | Progress string `json:"progress"` 63 | ProgressDetail *struct { 64 | Current float64 `json:"current"` 65 | Total float64 `json:"total"` 66 | } `json:"progressDetail"` 67 | } 68 | 69 | var event *Event 70 | for { 71 | if err := d.Decode(&event); err != nil { 72 | if err == io.EOF { 73 | events.Close() 74 | break 75 | } 76 | 77 | panic(err) 78 | } 79 | 80 | percent := 0.0 81 | percentStr := "" 82 | if event.ProgressDetail != nil { 83 | current := event.ProgressDetail.Current 84 | total := event.ProgressDetail.Total 85 | if total > 0 { 86 | percent = (current / total) * 100 87 | percentStr = fmt.Sprintf("%2.2f", percent) 88 | } 89 | } 90 | 91 | msg := event.Status 92 | if event.ID != "" { 93 | msg += " " + event.ID 94 | } 95 | 96 | if percentStr != "" { 97 | msg += " " + percentStr + "%" 98 | } 99 | 100 | msg += "\n" 101 | 102 | _, _ = writeInfo.Write([]byte(msg)) 103 | } 104 | 105 | cont, err = createContainer() 106 | if err != nil { 107 | panic(errors.Wrap(err, "ERROR: Downloaded Docker image, but could not create container nevertheless")) 108 | } 109 | } 110 | 111 | if err := cli.ContainerStart(ctx, cont.ID, dockerTypes.ContainerStartOptions{}); err != nil { 112 | panic(errors.Wrap(err, "ERROR: Downloaded image, but to no avail")) 113 | } 114 | 115 | go dockerLogs(ctx, cont, cli, writeStdOut, writeStdErr) 116 | 117 | if _, err := cli.ContainerWait(ctx, cont.ID); err != nil { 118 | panic(errors.Wrap(err, "Error: can not wait for container to end")) 119 | } 120 | 121 | // Work around docker bug: not providing log if no newline present 122 | swOut, ok := writeStdOut.(*types.StreamWriter) 123 | if ok && swOut.BytesWritten() == 0 { 124 | dockerLogs(ctx, cont, cli, writeStdOut, nil) 125 | } 126 | 127 | swErr, ok := writeStdErr.(*types.StreamWriter) 128 | if ok && swErr.BytesWritten() == 0 { 129 | dockerLogs(ctx, cont, cli, nil, writeStdErr) 130 | } 131 | 132 | err = cli.ContainerRemove(ctx, cont.ID, dockerTypes.ContainerRemoveOptions{}) 133 | if err != nil { 134 | panic(errors.Wrapf(err, "ERROR: Could not remove container "+cont.ID)) 135 | } 136 | }, 137 | StopFunc: func() { 138 | err := cli.ContainerKill(ctx, cont.ID, "SIGKILL") 139 | if err != nil { 140 | panic(errors.Wrapf(err, "ERROR: Could not kill container %s", cont.ID)) 141 | } 142 | 143 | ctx.Done() 144 | }, 145 | } 146 | } 147 | 148 | func dockerLogs(ctx context.Context, cont container.ContainerCreateCreatedBody, cli *client.Client, writeStdOut, writeStdErr io.Writer) { 149 | reader, err := cli.ContainerLogs(ctx, cont.ID, dockerTypes.ContainerLogsOptions{ 150 | ShowStderr: writeStdErr != nil, 151 | ShowStdout: writeStdOut != nil, 152 | Timestamps: false, 153 | Follow: true, 154 | Tail: "all", 155 | }) 156 | 157 | if err != nil { 158 | panic(err) 159 | } 160 | defer reader.Close() 161 | 162 | _, err = stdcopy.StdCopy(writeStdOut, writeStdErr, reader) // demux 163 | if err != nil { 164 | panic(errors.Wrap(err, "Could not copy output")) 165 | } 166 | } 167 | 168 | func stdExecLocal(processinfo types.ProcessInfo, writeStdOut, writeStdErr, writeInfo io.Writer) types.ExecHandler { 169 | var cmd *exec.Cmd 170 | 171 | return types.CallbackExecHandler{ 172 | StartFunc: func() { 173 | cmd = exec.Command(processinfo.Cmd[0], processinfo.Cmd[1:]...) 174 | cmd.Dir = processinfo.Cwd 175 | cmd.Env = append(os.Environ(), serializeEnv(processinfo.Env)...) 176 | cmd.Stdout = writeStdOut 177 | cmd.Stderr = writeStdErr 178 | err := cmd.Run() 179 | if err != nil { 180 | _, _ = writeStdErr.Write([]byte(err.Error() + "\n")) 181 | return 182 | } 183 | 184 | if !cmd.ProcessState.Success() { 185 | _, _ = writeStdErr.Write([]byte(fmt.Sprintf("Process exited with status code %d\n", cmd.ProcessState.ExitCode()))) 186 | } 187 | }, 188 | StopFunc: func() { 189 | _ = cmd.Process.Kill() 190 | }, 191 | } 192 | } 193 | 194 | func serializeEnv(env types.EnvInfo) []string { 195 | serializedenv := []string{} 196 | 197 | if env == nil { 198 | return serializedenv 199 | } 200 | 201 | for key, val := range env { 202 | serializedenv = append(serializedenv, fmt.Sprintf("%s=%s", key, val)) 203 | } 204 | 205 | return serializedenv 206 | } 207 | 208 | func mountsToDockerBinds(mounts []types.ContainerMount) []string { 209 | dockerbinds := []string{} 210 | if mounts == nil { 211 | return dockerbinds 212 | } 213 | 214 | for _, mount := range mounts { 215 | dockerbinds = append(dockerbinds, fmt.Sprintf("%s:%s:%s", mount.From, mount.To, mount.Mode)) 216 | } 217 | 218 | return dockerbinds 219 | } 220 | -------------------------------------------------------------------------------- /src/core/shared/recipe/helper/stdrecipe.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/netgusto/nodebook/src/core/shared/types" 7 | ) 8 | 9 | type cmdBuilderFunc = func(types.Notebook) []string 10 | type envBuilderFunc = func(types.Notebook, map[string]string) map[string]string 11 | type mountsBuilderFunc = func(types.Notebook, []types.ContainerMount) []types.ContainerMount 12 | 13 | func StdRecipe( 14 | key, name, language, mainfile, cmmode, dockerImage string, 15 | dockerCmd, localCmd cmdBuilderFunc, 16 | addEnv envBuilderFunc, 17 | addMounts mountsBuilderFunc, 18 | ) types.Recipe { 19 | return types.MakeRecipeReal( 20 | key, // key 21 | name, // name 22 | language, // language 23 | mainfile, // mainfile 24 | cmmode, // cmmode 25 | "/src/recipes/"+key, // dir 26 | func(notebook types.Notebook, docker bool, writeStdOut, writeStdErr, writeInfo io.Writer, env map[string]string) types.ExecHandler { // exec 27 | 28 | if env == nil { 29 | env = map[string]string{} 30 | } 31 | 32 | if addEnv != nil { 33 | env = addEnv(notebook, env) 34 | } 35 | 36 | mounts := []types.ContainerMount{ 37 | types.ContainerMount{ 38 | From: notebook.GetAbsdir(), 39 | To: "/code", 40 | Mode: "rw", 41 | }, 42 | } 43 | 44 | if addMounts != nil { 45 | mounts = addMounts(notebook, mounts) 46 | } 47 | 48 | if docker { 49 | return stdExecDocker(types.ContainerInfo{ 50 | Image: dockerImage, 51 | Cmd: dockerCmd(notebook), 52 | Cwd: "/code", 53 | Mounts: mounts, 54 | Env: env, 55 | }, writeStdOut, writeStdErr, writeInfo) 56 | } 57 | return stdExecLocal(types.ProcessInfo{ 58 | Cmd: localCmd(notebook), 59 | Cwd: notebook.GetAbsdir(), 60 | Env: env, 61 | }, writeStdOut, writeStdErr, writeInfo) 62 | }, 63 | func(recipe types.Recipe, notebookspath, name string) error { // init 64 | return defaultInitNotebook(recipe, notebookspath, name) 65 | }, 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_c.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func C() types.Recipe { 9 | return helper.StdRecipe( 10 | "c", // key 11 | "c11", // name 12 | "C", // language 13 | "main.c", // mainfile 14 | "clike", // cmmode 15 | "docker.io/library/gcc:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"sh", "-c", "gcc -Wall -Wextra -Werror -o /tmp/code.out /code/" + notebook.GetRecipe().GetMainfile() + " && /tmp/code.out"} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"sh", "-c", "gcc -Wall -Wextra -Werror -o /tmp/code.out '" + notebook.GetMainFileAbsPath() + "' && /tmp/code.out"} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_clojure.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Clojure() types.Recipe { 9 | return helper.StdRecipe( 10 | "clojure", // key 11 | "Clojure", // name 12 | "Clojure", // language 13 | "index.clj", // mainfile 14 | "clojure", // cmmode 15 | "docker.io/library/clojure:tools-deps", 16 | func(notebook types.Notebook) []string { 17 | return []string{"sh", "-c", "clojure " + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"sh", "-c", "clojure '" + notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_cpp.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Cpp() types.Recipe { 9 | return helper.StdRecipe( 10 | "cpp", // key 11 | "C++14", // name 12 | "C++", // language 13 | "main.cpp", // mainfile 14 | "clike", // cmmode 15 | "docker.io/library/gcc:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"sh", "-c", "g++ -std=c++14 -Wall -Wextra -Werror -o /tmp/code.out /code/" + notebook.GetRecipe().GetMainfile() + " && /tmp/code.out"} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"sh", "-c", "g++ -std=c++14 -Wall -Wextra -Werror -o /tmp/code.out '" + notebook.GetMainFileAbsPath() + "' && /tmp/code.out"} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_csharp.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Csharp() types.Recipe { 9 | return helper.StdRecipe( 10 | "csharp", // key 11 | "C#", // name 12 | "csharp", // language 13 | "Program.cs", // mainfile 14 | "clike", // cmmode 15 | "docker.io/microsoft/dotnet", 16 | func(notebook types.Notebook) []string { 17 | return []string{"dotnet", "run"} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"dotnet", "run"} 21 | }, 22 | func(notebook types.Notebook, env map[string]string) map[string]string { 23 | env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1" 24 | return env 25 | }, 26 | nil, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_elixir.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Elixir() types.Recipe { 9 | return helper.StdRecipe( 10 | "elixir", // key 11 | "Elixir", // name 12 | "Elixir", // language 13 | "main.ex", // mainfile 14 | "elixir", // cmmode 15 | "docker.io/library/elixir:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"elixir", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"elixir", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_fsharp.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Fsharp() types.Recipe { 9 | return helper.StdRecipe( 10 | "fsharp", // key 11 | "F#", // name 12 | "fsharp", // language 13 | "Program.fs", // mainfile 14 | "mllike", // cmmode 15 | "docker.io/microsoft/dotnet", 16 | func(notebook types.Notebook) []string { 17 | return []string{"dotnet", "run"} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"dotnet", "run"} 21 | }, 22 | func(notebook types.Notebook, env map[string]string) map[string]string { 23 | env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1" 24 | return env 25 | }, 26 | nil, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_go.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Go() types.Recipe { 9 | return helper.StdRecipe( 10 | "go", // key 11 | "Go", // name 12 | "Go", // language 13 | "main.go", // mainfile 14 | "go", // cmmode 15 | "docker.io/library/golang:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"go", "run", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"go", "run", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_haskell.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Haskell() types.Recipe { 9 | return helper.StdRecipe( 10 | "haskell", // key 11 | "Haskell", // name 12 | "Haskell", // language 13 | "main.hs", // mainfile 14 | "haskell", // cmmode 15 | "docker.io/library/haskell:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"sh", "-c", "ghc -v0 -H14m -outputdir /tmp -o /tmp/code \"/code/" + notebook.GetRecipe().GetMainfile() + "\" && /tmp/code"} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"bash", "-c", "ghc -v0 -H14m -outputdir /tmp -o /tmp/code \"" + notebook.GetMainFileAbsPath() + "\" && /tmp/code"} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_java.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Java() types.Recipe { 9 | return helper.StdRecipe( 10 | "java", // key 11 | "Java", // name 12 | "Java", // language 13 | "main.java", // mainfile 14 | "clike", // cmmode 15 | "docker.io/library/java:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"sh", "-c", `javac -d /tmp "/code/` + notebook.GetRecipe().GetMainfile() + `" && cd /tmp && java Main`} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"sh", "-c", `javac -d /tmp "` + notebook.GetMainFileAbsPath() + `" && cd /tmp && java Main`} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_lua.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Lua() types.Recipe { 9 | return helper.StdRecipe( 10 | "lua", // key 11 | "Lua", // name 12 | "Lua", // language 13 | "main.lua", // mainfile 14 | "lua", // cmmode 15 | "docker.io/superpaintman/lua:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"lua", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"lua", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_nodejs.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func NodeJS() types.Recipe { 9 | return helper.StdRecipe( 10 | "nodejs", // key 11 | "NodeJS", // name 12 | "JavaScript", // language 13 | "index.js", // mainfile 14 | "javascript", // cmmode 15 | "docker.io/library/node:alpine", 16 | func(notebook types.Notebook) []string { 17 | return []string{"node", "--harmony", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"node", "--harmony", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_ocaml.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Ocaml() types.Recipe { 9 | return helper.StdRecipe( 10 | "ocaml", // key 11 | "OCaml", // name 12 | "OCaml", // language 13 | "index.ml", // mainfile 14 | "mllike", // cmmode 15 | "docker.io/ocaml/opam2:alpine", 16 | func(notebook types.Notebook) []string { 17 | return []string{"sh", "-c", "ocamlc -o /tmp/code.out /code/" + notebook.GetRecipe().GetMainfile() + " && /tmp/code.out"} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"sh", "-c", "ocamlc -o /tmp/code.out " + notebook.GetMainFileAbsPath() + " && /tmp/code.out"} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_php.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Php() types.Recipe { 9 | return helper.StdRecipe( 10 | "php", // key 11 | "PHP", // name 12 | "PHP", // language 13 | "main.php", // mainfile 14 | "php", // cmmode 15 | "docker.io/library/php:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"php", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"php", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_python3.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Python3() types.Recipe { 9 | return helper.StdRecipe( 10 | "python3", // key 11 | "Python 3", // name 12 | "Python", // language 13 | "main.py", // mainfile 14 | "python", // cmmode 15 | "docker.io/library/python:3", 16 | func(notebook types.Notebook) []string { 17 | return []string{"python", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"python", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_r.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func R() types.Recipe { 9 | return helper.StdRecipe( 10 | "r", // key 11 | "R", // name 12 | "R", // language 13 | "main.r", // mainfile 14 | "r", // cmmode 15 | "docker.io/library/r-base:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"Rscript", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"Rscript", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_ruby.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Ruby() types.Recipe { 9 | return helper.StdRecipe( 10 | "ruby", // key 11 | "Ruby", // name 12 | "Ruby", // language 13 | "main.rb", // mainfile 14 | "ruby", // cmmode 15 | "docker.io/library/ruby:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"ruby", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"ruby", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_rust.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 8 | "github.com/netgusto/nodebook/src/core/shared/types" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func Rust() types.Recipe { 13 | return helper.StdRecipe( 14 | "rust", // key 15 | "Rust", // name 16 | "Rust", // language 17 | "main.rs", // mainfile 18 | "rust", // cmmode 19 | "docker.io/library/rust:latest", 20 | func(notebook types.Notebook) []string { 21 | if hasCargo(notebook) { 22 | return []string{"sh", "-c", "cd /code/ && cargo run"} 23 | } 24 | 25 | return []string{"sh", "-c", "rustc -o /tmp/code.out /code/" + notebook.GetRecipe().GetMainfile() + " && /tmp/code.out"} 26 | }, 27 | func(notebook types.Notebook) []string { 28 | if hasCargo(notebook) { 29 | return []string{"cargo", "run"} 30 | } 31 | 32 | return []string{"sh", "-c", "rustc -o /tmp/code.out \"" + notebook.GetMainFileAbsPath() + "\" && /tmp/code.out"} 33 | }, 34 | nil, 35 | func(notebook types.Notebook, mounts []types.ContainerMount) []types.ContainerMount { 36 | if hasCargo(notebook) { 37 | mounts = append(mounts, types.ContainerMount{ 38 | From: path.Join(rustCargoHome(), "registry"), 39 | To: "/usr/local/cargo/registry", 40 | Mode: "rw", 41 | }) 42 | } 43 | 44 | return mounts 45 | }, 46 | ) 47 | } 48 | 49 | func hasCargo(notebook types.Notebook) bool { 50 | info, err := os.Stat(path.Join(notebook.GetAbsdir(), "Cargo.toml")) 51 | return err == nil && info.Mode().IsRegular() 52 | } 53 | 54 | func rustCargoHome() string { 55 | if value, found := os.LookupEnv("CARGO_HOME"); found { 56 | return value 57 | } 58 | 59 | homedir, err := os.UserHomeDir() 60 | if err != nil { 61 | panic(errors.Wrap(err, "Could not identify user home dir")) 62 | } 63 | 64 | return path.Join(homedir, ".cargo") 65 | } 66 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_swift.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 5 | "github.com/netgusto/nodebook/src/core/shared/types" 6 | ) 7 | 8 | func Swift() types.Recipe { 9 | return helper.StdRecipe( 10 | "swift", // key 11 | "Swift", // name 12 | "Swift", // language 13 | "main.swift", // mainfile 14 | "swift", // cmmode 15 | "docker.io/library/swift:latest", 16 | func(notebook types.Notebook) []string { 17 | return []string{"swift", "/code/" + notebook.GetRecipe().GetMainfile()} 18 | }, 19 | func(notebook types.Notebook) []string { 20 | return []string{"swift", notebook.GetMainFileAbsPath()} 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/shared/recipe/recipe_typescript.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/netgusto/nodebook/src/core/shared/recipe/helper" 8 | "github.com/netgusto/nodebook/src/core/shared/types" 9 | ) 10 | 11 | func Typescript() types.Recipe { 12 | return helper.StdRecipe( 13 | "typescript", // key 14 | "TypeScript", // name 15 | "TypeScript", // language 16 | "index.ts", // mainfile 17 | "javascript", // cmmode 18 | "docker.io/sandrokeil/typescript:latest", 19 | func(notebook types.Notebook) []string { 20 | if hasTsNode(notebook) { 21 | return []string{"sh", "-c", "node_modules/.bin/ts-node " + notebook.GetRecipe().GetMainfile()} 22 | } 23 | 24 | return []string{"sh", "-c", "tsc --allowJs --outFile /tmp/code.js " + notebook.GetRecipe().GetMainfile() + " && node /tmp/code.js"} 25 | }, 26 | func(notebook types.Notebook) []string { 27 | if hasTsNode(notebook) { 28 | return []string{"sh", "-c", "node_modules/.bin/ts-node " + notebook.GetRecipe().GetMainfile()} 29 | } 30 | 31 | return []string{"sh", "-c", "tsc --allowJs --outFile /tmp/code.js " + notebook.GetRecipe().GetMainfile() + " && node /tmp/code.js"} 32 | }, 33 | nil, 34 | nil, 35 | ) 36 | } 37 | 38 | func hasTsNode(notebook types.Notebook) bool { 39 | info, err := os.Stat(path.Join(notebook.GetAbsdir(), "node_modules", ".bin", "ts-node")) 40 | return err == nil && info.Mode().IsRegular() 41 | } 42 | -------------------------------------------------------------------------------- /src/core/shared/recipe/registry.go: -------------------------------------------------------------------------------- 1 | package recipe 2 | 3 | import ( 4 | "github.com/netgusto/nodebook/src/core/shared/service" 5 | ) 6 | 7 | func AddRecipesToRegistry(recipeRegistry *service.RecipeRegistry) { 8 | 9 | recipeRegistry. 10 | AddRecipe(C()). 11 | AddRecipe(Clojure()). 12 | AddRecipe(Cpp()). 13 | AddRecipe(Csharp()). 14 | AddRecipe(Elixir()). 15 | AddRecipe(Fsharp()). 16 | AddRecipe(Go()). 17 | AddRecipe(Haskell()). 18 | AddRecipe(Java()). 19 | AddRecipe(Lua()). 20 | AddRecipe(NodeJS()). 21 | AddRecipe(Ocaml()). 22 | AddRecipe(Php()). 23 | AddRecipe(Python3()). 24 | AddRecipe(R()). 25 | AddRecipe(Ruby()). 26 | AddRecipe(Rust()). 27 | AddRecipe(Swift()). 28 | AddRecipe(Typescript()) 29 | } 30 | -------------------------------------------------------------------------------- /src/core/shared/service/csrf.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | ) 7 | 8 | type CSRFService struct { 9 | tokens map[CSRFToken]CSRFToken 10 | } 11 | 12 | type CSRFToken string 13 | 14 | func NewCSRFService() *CSRFService { 15 | return &CSRFService{ 16 | tokens: map[CSRFToken]CSRFToken{}, 17 | } 18 | } 19 | 20 | func makeToken() CSRFToken { 21 | token := make([]byte, 32) 22 | _, _ = rand.Read(token) 23 | 24 | return CSRFToken(hex.EncodeToString(token)) 25 | } 26 | 27 | func (cs *CSRFService) NewToken() CSRFToken { 28 | token := makeToken() 29 | cs.tokens[token] = token 30 | return token 31 | } 32 | 33 | func (cs CSRFService) IsValid(token CSRFToken) bool { 34 | _, found := cs.tokens[token] 35 | return found 36 | } 37 | -------------------------------------------------------------------------------- /src/core/shared/service/notebookregistry.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | "time" 11 | 12 | "github.com/netgusto/nodebook/src/core/shared/types" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type NotebookRegistry struct { 17 | notebookspath string 18 | depth int 19 | reciperegistry *RecipeRegistry 20 | notebookscache []types.Notebook 21 | } 22 | 23 | func NewNotebookRegistry(notebookspath string, reciperegistry *RecipeRegistry) *NotebookRegistry { 24 | return &NotebookRegistry{ 25 | notebookspath: path.Clean(notebookspath), 26 | depth: 2, 27 | reciperegistry: reciperegistry, 28 | notebookscache: []types.Notebook{}, 29 | } 30 | } 31 | 32 | func (r NotebookRegistry) GetNotebooksPath() string { 33 | return r.notebookspath 34 | } 35 | 36 | func (r *NotebookRegistry) RegisterNotebook(notebook types.Notebook) { 37 | r.notebookscache = append(r.notebookscache, notebook) 38 | } 39 | 40 | func (r NotebookRegistry) GetNotebookByName(name string) (types.Notebook, error) { 41 | for _, notebook := range r.notebookscache { 42 | if notebook.GetName() == name { 43 | return notebook, nil 44 | } 45 | } 46 | 47 | return nil, errors.Errorf("Could not find notebook for name %s", name) 48 | } 49 | 50 | func (r NotebookRegistry) BuildNotebookDescriptor(notebookname string, recipe types.Recipe) (types.Notebook, error) { 51 | 52 | parts := strings.Split(notebookname, "/") 53 | if len(parts) > 1 && parts[0] == "src" && recipe.GetKey() == "rust" { 54 | notebookname = parts[0] 55 | } 56 | 57 | absdir := filepath.Join(r.notebookspath, notebookname) 58 | 59 | info, err := os.Stat(absdir) 60 | if err != nil { 61 | return nil, errors.Wrapf(err, "buildNotebookDescriptor: Could not stat notebook dir %s", absdir) 62 | } 63 | 64 | notebook := types.MakeNotebookReal( 65 | notebookname, 66 | absdir, 67 | info.ModTime().Format(time.RFC3339), 68 | recipe, 69 | ) 70 | 71 | mainFile := notebook.GetMainFileAbsPath() 72 | infoFile, err := os.Stat(mainFile) 73 | if err != nil { 74 | return nil, errors.Wrapf(err, "buildNotebookDescriptor: Could not stat notebook main file %s", mainFile) 75 | } 76 | 77 | if infoFile.ModTime().After(info.ModTime()) { 78 | notebook.SetMtime(infoFile.ModTime().Format(time.RFC3339)) 79 | } 80 | 81 | return ¬ebook, nil 82 | } 83 | 84 | func (r NotebookRegistry) DetermineNotebookNameByAbsDir(absdir string) (string, error) { 85 | if len(absdir) < len(r.notebookspath)+1 { 86 | return "", fmt.Errorf("Notebook \"%s\" not in base directory \"%s\"", absdir, r.notebookspath) 87 | } 88 | 89 | return absdir[len(r.notebookspath)+1:], nil 90 | } 91 | 92 | func (r NotebookRegistry) GetNotebooks() []types.Notebook { 93 | return r.notebookscache 94 | } 95 | 96 | func (r NotebookRegistry) FindNotebooks(folderpath string) ([]types.Notebook, error) { 97 | 98 | paths := []types.Notebook{} 99 | 100 | info, err := os.Stat(folderpath) 101 | if err != nil || !info.IsDir() { 102 | return nil, errors.Wrapf(err, "Could not find notebooks in %s", folderpath) 103 | } 104 | 105 | searchedNames := map[string]*types.Recipe{} 106 | for i, recipe := range r.reciperegistry.recipes { 107 | searchedNames[recipe.GetMainfile()] = &r.reciperegistry.recipes[i] 108 | } 109 | 110 | err = walk(folderpath, info, 0, func(absPath string, info os.FileInfo, depth int, err error) error { 111 | if err != nil { 112 | return err 113 | } 114 | 115 | name := info.Name() 116 | 117 | if info.IsDir() { 118 | if depth > r.depth { 119 | return filepath.SkipDir 120 | } 121 | 122 | if name == "node_modules" || strings.HasPrefix(name, ".") { 123 | return filepath.SkipDir 124 | } 125 | } else { 126 | if recipe, found := searchedNames[name]; found { 127 | notebookname, err := r.DetermineNotebookNameByAbsDir(filepath.Dir(absPath)) 128 | if err != nil { 129 | return nil // ignore directory 130 | } 131 | notebook, err := r.BuildNotebookDescriptor(notebookname, *recipe) 132 | if err != nil { 133 | return nil // ignore directory 134 | } 135 | paths = append(paths, notebook) 136 | } 137 | } 138 | 139 | return nil 140 | }) 141 | 142 | if err != nil && err != filepath.SkipDir { 143 | return nil, errors.Wrap(err, "Error while looking for notebooks") 144 | } 145 | 146 | return paths, nil // return collected paths 147 | } 148 | 149 | func (r *NotebookRegistry) Renamed(notebook types.Notebook, newname string) (types.Notebook, error) { 150 | renamedNotebook, err := r.BuildNotebookDescriptor(newname, notebook.GetRecipe()) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | for i, nb := range r.notebookscache { 156 | if nb.GetName() == notebook.GetName() { 157 | r.notebookscache[i] = renamedNotebook 158 | return renamedNotebook, nil 159 | } 160 | } 161 | 162 | return nil, fmt.Errorf("Could not find notebook %s in registry", notebook.GetName()) 163 | } 164 | 165 | func (r *NotebookRegistry) Refresh(notebook types.Notebook) (types.Notebook, error) { 166 | refreshedNotebook, err := r.BuildNotebookDescriptor(notebook.GetName(), notebook.GetRecipe()) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | for i, nb := range r.notebookscache { 172 | if nb.GetName() == notebook.GetName() { 173 | r.notebookscache[i] = refreshedNotebook 174 | return refreshedNotebook, nil 175 | } 176 | } 177 | 178 | return nil, fmt.Errorf("Could not find notebook %s in registry", notebook.GetName()) 179 | } 180 | 181 | type WalkFunc func(path string, info os.FileInfo, depth int, err error) error 182 | 183 | // walk recursively descends path, calling walkFn. 184 | // fork from filepath.walk, adding depth info 185 | func walk(pathStr string, info os.FileInfo, depth int, walkFn WalkFunc) error { 186 | if !info.IsDir() { 187 | return walkFn(pathStr, info, depth, nil) 188 | } 189 | 190 | names, err := readDirNames(pathStr) 191 | err1 := walkFn(pathStr, info, depth, err) 192 | // If err != nil, walk can't walk into this directory. 193 | // err1 != nil means walkFn want walk to skip this directory or stop walking. 194 | // Therefore, if one of err and err1 isn't nil, walk will return. 195 | if err != nil || err1 != nil { 196 | // The caller's behavior is controlled by the return value, which is decided 197 | // by walkFn. walkFn may ignore err and return nil. 198 | // If walkFn returns SkipDir, it will be handled by the caller. 199 | // So walk should return whatever walkFn returns. 200 | return err1 201 | } 202 | 203 | for _, name := range names { 204 | filename := path.Join(pathStr, name) 205 | fileInfo, err := os.Lstat(filename) 206 | if err != nil { 207 | if err := walkFn(filename, fileInfo, depth, err); err != nil && err != filepath.SkipDir { 208 | return err 209 | } 210 | } else { 211 | err = walk(filename, fileInfo, depth+1, walkFn) 212 | if err != nil { 213 | if !fileInfo.IsDir() || err != filepath.SkipDir { 214 | return err 215 | } 216 | } 217 | } 218 | } 219 | return nil 220 | } 221 | 222 | // readDirNames reads the directory named by dirname and returns 223 | // a sorted list of directory entries. 224 | func readDirNames(dirname string) ([]string, error) { 225 | f, err := os.Open(dirname) 226 | if err != nil { 227 | return nil, err 228 | } 229 | names, err := f.Readdirnames(-1) 230 | f.Close() 231 | if err != nil { 232 | return nil, err 233 | } 234 | sort.Strings(names) 235 | return names, nil 236 | } 237 | -------------------------------------------------------------------------------- /src/core/shared/service/notebookwatcher.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "log" 5 | "path/filepath" 6 | 7 | "github.com/fsnotify/fsnotify" 8 | "github.com/netgusto/nodebook/src/core/shared/types" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type NotebookWatcher struct { 13 | watcher *fsnotify.Watcher 14 | onChange func(types.Notebook) 15 | notebookregistry *NotebookRegistry 16 | } 17 | 18 | func NewNotebookWatcher(registry *NotebookRegistry, onChange func(types.Notebook)) (*NotebookWatcher, error) { 19 | watcher, err := fsnotify.NewWatcher() 20 | 21 | if err != nil { 22 | return nil, errors.Wrap(err, "Could not build FS watcher") 23 | } 24 | // defer w.watcher.Close() 25 | 26 | return &NotebookWatcher{ 27 | notebookregistry: registry, 28 | watcher: watcher, 29 | onChange: onChange, 30 | }, nil 31 | } 32 | 33 | func (w *NotebookWatcher) AddNotebook(notebook types.Notebook) error { 34 | if err := w.watcher.Add(notebook.GetAbsdir()); err != nil { 35 | return errors.Wrapf(err, "Could not watch %s", notebook.GetAbsdir()) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (w *NotebookWatcher) Watch() error { 42 | 43 | //////////////// 44 | // watch 45 | 46 | done := make(chan bool) 47 | go func() { 48 | for { 49 | select { 50 | case event, ok := <-w.watcher.Events: 51 | if !ok { 52 | return 53 | } 54 | 55 | if event.Op&fsnotify.Write == fsnotify.Write { 56 | 57 | absdir := filepath.Dir(event.Name) 58 | notebookname, err := w.notebookregistry.DetermineNotebookNameByAbsDir(absdir) 59 | if err != nil { 60 | log.Printf("ERROR: COULD NOT FIND NOTEBOOK FOR DIR %s\n", absdir) 61 | } 62 | 63 | notebook, err := w.notebookregistry.GetNotebookByName(notebookname) 64 | if err != nil { 65 | log.Printf("ERROR: COULD NOT FIND NOTEBOOK FOR NAME %s\n", notebookname) 66 | } 67 | 68 | if w.onChange != nil { 69 | w.onChange(notebook) 70 | } 71 | } 72 | case err, ok := <-w.watcher.Errors: 73 | if !ok { 74 | return 75 | } 76 | 77 | panic(err) 78 | } 79 | } 80 | }() 81 | 82 | <-done 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /src/core/shared/service/reciperegistry.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/netgusto/nodebook/src/core/shared/types" 7 | ) 8 | 9 | type RecipeRegistry struct { 10 | recipes []types.Recipe 11 | } 12 | 13 | func NewRecipeRegistry() *RecipeRegistry { 14 | return &RecipeRegistry{ 15 | recipes: []types.Recipe{}, 16 | } 17 | } 18 | 19 | func (rr RecipeRegistry) GetRecipes() []types.Recipe { 20 | return rr.recipes 21 | } 22 | 23 | func (rr RecipeRegistry) GetRecipeByKey(key string) *types.Recipe { 24 | for _, r := range rr.recipes { 25 | if r.GetKey() == key { 26 | return &r 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (rr *RecipeRegistry) AddRecipe(recipe types.Recipe) *RecipeRegistry { 34 | rr.recipes = append(rr.recipes, recipe) 35 | return rr 36 | } 37 | 38 | func (rr RecipeRegistry) GetRecipeForMainFilename(mainfilename string) (types.Recipe, error) { 39 | for _, recipe := range rr.recipes { 40 | if recipe.GetMainfile() == mainfilename { 41 | return recipe, nil 42 | } 43 | } 44 | 45 | return nil, errors.New("Recipe not found (matching " + mainfilename + ")") 46 | } 47 | -------------------------------------------------------------------------------- /src/core/shared/service/routes.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "net/url" 4 | 5 | type Routes struct{} 6 | 7 | func NewRoutes() *Routes { 8 | return &Routes{} 9 | } 10 | 11 | func (r Routes) APINewNotebook() string { 12 | return "/api/notebook/new" 13 | } 14 | 15 | func (r Routes) APINotebookSetContent(name string) string { 16 | return "/api/notebook/" + url.PathEscape(name) + "/setcontent" 17 | } 18 | 19 | func (r Routes) APINotebookExec(name string) string { 20 | return "/api/notebook/" + url.PathEscape(name) + "/exec" 21 | } 22 | 23 | func (r Routes) APINotebookStop(name string) string { 24 | return "/api/notebook/" + url.PathEscape(name) + "/stop" 25 | } 26 | 27 | func (r Routes) APINotebookRename(name string) string { 28 | return "/api/notebook/" + url.PathEscape(name) + "/rename" 29 | } 30 | 31 | func (r Routes) Home() string { 32 | return "/" 33 | } 34 | 35 | func (r Routes) Notebook(name string) string { 36 | return "/notebook/" + url.PathEscape(name) 37 | } 38 | -------------------------------------------------------------------------------- /src/core/shared/service/sanitize.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var r1 *regexp.Regexp = regexp.MustCompile(`\.{2,}`) 10 | var r2 *regexp.Regexp = regexp.MustCompile(`\\`) 11 | var r3 *regexp.Regexp = regexp.MustCompile(`\/`) 12 | var r4 *regexp.Regexp = regexp.MustCompile(`[^a-zA-Z0-9àâäéèëêìïîùûüÿŷ\s-_\.]`) 13 | var r5 *regexp.Regexp = regexp.MustCompile(`\s+`) 14 | 15 | func SanitizeNotebookName(name string) (string, error) { 16 | 17 | name = r1.ReplaceAllString(name, ".") 18 | name = r2.ReplaceAllString(name, "_") 19 | name = r3.ReplaceAllString(name, "_") 20 | name = r4.ReplaceAllString(name, "") 21 | name = r5.ReplaceAllString(name, " ") 22 | name = strings.TrimSpace(name) 23 | 24 | if name == "" || name[0] == '.' { 25 | return "", errors.New("Invalid name") 26 | } 27 | 28 | return strings.TrimSpace(name), nil 29 | } 30 | -------------------------------------------------------------------------------- /src/core/shared/types/callbackexechandler.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type CallbackExecHandler struct { 4 | StartFunc func() 5 | StopFunc func() 6 | } 7 | 8 | func (ceh CallbackExecHandler) Start() { 9 | ceh.StartFunc() 10 | } 11 | 12 | func (ceh CallbackExecHandler) Stop() { 13 | ceh.StopFunc() 14 | } 15 | -------------------------------------------------------------------------------- /src/core/shared/types/container.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ContainerInfo struct { 4 | Cmd []string 5 | Cwd string 6 | Env EnvInfo 7 | Image string 8 | Mounts []ContainerMount 9 | } 10 | 11 | type ContainerMount struct { 12 | From string 13 | To string 14 | Mode string // 'ro'|'rw' 15 | } 16 | -------------------------------------------------------------------------------- /src/core/shared/types/exechandler.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ExecHandler interface { 4 | Start() 5 | Stop() 6 | } 7 | 8 | type EnvInfo = map[string]string 9 | -------------------------------------------------------------------------------- /src/core/shared/types/notebook.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "path" 6 | ) 7 | 8 | type Notebook interface { 9 | GetName() string 10 | GetAbsdir() string 11 | GetMtime() string 12 | SetMtime(mtime string) 13 | GetRecipe() Recipe 14 | GetMainFileAbsPath() string 15 | } 16 | 17 | func MakeNotebookReal(notebookname, absdir, mtime string, recipe Recipe) NotebookReal { 18 | return NotebookReal{ 19 | name: notebookname, 20 | absdir: absdir, 21 | mtime: mtime, 22 | recipe: recipe, 23 | } 24 | } 25 | 26 | type NotebookReal struct { 27 | name string 28 | absdir string 29 | mtime string 30 | recipe Recipe 31 | } 32 | 33 | func (n *NotebookReal) SetMtime(mtime string) { 34 | n.mtime = mtime 35 | } 36 | 37 | func (n NotebookReal) GetName() string { 38 | return n.name 39 | } 40 | 41 | func (n NotebookReal) GetAbsdir() string { 42 | return n.absdir 43 | } 44 | 45 | func (n NotebookReal) GetMtime() string { 46 | return n.mtime 47 | } 48 | 49 | func (n NotebookReal) GetRecipe() Recipe { 50 | return n.recipe 51 | } 52 | 53 | func (n NotebookReal) MarshalJSON() ([]byte, error) { 54 | return json.Marshal(&struct { 55 | Name string `json:"name"` 56 | AbsDir string `json:"absdir"` 57 | MTime string `json:"mtime"` 58 | Recipe Recipe `json:"recipe"` 59 | }{ 60 | Name: n.GetName(), 61 | AbsDir: n.GetAbsdir(), 62 | MTime: n.GetMtime(), 63 | Recipe: n.GetRecipe(), 64 | }) 65 | } 66 | 67 | func (n NotebookReal) GetMainFileAbsPath() string { 68 | return path.Join(n.absdir, n.recipe.GetMainfile()) 69 | } 70 | -------------------------------------------------------------------------------- /src/core/shared/types/parameters.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type StdParameters struct { 4 | Docker bool `default:"false"` 5 | NotebooksPathFlag string `name:"notebooks" default:"" type:"path"` 6 | NotebooksPathArg string `arg:"" optional:"" name:"path" default:"" type:"path"` 7 | } 8 | 9 | func (p StdParameters) GetNotebooksPath() string { 10 | if p.NotebooksPathFlag != "" { 11 | return p.NotebooksPathFlag 12 | } 13 | 14 | return p.NotebooksPathArg 15 | } 16 | -------------------------------------------------------------------------------- /src/core/shared/types/process.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ProcessInfo struct { 4 | Cmd []string 5 | Cwd string 6 | Env EnvInfo 7 | } 8 | -------------------------------------------------------------------------------- /src/core/shared/types/recipe.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | type Recipe interface { 9 | GetKey() string 10 | GetName() string 11 | GetLanguage() string 12 | GetMainfile() string 13 | GetCmmode() string 14 | GetDir() string 15 | ExecNotebook(notebook Notebook, docker bool, writeStdOut io.Writer, writeStdErr io.Writer, writeInfo io.Writer, env EnvInfo) ExecHandler 16 | InitNotebook(recipe Recipe, notebookspath string, name string) error 17 | } 18 | 19 | func MakeRecipeReal( 20 | key, 21 | name, 22 | language, 23 | mainfile, 24 | cmmode, 25 | dir string, 26 | exec func( 27 | notebook Notebook, 28 | docker bool, 29 | writeStdOut io.Writer, 30 | writeStdErr io.Writer, 31 | writeInfo io.Writer, 32 | env EnvInfo, 33 | ) ExecHandler, 34 | init func( 35 | recipe Recipe, 36 | notebookspath string, 37 | name string, 38 | ) error, 39 | ) RecipeReal { 40 | return RecipeReal{ 41 | key, 42 | name, 43 | language, 44 | mainfile, 45 | cmmode, 46 | dir, 47 | exec, 48 | init, 49 | } 50 | } 51 | 52 | type RecipeReal struct { 53 | key string 54 | name string 55 | language string 56 | mainfile string 57 | cmmode string 58 | dir string 59 | exec func( 60 | notebook Notebook, 61 | docker bool, 62 | writeStdOut io.Writer, 63 | writeStdErr io.Writer, 64 | writeInfo io.Writer, 65 | env EnvInfo, 66 | ) ExecHandler 67 | init func( 68 | recipe Recipe, 69 | notebookspath string, 70 | name string, 71 | ) error 72 | } 73 | 74 | func (r RecipeReal) ExecNotebook(notebook Notebook, 75 | docker bool, 76 | writeStdOut io.Writer, 77 | writeStdErr io.Writer, 78 | writeInfo io.Writer, 79 | env EnvInfo) ExecHandler { 80 | return r.exec(notebook, docker, writeStdOut, writeStdErr, writeInfo, env) 81 | } 82 | 83 | func (r RecipeReal) InitNotebook( 84 | recipe Recipe, 85 | notebookspath string, 86 | name string) error { 87 | return r.init(recipe, notebookspath, name) 88 | } 89 | 90 | func (r RecipeReal) MarshalJSON() ([]byte, error) { 91 | return json.Marshal(&struct { 92 | Key string `json:"key"` 93 | Name string `json:"name"` 94 | Language string `json:"language"` 95 | Mainfile string `json:"mainfile"` 96 | CMMode string `json:"cmmode"` 97 | Dir string `json:"dir"` 98 | }{ 99 | Key: r.key, 100 | Name: r.name, 101 | Language: r.language, 102 | Mainfile: r.mainfile, 103 | CMMode: r.cmmode, 104 | Dir: r.dir, 105 | }) 106 | } 107 | 108 | func (r RecipeReal) GetKey() string { 109 | return r.key 110 | } 111 | 112 | func (r RecipeReal) GetName() string { 113 | return r.name 114 | } 115 | 116 | func (r RecipeReal) GetLanguage() string { 117 | return r.language 118 | } 119 | 120 | func (r RecipeReal) GetMainfile() string { 121 | return r.mainfile 122 | } 123 | 124 | func (r RecipeReal) GetCmmode() string { 125 | return r.cmmode 126 | } 127 | 128 | func (r RecipeReal) GetDir() string { 129 | return r.dir 130 | } 131 | -------------------------------------------------------------------------------- /src/core/shared/types/streamwriter.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type StreamWriter struct { 4 | write func(data string) 5 | bytesWritten int 6 | } 7 | 8 | func NewStreamWriter(f func(data string)) *StreamWriter { 9 | return &StreamWriter{ 10 | write: f, 11 | } 12 | } 13 | 14 | func (sw *StreamWriter) Write(p []byte) (n int, err error) { 15 | sw.write(string(p)) 16 | sw.bytesWritten += len(p) 17 | return len(p), nil 18 | } 19 | 20 | func (sw StreamWriter) BytesWritten() int { 21 | return sw.bytesWritten 22 | } 23 | -------------------------------------------------------------------------------- /src/frontend/ApiClient/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiClient as ApiClientType, 3 | Notebook as NotebookType 4 | } from '../types'; 5 | 6 | class ApiClient implements ApiClientType { 7 | private csrfToken: string; 8 | private debounceWait = 400; 9 | private debounceTimeout; 10 | private options = { 11 | headers: { 12 | 'Accept': 'application/json', 13 | 'Content-Type': 'application/json' 14 | } 15 | }; 16 | 17 | private post(url, body) { 18 | return this.getCsrfToken().then((csrfToken) => { 19 | const options = Object.assign({}, this.options, { 20 | method: 'POST', 21 | body: JSON.stringify(Object.assign({}, body, { csrfToken })) 22 | }) 23 | return window.fetch(url, options); 24 | }); 25 | } 26 | 27 | getCsrfToken(): Promise { 28 | if (this.csrfToken) return Promise.resolve(this.csrfToken); 29 | return window.fetch('/csrf') 30 | .then((res) => res.json()) 31 | .then((json) => { 32 | this.csrfToken = json.csrfToken; 33 | return this.csrfToken; 34 | }) 35 | } 36 | 37 | persist(url: string, content: string) { 38 | return this.post(url, { content }); 39 | } 40 | 41 | debouncedPersist(url: string, value: string) { 42 | clearTimeout(this.debounceTimeout); 43 | this.debounceTimeout = setTimeout(() => { 44 | this.persist(url, value) 45 | }, this.debounceWait); 46 | } 47 | 48 | stop(url: string) { 49 | return this.post(url, {}); 50 | } 51 | 52 | rename(url: string, newname: string) { 53 | return this.post(url, { newname }); 54 | } 55 | 56 | create(url, recipekey) { 57 | return this.post(url, { recipekey }); 58 | } 59 | } 60 | 61 | export default ApiClient; 62 | -------------------------------------------------------------------------------- /src/frontend/Components/App/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | Route as RouteType, 5 | NotebookHandle as NotebookHandleType, 6 | Notebook as NotebookType, 7 | ApiClient as ApiClientType, 8 | Recipe 9 | } from "../../types"; 10 | 11 | import Home, { Props as HomeProps } from "../Home"; 12 | import Notebook, { Props as NotebookProps } from "../Notebook"; 13 | 14 | 15 | interface Props { 16 | apiClient: ApiClientType, 17 | route: RouteType; 18 | recipes?: Recipe[]; 19 | notebooks?: NotebookHandleType[]; 20 | notebook?: NotebookType; 21 | homeurl?: string; 22 | execurl?: string; 23 | stopurl?: string; 24 | persisturl?: string; 25 | content?: string; 26 | renamenotebookurl?: string; 27 | newnotebookurl?: string; 28 | } 29 | 30 | export default function(props: Props) { 31 | const { route } = props; 32 | const NB = Notebook as any; 33 | switch(route) { 34 | case "home": return ; break; 35 | case "notebook": return ; break; 36 | default: throw new Error("Unknown route:" + route); 37 | } 38 | } -------------------------------------------------------------------------------- /src/frontend/Components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import cx from 'classnames'; 3 | import subDays from 'date-fns/subDays'; 4 | import isBefore from 'date-fns/isBefore'; 5 | import Spinner from 'react-spinkit'; 6 | 7 | import { NotebookHandle, Recipe, ApiClient } from "../../types"; 8 | 9 | interface NotebooksListProps { 10 | notebooks: NotebookHandle[]; 11 | } 12 | 13 | function NotebooksList(props: NotebooksListProps) { 14 | const { notebooks } = props; 15 | return ( 16 |
17 |
    18 | {notebooks.map(notebook => ( 19 |
  • 20 | {notebook.name} 21 | {notebook.recipe.name} 22 |
  • 23 | ))} 24 |
25 |
26 | ); 27 | } 28 | 29 | export interface Props { 30 | apiClient: ApiClient; 31 | notebooks: NotebookHandle[]; 32 | recipes: Recipe[]; 33 | newnotebookurl: string; 34 | } 35 | 36 | interface State { 37 | menuopen: boolean; 38 | creating: boolean; 39 | } 40 | 41 | export default class Home extends React.Component { 42 | 43 | state: State = { 44 | menuopen: false, 45 | creating: false, 46 | }; 47 | 48 | props: Props; 49 | 50 | render() { 51 | 52 | const { notebooks, recipes } = this.props; 53 | const { menuopen, creating } = this.state; 54 | 55 | const recents = []; 56 | const horizon = subDays(new Date(), 1); 57 | 58 | notebooks.forEach(notebook => { 59 | if (!isBefore(new Date(notebook.mtime), horizon)) { 60 | recents.push(notebook); 61 | } 62 | }); 63 | 64 | recents.sort((a, b) => a.mtime > b.mtime ? -1 : 1); 65 | 66 | const showTitle = recents.length > 0; 67 | 68 | return ( 69 |
70 | 71 |
72 | {recents.length > 0 && ( 73 |
74 |

Recently used

75 | 76 |
77 | )} 78 | 79 | {notebooks.length > 0 && notebooks.length > recents.length && ( 80 |
81 | {showTitle &&

All notebooks

} 82 | 83 |
84 | )} 85 |
86 | 87 |
88 | 93 | {menuopen && ( 94 |
{recipes.map(recipe => ( 95 | this.selectRecipe(recipe.key)}>{recipe.name} 96 | ))}
97 | )} 98 |
99 |
100 | ); 101 | } 102 | 103 | private selectRecipe(recipekey: string) { 104 | const { newnotebookurl } = this.props; 105 | const { creating } = this.state; 106 | if (creating) return; 107 | 108 | (this as any).setState({ creating: true }); 109 | 110 | this.props.apiClient.create(newnotebookurl, recipekey) 111 | .then(res => res.json()) 112 | .then(({ url }) => { 113 | document.location.href = url; 114 | }) 115 | .catch(_ => { 116 | alert('Error: Notebook could not be created.'); 117 | }); 118 | } 119 | } -------------------------------------------------------------------------------- /src/frontend/Components/Notebook/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import cx from 'classnames'; 3 | import { AllHtmlEntities as Entities } from 'html-entities'; 4 | import Convert from 'ansi-to-html'; 5 | 6 | import {UnControlled as CodeMirror} from 'react-codemirror2' 7 | const CM = CodeMirror as any; 8 | 9 | import 'codemirror/lib/codemirror.css'; 10 | import 'codemirror/theme/monokai.css'; 11 | 12 | import 'codemirror/mode/javascript/javascript'; 13 | import 'codemirror/mode/clike/clike'; 14 | import 'codemirror/mode/clojure/clojure'; 15 | import 'codemirror/mode/go/go'; 16 | import 'codemirror/mode/haskell/haskell'; 17 | import 'codemirror/mode/lua/lua'; 18 | import 'codemirror/mode/mllike/mllike'; 19 | import 'codemirror/mode/php/php'; 20 | import 'codemirror/mode/python/python'; 21 | import 'codemirror/mode/r/r'; 22 | import 'codemirror/mode/ruby/ruby'; 23 | import 'codemirror/mode/rust/rust'; 24 | import 'codemirror/mode/swift/swift'; 25 | import 'codemirror/mode/swift/swift'; 26 | import 'codemirror-mode-elixir'; 27 | 28 | import 'codemirror/keymap/sublime'; 29 | 30 | import 'codemirror/addon/selection/active-line'; 31 | import 'codemirror/addon/edit/matchbrackets'; 32 | import 'codemirror/addon/edit/closebrackets'; 33 | import 'codemirror/addon/scroll/annotatescrollbar'; 34 | import 'codemirror/addon/search/matchesonscrollbar'; 35 | import 'codemirror/addon/search/searchcursor'; 36 | import 'codemirror/addon/search/match-highlighter'; 37 | import 'codemirror/addon/fold/indent-fold'; 38 | import 'codemirror/addon/scroll/scrollpastend'; 39 | 40 | import { Notebook, ApiClient } from "../../types"; 41 | import "./style.scss"; 42 | 43 | export interface Props { 44 | apiClient: ApiClient, 45 | notebook: Notebook; 46 | homeurl: string; 47 | execurl: string; 48 | stopurl: string; 49 | persisturl: string; 50 | content: string; 51 | renamenotebookurl: string; 52 | } 53 | 54 | export interface State { 55 | autoclear: boolean; 56 | newname: string|undefined; 57 | running: boolean; 58 | dragging: boolean; 59 | codeWidth: number; 60 | } 61 | 62 | export default class NotebookComponent extends React.Component { 63 | 64 | props: Props; 65 | state: State = { 66 | autoclear: false, 67 | newname: undefined, 68 | running: false, 69 | dragging: false, 70 | codeWidth: 60, 71 | }; 72 | 73 | private boundHandleKeyDown: EventListener; 74 | private boundHandleMouseUp: EventListener; 75 | private boundHandleMouseDown: EventListener; 76 | private boundHandleMouseMove: EventListener; 77 | private editorvalue: string; 78 | private entities: any; 79 | private ansiConvert: Convert; 80 | private console: HTMLElement; 81 | 82 | private commWorker: Worker; 83 | 84 | constructor(props: Props) { 85 | super(props); 86 | this.boundHandleKeyDown = this.handleKeyDown.bind(this); 87 | this.boundHandleMouseDown = this.handleMouseDown.bind(this); 88 | this.boundHandleMouseUp = this.handleMouseUp.bind(this); 89 | this.boundHandleMouseMove = this.handleMouseMove.bind(this); 90 | this.onNotebookNameKeyDown = this.onNotebookNameKeyDown.bind(this); 91 | this.editorvalue = props.content; 92 | this.entities = new Entities(); 93 | this.ansiConvert = new Convert(); 94 | } 95 | 96 | // Pleasing TS 97 | setState(s: Partial) { super.setState(s); } 98 | 99 | componentWillMount() { 100 | document.addEventListener('keydown', this.boundHandleKeyDown); 101 | document.addEventListener('mouseup', this.boundHandleMouseUp); 102 | document.addEventListener('mousemove', this.boundHandleMouseMove); 103 | } 104 | 105 | componentDidMount() { 106 | 107 | let i = 0; 108 | this.commWorker = new Worker('worker.ts'); 109 | this.commWorker.onmessage = (e: MessageEvent) => { 110 | switch(e.data.action) { 111 | case 'consoleLogIfRunning': { 112 | if (this.state.running) { 113 | this.consoleLog(e.data.payload.msg, e.data.payload.chan); 114 | } 115 | break; 116 | } 117 | case 'execEnded': { 118 | this.setState({ running: false }); 119 | break; 120 | } 121 | } 122 | }; 123 | 124 | this.consoleLog('Ready.\n', 'info'); 125 | } 126 | 127 | componentWillUnmount() { 128 | this.commWorker.terminate(); 129 | document.removeEventListener('keydown', this.boundHandleKeyDown); 130 | } 131 | 132 | handleMouseDown(event) { 133 | this.setState({ dragging: true }); 134 | } 135 | 136 | handleMouseUp(event) { 137 | this.setState({ dragging: false }); 138 | } 139 | 140 | handleMouseMove(event) { 141 | if (this.state.dragging) { 142 | const percent = (event.pageX / window.innerWidth) * 100; 143 | this.setState({ codeWidth: percent }); 144 | } 145 | } 146 | 147 | handleKeyDown(event) { 148 | if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) { // cmd + Enter 149 | event.preventDefault(); 150 | this.execNotebook(); 151 | } else if ((event.metaKey || event.ctrlKey) && event.key === 's') { // cmd + S 152 | 153 | const { persisturl } = this.props; 154 | 155 | event.preventDefault(); 156 | this.props.apiClient.persist(persisturl, this.editorvalue).then(() => this.execNotebook()); 157 | } else if (event.ctrlKey && event.key === 'c') { // ctrl+c 158 | event.preventDefault(); 159 | if (this.state.running) { 160 | this.stopExecution(); 161 | } 162 | } else if (event.ctrlKey && event.key === 'r') { // ctrl+r 163 | event.preventDefault(); 164 | this.execNotebook(); 165 | } 166 | } 167 | 168 | render() { 169 | const { notebook, homeurl, persisturl, content } = this.props; 170 | const { autoclear, newname, running, codeWidth } = this.state; 171 | const layoutStyle = { gridTemplateColumns: `${codeWidth}% 5px calc(${100-codeWidth}% - 5px)` }; 172 | 173 | return ( 174 |
175 |
176 |
177 |
178 | 181 |
182 | 183 |
184 | this.onNotebookNameCommit()} 188 | onInput={e => this.onNotebookNameChange((e.target as HTMLElement).innerText)} 189 | onKeyDown={this.onNotebookNameKeyDown} 190 | > 191 | {newname === undefined ? notebook.name : newname} 192 | 193 | {notebook.recipe.name} 194 |
195 | 196 |
197 | 198 |
199 | 200 |
201 | 202 |
203 |
204 |
205 |
206 | cm.execCommand("indentMore"), 221 | "Shift-Tab": (cm) => cm.execCommand("indentLess"), 222 | "Cmd-Enter": (cm) => {}, 223 | } 224 | }} 225 | onChange={(_, __, value) => { 226 | this.editorvalue = value; 227 | this.props.apiClient.debouncedPersist(persisturl, value); 228 | }} 229 | /> 230 |
231 |
232 |
233 | 236 |
237 |
238 | ); 239 | } 240 | 241 | private sanitizeName(name: string) { 242 | if (typeof name !== 'string') throw new Error('The notebook name should be a string'); 243 | 244 | name = name. 245 | replace(/\.{2,}/g, '.'). 246 | replace(/\\/g, '_'). 247 | replace(/\//g, '_'). 248 | replace(/[^a-zA-Z0-9àâäéèëêìïîùûüÿŷ\s-_\.]/g, ''). 249 | replace(/\s+/g, ' '). 250 | trim(); 251 | 252 | if (name === '' || name[0] === '.') throw new Error('Invalid name'); 253 | 254 | return name; 255 | } 256 | private onNotebookNameKeyDown(event) { 257 | if (event.keyCode == 13) { // Enter 258 | event.preventDefault(); 259 | this.onNotebookNameCommit(); 260 | return; 261 | } 262 | } 263 | private onNotebookNameChange(newname) { 264 | this.setState({ newname }); 265 | } 266 | 267 | private onNotebookNameCommit() { 268 | 269 | const { newname } = this.state; 270 | const { notebook, renamenotebookurl } = this.props; 271 | 272 | if (newname === undefined) return; 273 | 274 | // Sanitize name 275 | 276 | let sanitizedName; 277 | try { 278 | sanitizedName = this.sanitizeName(newname); 279 | } catch(e) { 280 | this.setState({ newname: undefined }); 281 | window.setTimeout(() => alert('Invalid name.'), 10); 282 | return; 283 | } 284 | 285 | if (sanitizedName === notebook.name) { 286 | this.setState({ newname: undefined }); 287 | return; 288 | } 289 | 290 | this.setState({ newname: sanitizedName }); 291 | 292 | // Persist name change 293 | 294 | return this.props.apiClient.rename(renamenotebookurl, sanitizedName) 295 | .then(res => res.json()) 296 | .then(({ url }) => document.location.href = url) 297 | .catch(_ => alert('Error: Notebook could not be renamed.')); 298 | } 299 | 300 | private stopExecution() { 301 | const { running } = this.state; 302 | if (!running) return; 303 | 304 | const { stopurl } = this.props; 305 | 306 | return this.props.apiClient.stop(stopurl) 307 | .then(() => { 308 | this.consoleLog('--- Execution stopped.\n\n', 'info'); 309 | this.setState({ running: false }); 310 | }) 311 | .catch(() => { 312 | alert('An error occured when stopping the current execution.'); 313 | }); 314 | } 315 | 316 | private execNotebook() { 317 | 318 | const { autoclear, running } = this.state; 319 | const { execurl } = this.props; 320 | 321 | if (running) return; 322 | 323 | this.setState({ running: true }); 324 | 325 | if (autoclear) this.consoleClear(); 326 | 327 | this.consoleLog('--- Running...\n', 'info'); 328 | 329 | this.props.apiClient.getCsrfToken() 330 | .then((csrfToken) => { 331 | this.commWorker.postMessage({ 332 | action: 'exec', 333 | url: execurl, 334 | csrfToken 335 | }); 336 | }) 337 | } 338 | 339 | msgstack: any[] = [] 340 | 341 | consoleLog(msg: string, cls: string) { 342 | this.msgstack.push({ msg, cls }); 343 | window.requestAnimationFrame(() => { 344 | if (!this.msgstack.length) return; 345 | 346 | const maxchilds = 300; 347 | 348 | const allhtml = this.msgstack.map(m => { 349 | return '' + this.ansiConvert.toHtml(this.entities.encode(m.msg)) + ''; 350 | }).join(''); 351 | 352 | this.console.innerHTML += allhtml; 353 | 354 | const nbchilds = this.console.childElementCount; 355 | if(nbchilds > maxchilds) { 356 | for (let i = nbchilds - maxchilds; i >= 0; i--) { 357 | this.console.children[i].remove() 358 | } 359 | const truncated = document.createElement('span'); 360 | truncated.className = 'info'; 361 | truncated.innerText = 'Output truncated (too big).\n'; 362 | this.console.prepend(truncated); 363 | } 364 | 365 | this.console.scrollTop = this.console.scrollHeight; 366 | this.msgstack = []; 367 | }); 368 | } 369 | 370 | consoleClear() { 371 | this.console.innerHTML = ''; 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/frontend/Components/Notebook/style.scss: -------------------------------------------------------------------------------- 1 | $yellowcolor: #FFC312; 2 | 3 | html, body {margin: 0; height: 100%} 4 | html.notebook, html.notebook body {overflow-y: hidden} 5 | 6 | body { 7 | margin: 0; padding: 0; 8 | background: #272822; 9 | 10 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 11 | 12 | .notebook-recipe { 13 | color: $yellowcolor; 14 | } 15 | 16 | .notebook-recipe:before { 17 | display: inline-block; 18 | content: '—'; // mdash 19 | margin: 0 10px; 20 | } 21 | 22 | .bigbutton { 23 | height: 30px; 24 | color: white; 25 | border: none; 26 | border-radius: 5px; 27 | font-size: 1.1rem; 28 | font-weight: bold; 29 | cursor: pointer; 30 | } 31 | } 32 | 33 | .home-app { 34 | 35 | padding: 10px; 36 | font-size: 18px; 37 | 38 | &, a { 39 | color: white; 40 | &:visited { 41 | color: #aaa; 42 | } 43 | } 44 | 45 | display: grid; 46 | grid-template-columns: auto 150px; 47 | grid-template-areas: "notebooklist tools"; 48 | 49 | .list { 50 | grid-area: notebooklist; 51 | 52 | h2 { 53 | margin-left: 16px; 54 | } 55 | } 56 | 57 | .tools { 58 | grid-area: tools; 59 | text-align: left; 60 | padding-top: 15px; 61 | 62 | .btn-new { 63 | background-color: #00a8ff; 64 | text-align: right; 65 | width: 113px; 66 | white-space: nowrap; 67 | 68 | &.creating { 69 | text-align: center; 70 | & .sk-spinner { 71 | margin: 0 auto; 72 | } 73 | } 74 | } 75 | 76 | .recipe-list { 77 | padding: 10px; 78 | } 79 | 80 | .recipe-list .recipe-item { 81 | display: block; 82 | cursor: pointer; 83 | text-decoration: underline; 84 | 85 | padding-top: 5px; 86 | 87 | &:hover { 88 | color: $yellowcolor; 89 | } 90 | } 91 | } 92 | } 93 | 94 | .notebook-app { 95 | 96 | body { 97 | font-size: 14px; 98 | } 99 | 100 | .CodeMirror { 101 | width: 100%; 102 | height: calc(100vh - 50px); 103 | 104 | font-family: 'Roboto Mono', Menlo, 'Ubuntu Mono', Monaco, Consolas, 'source-code-pro', monospace; 105 | line-height: 1.414; 106 | } 107 | 108 | .CodeMirror-linenumber { 109 | padding-right: 20px; 110 | } 111 | 112 | .cm-s-monokai .CodeMirror-linenumber { 113 | color: #777; 114 | } 115 | 116 | #layout { 117 | display: grid; 118 | grid-template-rows: 50px auto; 119 | grid-template-areas: 'top top top' 120 | 'code gutter console'; 121 | } 122 | 123 | #top { 124 | grid-area: top; 125 | background-color: #333; 126 | padding: 10px; 127 | vertical-align: middle; 128 | 129 | display: grid; 130 | grid-template-columns: 100px auto auto 100px; 131 | grid-template-areas: 'btn-run notebook-header console-options btn-home'; 132 | } 133 | 134 | #top #btn-run { 135 | grid-area: btn-run; 136 | } 137 | 138 | #top #notebook-header { 139 | grid-area: notebook-header; 140 | font-size: 20px; 141 | line-height: 30px; 142 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 143 | } 144 | 145 | #top #notebook-header .notebook-name { 146 | color: white; 147 | font-weight: bold; 148 | cursor: text; 149 | } 150 | 151 | #top #console-options { 152 | grid-area: console-options; 153 | text-align: right; 154 | 155 | color: white; 156 | line-height: 30px; 157 | padding-right: 30px; 158 | } 159 | 160 | #top #btn-home { 161 | grid-area: btn-home; 162 | text-align: right; 163 | } 164 | 165 | #top #btn-run .bigbutton.run { 166 | width: 90px; 167 | 168 | background-color: rgb(107, 183, 175); 169 | 170 | &.running { 171 | background-color: #f06; 172 | } 173 | } 174 | 175 | #top #btn-home .bigbutton { 176 | background-color: #9b59b6; 177 | } 178 | 179 | #left { 180 | grid-area: code; 181 | } 182 | 183 | #gutter { 184 | grid-area: gutter; 185 | background-color: #333; 186 | cursor: col-resize; 187 | } 188 | 189 | #right { 190 | grid-area: console; 191 | } 192 | 193 | #console { 194 | color: #f8f8f2; 195 | width: 100%; 196 | height: calc(100vh - 50px); // 70: 50 + 10 (padding) * 2 197 | overflow: scroll; 198 | unicode-bidi: embed; 199 | font-family: 'Roboto Mono', Menlo, 'Ubuntu Mono', Monaco, Consolas, 'source-code-pro', monospace; 200 | white-space: pre; 201 | margin-bottom: 20px; 202 | // padding-left: 10px; 203 | } 204 | 205 | #console .stderr { color: red; } 206 | #console .info { color: #888; } 207 | #console .forcednl { background-color: #f06; color: white; } 208 | } -------------------------------------------------------------------------------- /src/frontend/Components/Notebook/worker.ts: -------------------------------------------------------------------------------- 1 | onmessage = function(e: MessageEvent) { 2 | switch(e.data.action) { 3 | case 'exec': { 4 | execAction(e.data.url, e.data.csrfToken); 5 | } 6 | } 7 | } 8 | 9 | function consoleLogIfRunning(msg: string, chan: string) { 10 | (postMessage as any)({ 11 | action: 'consoleLogIfRunning', 12 | payload: { 13 | msg, 14 | chan, 15 | } 16 | }); 17 | } 18 | 19 | function notifyExecEnded() { 20 | (postMessage as any)({ 21 | action: 'execEnded', 22 | }); 23 | } 24 | 25 | function execAction(url: string, csrfToken: string) { 26 | return fetch(url, { 27 | method: 'POST', 28 | body: JSON.stringify({ csrfToken }), 29 | headers: { 30 | 'Accept': 'application/json', 31 | 'Content-Type': 'application/json' 32 | }, 33 | }) 34 | .then(res => { 35 | if (!res.body) { 36 | // response not streamable; use it in one piece 37 | 38 | let hasLastNewLine = true; 39 | return res.text() 40 | .then(text => { 41 | text.split('\n').map(jsonline => { 42 | 43 | if (jsonline.trim().length === 0) return; 44 | 45 | const data = JSON.parse(jsonline); 46 | const txt = JSON.parse(data.data); 47 | const lastnl = txt.lastIndexOf('\n'); 48 | hasLastNewLine = (lastnl === txt.length - 1); 49 | consoleLogIfRunning(txt, data.chan); 50 | }); 51 | 52 | return { hasLastNewLine }; 53 | }); 54 | } else { 55 | return new Promise((resolve, reject) => { 56 | const reader = res.body.getReader(); 57 | const decoder = new TextDecoder("utf-8"); 58 | let hasLastNewLine = true; 59 | const pump = () => { 60 | reader.read().then(({ done, value }) => { 61 | if (done) { 62 | resolve({ hasLastNewLine }); 63 | return; 64 | } 65 | 66 | decoder.decode(value).split('\n').map(jsonline => { 67 | 68 | if (jsonline.trim().length === 0) return; 69 | 70 | const data = JSON.parse(jsonline); 71 | const txt = JSON.parse(data.data); 72 | const lastnl = txt.lastIndexOf('\n'); 73 | hasLastNewLine = (lastnl === txt.length - 1); 74 | consoleLogIfRunning(txt, data.chan); 75 | }); 76 | 77 | // Get the data and send it to the browser via the controller 78 | pump(); 79 | }); 80 | } 81 | 82 | pump(); 83 | }); 84 | } 85 | }) 86 | .then(({ hasLastNewLine }) => { 87 | if (!hasLastNewLine) { 88 | consoleLogIfRunning('%\n', 'forcednl'); 89 | } 90 | 91 | consoleLogIfRunning('--- Done.\n\n', 'info'); 92 | notifyExecEnded(); 93 | }) 94 | .catch(err => { 95 | consoleLogIfRunning('\n--- An error occurred during execution.\n\n', 'stderr'); 96 | notifyExecEnded(); 97 | }); 98 | } -------------------------------------------------------------------------------- /src/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import ApiClient from './ApiClient'; 5 | import { Route as RouteType } from "./types"; 6 | 7 | import App from './Components/App'; 8 | 9 | const apiClient = new ApiClient(); 10 | 11 | (window as any).main = (function main(element: HTMLElement, route: RouteType, params: any) { 12 | ReactDOM.render( 13 | , 14 | element, 15 | ); 16 | }); -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nbk", 3 | "version": "0.1.31", 4 | "description": "Nodebook - Multi-Language REPL with web UI", 5 | "main": "dist/nodejs/backend/index.js", 6 | "scripts": { 7 | "test": "", 8 | "build": "rm -Rf ../../dist && parcel build *.html --out-dir ../../dist/frontend && npm run test" 9 | }, 10 | "author": "@netgusto", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@octokit/rest": "^16.36.0", 14 | "body-parser": "^1.19.0", 15 | "chalk": "^3.0.0", 16 | "chokidar": "^3.3.1", 17 | "codemirror": "^5.50.2", 18 | "compression": "^1.7.4", 19 | "dockerode": "^3.0.2", 20 | "dotenv": "^8.2.0", 21 | "event-stream": "4.0.1", 22 | "express": "^4.17.1", 23 | "globby": "^11.0.0", 24 | "minimist": "^1.2.0", 25 | "project-name-generator": "^2.1.7", 26 | "recursive-copy": "^2.0.10", 27 | "title-case": "^3.0.2", 28 | "tree-kill": "^1.2.2", 29 | "trunk": "^1.1.0" 30 | }, 31 | "devDependencies": { 32 | "@types/dockerode": "^2.5.21", 33 | "@types/minimist": "^1.2.0", 34 | "@types/node": "^13.1.7", 35 | "chai": "^4.2.0", 36 | "chai-as-promised": "^7.1.1", 37 | "chai-http": "^4.3.0", 38 | "classnames": "^2.2.6", 39 | "codemirror-mode-elixir": "^1.1.2", 40 | "concurrently": "^5.0.2", 41 | "cpx": "^1.5.0", 42 | "date-fns": "^2.9.0", 43 | "html-entities": "^1.2.1", 44 | "mocha": "^7.0.0", 45 | "node-sass": "^4.13.0", 46 | "nodemon": "^2.0.2", 47 | "parcel-bundler": "^1.12.4", 48 | "portfinder": "^1.0.25", 49 | "preact": "^10.2.1", 50 | "preact-compat": "^3.19.0", 51 | "react": "^16.12.0", 52 | "react-codemirror2": "^6.0.0", 53 | "react-dom": "^16.12.0", 54 | "react-spinkit": "^3.0.0", 55 | "sass": "^1.23.0", 56 | "typescript": "^3.7.4" 57 | }, 58 | "alias": { 59 | "react": "preact/compat", 60 | "react-dom": "preact/compat" 61 | }, 62 | "bin": { 63 | "nbk": "bin/nbk", 64 | "nbkcli": "bin/nbkcli" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "dist/nodejs", 6 | "noImplicitAny": true, 7 | "allowJs": true, 8 | "sourceMap": false 9 | }, 10 | "include": [ 11 | "src/**/*.ts" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | "src/recipes/*/defaultcontent", 16 | "src/frontend" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/frontend/types.ts: -------------------------------------------------------------------------------- 1 | export type Route = "home" | "notebook"; 2 | 3 | export interface NotebookHandle { 4 | name: string; 5 | url: string; 6 | recipe: Recipe; 7 | mtime: string; 8 | } 9 | 10 | export interface Notebook extends NotebookHandle { 11 | } 12 | 13 | export interface Recipe { 14 | key: string; 15 | name: string; 16 | language: string; 17 | cmmode: string; 18 | } 19 | 20 | export interface ApiClient { 21 | getCsrfToken: () => Promise; 22 | persist: (url: string, value: string) => Promise; 23 | debouncedPersist: (url: string, value: string) => void; 24 | stop: (url: string) => Promise; 25 | rename: (url: string, name: string) => Promise; 26 | create: (url: string, recipekey: string) => Promise; 27 | } 28 | -------------------------------------------------------------------------------- /src/recipes/c/defaultcontent/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("Hello, World!\n"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/recipes/clojure/defaultcontent/index.clj: -------------------------------------------------------------------------------- 1 | (prn "Hello, World!") -------------------------------------------------------------------------------- /src/recipes/cpp/defaultcontent/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | std::cout << "Hello, World!\n"; 5 | return 0; 6 | } -------------------------------------------------------------------------------- /src/recipes/csharp/defaultcontent/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace HelloWorld 3 | { 4 | class Hello 5 | { 6 | static void Main() 7 | { 8 | Console.WriteLine("Hello World!"); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/recipes/csharp/defaultcontent/app.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/recipes/elixir/defaultcontent/main.ex: -------------------------------------------------------------------------------- 1 | IO.puts("Hello Elixir!") 2 | -------------------------------------------------------------------------------- /src/recipes/fsharp/defaultcontent/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org 2 | 3 | open System 4 | 5 | [] 6 | let main argv = 7 | printfn "Hello World from F#!" 8 | 0 // return an integer exit code 9 | -------------------------------------------------------------------------------- /src/recipes/fsharp/defaultcontent/app.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/recipes/go/defaultcontent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello, World!") 7 | } 8 | -------------------------------------------------------------------------------- /src/recipes/haskell/defaultcontent/main.hs: -------------------------------------------------------------------------------- 1 | module Main 2 | where 3 | 4 | main=putStrLn "Hello, World!" -------------------------------------------------------------------------------- /src/recipes/java/defaultcontent/main.java: -------------------------------------------------------------------------------- 1 | class Main { 2 | public static void main(String[] args) { 3 | System.out.println("Hello, World!"); 4 | } 5 | } -------------------------------------------------------------------------------- /src/recipes/lua/defaultcontent/main.lua: -------------------------------------------------------------------------------- 1 | print("Hello, World!") -------------------------------------------------------------------------------- /src/recipes/nodejs/defaultcontent/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/recipes/nodejs/defaultcontent/index.js: -------------------------------------------------------------------------------- 1 | console.log('Hello, World!'); -------------------------------------------------------------------------------- /src/recipes/ocaml/defaultcontent/index.ml: -------------------------------------------------------------------------------- 1 | print_string "Hello, World!\n" -------------------------------------------------------------------------------- /src/recipes/php/defaultcontent/main.php: -------------------------------------------------------------------------------- 1 | { 13 | return stdExec({ 14 | cmd: ['cat', notebook.mainfilename], 15 | cwd: notebook.absdir, 16 | env, 17 | }, writeStdOut, writeStdErr, writeInfo); 18 | 19 | }, 20 | init: async ({ name, notebookspath }) => await defaultInitNotebook(recipe, notebookspath, name), 21 | }); 22 | 23 | export default recipe; 24 | -------------------------------------------------------------------------------- /src/recipes/python3/defaultcontent/main.py: -------------------------------------------------------------------------------- 1 | print ("Hello, World!") -------------------------------------------------------------------------------- /src/recipes/r/defaultcontent/main.r: -------------------------------------------------------------------------------- 1 | cat("Hello, World!") -------------------------------------------------------------------------------- /src/recipes/ruby/defaultcontent/main.rb: -------------------------------------------------------------------------------- 1 | print "Hello, World!" -------------------------------------------------------------------------------- /src/recipes/rust/defaultcontent/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, World!"); 3 | } 4 | -------------------------------------------------------------------------------- /src/recipes/swift/defaultcontent/main.swift: -------------------------------------------------------------------------------- 1 | print("Hello, World!") -------------------------------------------------------------------------------- /src/recipes/typescript/defaultcontent/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/recipes/typescript/defaultcontent/index.ts: -------------------------------------------------------------------------------- 1 | function say(msg: string) { 2 | console.log(msg); 3 | } 4 | 5 | say('Hello, World!'); 6 | -------------------------------------------------------------------------------- /src/recipes/typescript/defaultcontent/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@types/node": { 6 | "version": "10.9.4", 7 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", 8 | "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==" 9 | }, 10 | "arrify": { 11 | "version": "1.0.1", 12 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 13 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" 14 | }, 15 | "buffer-from": { 16 | "version": "1.1.1", 17 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 18 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 19 | }, 20 | "diff": { 21 | "version": "3.5.0", 22 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 23 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" 24 | }, 25 | "make-error": { 26 | "version": "1.3.5", 27 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", 28 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==" 29 | }, 30 | "minimist": { 31 | "version": "1.2.0", 32 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 33 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 34 | }, 35 | "mkdirp": { 36 | "version": "0.5.1", 37 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 38 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 39 | "requires": { 40 | "minimist": "0.0.8" 41 | }, 42 | "dependencies": { 43 | "minimist": { 44 | "version": "0.0.8", 45 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 46 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 47 | } 48 | } 49 | }, 50 | "source-map": { 51 | "version": "0.6.1", 52 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 53 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 54 | }, 55 | "source-map-support": { 56 | "version": "0.5.9", 57 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", 58 | "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", 59 | "requires": { 60 | "buffer-from": "^1.0.0", 61 | "source-map": "^0.6.0" 62 | } 63 | }, 64 | "ts-node": { 65 | "version": "7.0.1", 66 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", 67 | "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", 68 | "requires": { 69 | "arrify": "^1.0.0", 70 | "buffer-from": "^1.1.0", 71 | "diff": "^3.1.0", 72 | "make-error": "^1.1.1", 73 | "minimist": "^1.2.0", 74 | "mkdirp": "^0.5.1", 75 | "source-map-support": "^0.5.6", 76 | "yn": "^2.0.0" 77 | } 78 | }, 79 | "typescript": { 80 | "version": "3.0.3", 81 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.3.tgz", 82 | "integrity": "sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==" 83 | }, 84 | "yn": { 85 | "version": "2.0.0", 86 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 87 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/recipes/typescript/defaultcontent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@types/node": "^10.9.4", 4 | "ts-node": "^7.0.1", 5 | "typescript": "^3.0.3" 6 | } 7 | } -------------------------------------------------------------------------------- /src/recipes/typescript/defaultcontent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": false, 4 | "noImplicitAny": false, 5 | "module": "commonjs", 6 | "target": "es2015" 7 | }, 8 | "include": [ 9 | "**/*" 10 | ], 11 | "exclude": [ 12 | "node_modules", 13 | "**/*.spec.ts" 14 | ] 15 | } -------------------------------------------------------------------------------- /test/fixtures/notebooks/C++/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | std::cout << "Hello, World!\n"; 5 | return 0; 6 | } -------------------------------------------------------------------------------- /test/fixtures/notebooks/C/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("Hello, World!\n"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/notebooks/Elixir/main.ex: -------------------------------------------------------------------------------- 1 | IO.puts("Hello Elixir!") 2 | -------------------------------------------------------------------------------- /test/fixtures/notebooks/Go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello, World!") 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/notebooks/Haskell/main.hs: -------------------------------------------------------------------------------- 1 | module Main 2 | where 3 | 4 | main=putStrLn "Hello, World!" -------------------------------------------------------------------------------- /test/fixtures/notebooks/Java/main.java: -------------------------------------------------------------------------------- 1 | class Main { 2 | public static void main(String[] args) { 3 | System.out.println("Hello, World!"); 4 | } 5 | } -------------------------------------------------------------------------------- /test/fixtures/notebooks/Javascript/index.js: -------------------------------------------------------------------------------- 1 | console.log('Hello, World!'); -------------------------------------------------------------------------------- /test/fixtures/notebooks/Lua/main.lua: -------------------------------------------------------------------------------- 1 | print("Hello, World!") -------------------------------------------------------------------------------- /test/fixtures/notebooks/PHP/main.php: -------------------------------------------------------------------------------- 1 |