summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGordon Ball <gordon@chronitis.net>2016-10-24 14:20:25 +0200
committerGordon Ball <gordon@chronitis.net>2016-10-24 14:20:25 +0200
commit1b789b6b3bfdc79fa5ba90dd5aa3dadd3ce542eb (patch)
tree18a231d0688550324f1a8148eb0367af497efd7c
Imported Upstream version 4.2.3
-rw-r--r--.bowerrc3
-rw-r--r--.dockerignore4
-rw-r--r--.gitignore31
-rw-r--r--.gitmodules0
-rw-r--r--.mailmap149
-rw-r--r--.travis.yml34
-rw-r--r--CONTRIBUTING.md69
-rw-r--r--COPYING.md60
-rw-r--r--Dockerfile108
-rw-r--r--MANIFEST.in28
-rw-r--r--README.md128
-rw-r--r--bower.json23
-rw-r--r--docs/Makefile201
-rw-r--r--docs/autogen_config.py45
-rw-r--r--docs/jsdoc_config.json21
-rw-r--r--docs/jsdoc_plugin.js12
-rw-r--r--docs/make.bat263
-rw-r--r--docs/requirements.txt11
-rw-r--r--docs/resources/Info.plist.example20
-rwxr-xr-xdocs/resources/generate_icons.sh16
-rw-r--r--docs/resources/icon_16x16.svg149
-rw-r--r--docs/resources/icon_24x24.svg167
-rw-r--r--docs/resources/icon_32x32.svg311
-rw-r--r--docs/resources/icon_512x512.svg226
-rw-r--r--docs/resources/ipynb.icnsbin0 -> 292771 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_1024x1024.pngbin0 -> 101069 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_128x128.pngbin0 -> 8031 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_128x128@2x.pngbin0 -> 18521 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_16x16.pngbin0 -> 541 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_16x16@2x.pngbin0 -> 1040 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_24x24.pngbin0 -> 770 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_24x24@2x.pngbin0 -> 1561 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_256x256.pngbin0 -> 18521 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_256x256@2x.pngbin0 -> 42915 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_32x32.pngbin0 -> 1200 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_32x32@2x.pngbin0 -> 2258 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_48x48.pngbin0 -> 2479 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_512x512.pngbin0 -> 42915 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_512x512@2x.pngbin0 -> 87724 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_64x64.pngbin0 -> 3522 bytes
-rw-r--r--docs/resources/ipynb.iconset/icon_64x64@2x.pngbin0 -> 8031 bytes
-rw-r--r--docs/resources/notebook_basics.pngbin0 -> 580014 bytes
-rw-r--r--docs/resources/running_code.pngbin0 -> 628620 bytes
-rw-r--r--docs/resources/running_code_med.pngbin0 -> 338883 bytes
-rw-r--r--docs/source/_static/.gitkeep0
-rw-r--r--docs/source/_static/images/cell-toolbar-41.pngbin0 -> 45028 bytes
-rw-r--r--docs/source/_static/images/command-palette-41.pngbin0 -> 39634 bytes
-rw-r--r--docs/source/_static/images/find-replace-41.pngbin0 -> 90501 bytes
-rw-r--r--docs/source/_static/images/jupyter-file-editor.pngbin0 -> 268819 bytes
-rw-r--r--docs/source/_static/images/jupyter-notebook-dashboard.pngbin0 -> 68135 bytes
-rw-r--r--docs/source/_static/images/jupyter-notebook-default.pngbin0 -> 164814 bytes
-rw-r--r--docs/source/_static/images/jupyter-notebook-edit.pngbin0 -> 223151 bytes
-rw-r--r--docs/source/_static/images/multi-select-41.pngbin0 -> 106470 bytes
-rw-r--r--docs/source/changelog.rst173
-rw-r--r--docs/source/conf.py334
-rw-r--r--docs/source/development_faq.rst8
-rw-r--r--docs/source/development_js.rst81
-rw-r--r--docs/source/examples/Notebook/Connecting with the Qt Console.ipynb132
-rw-r--r--docs/source/examples/Notebook/Custom Keyboard Shortcuts.ipynb142
-rw-r--r--docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb281
-rw-r--r--docs/source/examples/Notebook/Examples and Tutorials Index.ipynb81
-rw-r--r--docs/source/examples/Notebook/Importing Notebooks.ipynb523
-rw-r--r--docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb594
-rw-r--r--docs/source/examples/Notebook/Notebook Basics.ipynb253
-rw-r--r--docs/source/examples/Notebook/Running Code.ipynb289
-rw-r--r--docs/source/examples/Notebook/Typesetting Equations.ipynb274
-rw-r--r--docs/source/examples/Notebook/What is the Jupyter Notebook.ipynb182
-rw-r--r--docs/source/examples/Notebook/Working With Markdown Cells.ipynb322
-rw-r--r--docs/source/examples/Notebook/images/command_mode.pngbin0 -> 6673 bytes
-rw-r--r--docs/source/examples/Notebook/images/dashboard_files_tab.pngbin0 -> 116878 bytes
-rw-r--r--docs/source/examples/Notebook/images/dashboard_files_tab_btns.pngbin0 -> 13356 bytes
-rw-r--r--docs/source/examples/Notebook/images/dashboard_files_tab_new.pngbin0 -> 33908 bytes
-rw-r--r--docs/source/examples/Notebook/images/dashboard_files_tab_run.pngbin0 -> 85527 bytes
-rw-r--r--docs/source/examples/Notebook/images/dashboard_running_tab.pngbin0 -> 211313 bytes
-rw-r--r--docs/source/examples/Notebook/images/edit_mode.pngbin0 -> 6619 bytes
-rw-r--r--docs/source/examples/Notebook/images/menubar_toolbar.pngbin0 -> 30328 bytes
-rw-r--r--docs/source/examples/Notebook/images/nbconvert_arch.pngbin0 -> 114431 bytes
-rw-r--r--docs/source/examples/Notebook/nbpackage/__init__.py0
-rw-r--r--docs/source/examples/Notebook/nbpackage/mynotebook.ipynb69
-rw-r--r--docs/source/examples/Notebook/nbpackage/nbs/__init__.py0
-rw-r--r--docs/source/examples/Notebook/nbpackage/nbs/other.ipynb44
-rw-r--r--docs/source/examples/Notebook/rstversions/Connecting with the Qt Console.rst67
-rw-r--r--docs/source/examples/Notebook/rstversions/Custom Keyboard Shortcuts.rst71
-rw-r--r--docs/source/examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages.rst245
-rw-r--r--docs/source/examples/Notebook/rstversions/Examples and Tutorials Index.rst34
-rw-r--r--docs/source/examples/Notebook/rstversions/Importing Notebooks.rst286
-rw-r--r--docs/source/examples/Notebook/rstversions/JavaScript Notebook Extensions.rst389
-rw-r--r--docs/source/examples/Notebook/rstversions/Notebook Basics.rst212
-rw-r--r--docs/source/examples/Notebook/rstversions/Running Code.rst154
-rw-r--r--docs/source/examples/Notebook/rstversions/Typesetting Equations.rst290
-rw-r--r--docs/source/examples/Notebook/rstversions/What is the Jupyter Notebook.rst136
-rw-r--r--docs/source/examples/Notebook/rstversions/Working With Markdown Cells.rst313
-rw-r--r--docs/source/examples/Notebook/rstversions/index.rst37
-rw-r--r--docs/source/examples/images/FrontendKernel.graffle/data.plist461
-rw-r--r--docs/source/examples/images/FrontendKernel.graffle/image1.pngbin0 -> 14726 bytes
-rw-r--r--docs/source/examples/images/FrontendKernel.pngbin0 -> 33170 bytes
-rw-r--r--docs/source/examples/images/animation.m4vbin0 -> 11903 bytes
-rw-r--r--docs/source/examples/images/ipython_logo.pngbin0 -> 9216 bytes
-rw-r--r--docs/source/examples/images/jupyter_logo.pngbin0 -> 4473 bytes
-rw-r--r--docs/source/examples/images/python_logo.svg269
-rw-r--r--docs/source/examples/utils/list_pyfiles.ipy6
-rw-r--r--docs/source/examples/utils/list_subdirs.ipy7
-rw-r--r--docs/source/extending/contents.rst219
-rw-r--r--docs/source/extending/frontend_extensions.rst231
-rw-r--r--docs/source/extending/handlers.rst127
-rw-r--r--docs/source/extending/index.rst15
-rw-r--r--docs/source/extending/savehooks.rst77
-rw-r--r--docs/source/frontend_config.rst79
-rw-r--r--docs/source/index.rst52
-rw-r--r--docs/source/ipython_security.asc52
-rw-r--r--docs/source/links.txt39
-rw-r--r--docs/source/notebook.rst435
-rw-r--r--docs/source/public_server.rst231
-rw-r--r--docs/source/security.rst154
-rw-r--r--docs/source/template.tpl20
-rw-r--r--docs/source/ui_components.rst49
-rw-r--r--git-hooks/README.md9
-rwxr-xr-xgit-hooks/install-hooks.sh9
-rwxr-xr-xgit-hooks/post-checkout22
l---------git-hooks/post-merge1
-rw-r--r--notebook/__init__.py26
-rw-r--r--notebook/__main__.py5
-rw-r--r--notebook/_sysinfo.py95
-rw-r--r--notebook/_version.py13
-rw-r--r--notebook/allow76.py311
-rw-r--r--notebook/auth/__init__.py1
-rw-r--r--notebook/auth/login.py109
-rw-r--r--notebook/auth/logout.py23
-rw-r--r--notebook/auth/security.py101
-rw-r--r--notebook/auth/tests/__init__.py0
-rw-r--r--notebook/auth/tests/test_security.py25
-rw-r--r--notebook/base/__init__.py0
-rw-r--r--notebook/base/handlers.py615
-rw-r--r--notebook/base/zmqhandlers.py299
-rw-r--r--notebook/edit/__init__.py0
-rw-r--r--notebook/edit/handlers.py29
-rw-r--r--notebook/files/__init__.py0
-rw-r--r--notebook/files/handlers.py60
-rw-r--r--notebook/jstest.py635
-rw-r--r--notebook/kernelspecs/__init__.py0
-rw-r--r--notebook/kernelspecs/handlers.py27
-rw-r--r--notebook/log.py48
-rw-r--r--notebook/nbconvert/__init__.py0
-rw-r--r--notebook/nbconvert/handlers.py168
-rw-r--r--notebook/nbconvert/tests/__init__.py0
-rw-r--r--notebook/nbconvert/tests/test_nbconvert_handlers.py132
-rw-r--r--notebook/nbextensions.py1176
-rw-r--r--notebook/notebook/__init__.py0
-rw-r--r--notebook/notebook/handlers.py55
-rw-r--r--notebook/notebookapp.py1210
-rw-r--r--notebook/serverextensions.py341
-rw-r--r--notebook/services/__init__.py0
-rw-r--r--notebook/services/api/__init__.py0
-rw-r--r--notebook/services/api/api.yaml365
-rw-r--r--notebook/services/api/handlers.py23
-rw-r--r--notebook/services/config/__init__.py1
-rw-r--r--notebook/services/config/handlers.py43
-rw-r--r--notebook/services/config/manager.py51
-rw-r--r--notebook/services/config/tests/__init__.py0
-rw-r--r--notebook/services/config/tests/test_config_api.py68
-rw-r--r--notebook/services/contents/__init__.py0
-rw-r--r--notebook/services/contents/checkpoints.py142
-rw-r--r--notebook/services/contents/filecheckpoints.py201
-rw-r--r--notebook/services/contents/fileio.py305
-rw-r--r--notebook/services/contents/filemanager.py484
-rw-r--r--notebook/services/contents/handlers.py336
-rw-r--r--notebook/services/contents/manager.py471
-rw-r--r--notebook/services/contents/tests/__init__.py0
-rw-r--r--notebook/services/contents/tests/test_contents_api.py694
-rw-r--r--notebook/services/contents/tests/test_fileio.py131
-rw-r--r--notebook/services/contents/tests/test_manager.py629
-rw-r--r--notebook/services/contents/tz.py46
-rw-r--r--notebook/services/kernels/__init__.py0
-rw-r--r--notebook/services/kernels/handlers.py435
-rw-r--r--notebook/services/kernels/kernelmanager.py169
-rw-r--r--notebook/services/kernels/tests/__init__.py0
-rw-r--r--notebook/services/kernels/tests/test_kernels_api.py146
-rw-r--r--notebook/services/kernelspecs/__init__.py0
-rw-r--r--notebook/services/kernelspecs/handlers.py87
-rw-r--r--notebook/services/kernelspecs/tests/__init__.py0
-rw-r--r--notebook/services/kernelspecs/tests/test_kernelspecs_api.py129
-rw-r--r--notebook/services/nbconvert/__init__.py0
-rw-r--r--notebook/services/nbconvert/handlers.py25
-rw-r--r--notebook/services/nbconvert/tests/__init__.py0
-rw-r--r--notebook/services/nbconvert/tests/test_nbconvert_api.py31
-rw-r--r--notebook/services/security/__init__.py4
-rw-r--r--notebook/services/security/handlers.py23
-rw-r--r--notebook/services/sessions/__init__.py0
-rw-r--r--notebook/services/sessions/handlers.py161
-rw-r--r--notebook/services/sessions/sessionmanager.py235
-rw-r--r--notebook/services/sessions/tests/__init__.py0
-rw-r--r--notebook/services/sessions/tests/test_sessionmanager.py182
-rw-r--r--notebook/services/sessions/tests/test_sessions_api.py193
-rw-r--r--notebook/static/auth/css/override.css8
-rw-r--r--notebook/static/auth/js/loginmain.js14
-rw-r--r--notebook/static/auth/js/loginwidget.js38
-rw-r--r--notebook/static/auth/js/logoutmain.js12
-rw-r--r--notebook/static/auth/js/main.js9
-rw-r--r--notebook/static/auth/less/login.less6
-rw-r--r--notebook/static/auth/less/logout.less2
-rw-r--r--notebook/static/auth/less/style.less7
-rw-r--r--notebook/static/base/images/favicon.icobin0 -> 34494 bytes
-rw-r--r--notebook/static/base/images/logo.pngbin0 -> 4473 bytes
-rw-r--r--notebook/static/base/js/dialog.js220
-rw-r--r--notebook/static/base/js/events.js24
-rw-r--r--notebook/static/base/js/keyboard.js475
-rw-r--r--notebook/static/base/js/namespace.js82
-rw-r--r--notebook/static/base/js/notificationarea.js83
-rw-r--r--notebook/static/base/js/notificationwidget.js170
-rw-r--r--notebook/static/base/js/page.js62
-rw-r--r--notebook/static/base/js/security.js126
-rw-r--r--notebook/static/base/js/utils.js898
-rw-r--r--notebook/static/base/less/error.less20
-rw-r--r--notebook/static/base/less/flexbox.less269
-rw-r--r--notebook/static/base/less/mixins.less19
-rw-r--r--notebook/static/base/less/page.less149
-rw-r--r--notebook/static/base/less/style.less9
-rw-r--r--notebook/static/base/less/variables.less62
-rw-r--r--notebook/static/custom/custom.css7
-rw-r--r--notebook/static/custom/custom.js82
-rw-r--r--notebook/static/edit/js/editor.js220
-rw-r--r--notebook/static/edit/js/main.js98
-rw-r--r--notebook/static/edit/js/menubar.js166
-rw-r--r--notebook/static/edit/js/notificationarea.js29
-rw-r--r--notebook/static/edit/js/savewidget.js184
-rw-r--r--notebook/static/edit/less/edit.less51
-rw-r--r--notebook/static/edit/less/menubar.less26
-rw-r--r--notebook/static/edit/less/style.less7
-rw-r--r--notebook/static/notebook/css/override.css7
-rw-r--r--notebook/static/notebook/js/about.js46
-rw-r--r--notebook/static/notebook/js/actions.js733
-rw-r--r--notebook/static/notebook/js/cell.js747
-rw-r--r--notebook/static/notebook/js/celltoolbar.js466
-rw-r--r--notebook/static/notebook/js/celltoolbarpresets/default.js51
-rw-r--r--notebook/static/notebook/js/celltoolbarpresets/example.js150
-rw-r--r--notebook/static/notebook/js/celltoolbarpresets/rawcell.js86
-rw-r--r--notebook/static/notebook/js/celltoolbarpresets/slideshow.js46
-rw-r--r--notebook/static/notebook/js/codecell.js569
-rw-r--r--notebook/static/notebook/js/codemirror-ipython.js38
-rw-r--r--notebook/static/notebook/js/codemirror-ipythongfm.js62
-rw-r--r--notebook/static/notebook/js/commandpalette.js187
-rw-r--r--notebook/static/notebook/js/completer.js412
-rw-r--r--notebook/static/notebook/js/contexthint.js98
-rw-r--r--notebook/static/notebook/js/kernelselector.js347
-rw-r--r--notebook/static/notebook/js/keyboardmanager.js231
-rw-r--r--notebook/static/notebook/js/main.js195
-rw-r--r--notebook/static/notebook/js/maintoolbar.js144
-rw-r--r--notebook/static/notebook/js/mathjaxutils.js212
-rw-r--r--notebook/static/notebook/js/menubar.js417
-rw-r--r--notebook/static/notebook/js/notebook.js3045
-rw-r--r--notebook/static/notebook/js/notificationarea.js342
-rw-r--r--notebook/static/notebook/js/outputarea.js966
-rw-r--r--notebook/static/notebook/js/pager.js185
-rw-r--r--notebook/static/notebook/js/quickhelp.js306
-rw-r--r--notebook/static/notebook/js/savewidget.js221
-rw-r--r--notebook/static/notebook/js/scrollmanager.js232
-rw-r--r--notebook/static/notebook/js/searchandreplace.js384
-rw-r--r--notebook/static/notebook/js/textcell.js383
-rw-r--r--notebook/static/notebook/js/toolbar.js137
-rw-r--r--notebook/static/notebook/js/tooltip.js322
-rw-r--r--notebook/static/notebook/js/tour.js172
-rw-r--r--notebook/static/notebook/less/ansicolors.less23
-rw-r--r--notebook/static/notebook/less/cell.less150
-rw-r--r--notebook/static/notebook/less/celltoolbar.less70
-rw-r--r--notebook/static/notebook/less/codecell.less48
-rw-r--r--notebook/static/notebook/less/codemirror.less52
-rw-r--r--notebook/static/notebook/less/commandpalette.less45
-rw-r--r--notebook/static/notebook/less/completer.less26
-rw-r--r--notebook/static/notebook/less/highlight-refs.less5
-rw-r--r--notebook/static/notebook/less/highlight.less112
-rw-r--r--notebook/static/notebook/less/kernelselector.less10
-rw-r--r--notebook/static/notebook/less/menubar.less81
-rw-r--r--notebook/static/notebook/less/notebook.less101
-rw-r--r--notebook/static/notebook/less/notificationarea.less74
-rw-r--r--notebook/static/notebook/less/notificationwidget.less21
-rw-r--r--notebook/static/notebook/less/outputarea.less209
-rw-r--r--notebook/static/notebook/less/pager.less69
-rw-r--r--notebook/static/notebook/less/quickhelp.less15
-rw-r--r--notebook/static/notebook/less/renderedhtml.less93
-rw-r--r--notebook/static/notebook/less/savewidget.less43
-rw-r--r--notebook/static/notebook/less/searchandreplace.less37
-rw-r--r--notebook/static/notebook/less/style.less19
-rw-r--r--notebook/static/notebook/less/style_noapp.less14
-rw-r--r--notebook/static/notebook/less/textcell.less72
-rw-r--r--notebook/static/notebook/less/toolbar.less59
-rw-r--r--notebook/static/notebook/less/tooltip.less158
-rw-r--r--notebook/static/notebook/less/variables.less27
-rw-r--r--notebook/static/services/config.js130
-rw-r--r--notebook/static/services/contents.js258
-rw-r--r--notebook/static/services/kernels/comm.js216
-rw-r--r--notebook/static/services/kernels/kernel.js1112
-rw-r--r--notebook/static/services/kernels/serialize.js126
-rw-r--r--notebook/static/services/sessions/session.js321
-rw-r--r--notebook/static/style/ipython.less12
-rw-r--r--notebook/static/style/style.less35
-rw-r--r--notebook/static/terminal/css/override.css7
-rw-r--r--notebook/static/terminal/js/main.js69
-rw-r--r--notebook/static/terminal/js/terminado.js41
-rw-r--r--notebook/static/terminal/less/terminal.less32
-rw-r--r--notebook/static/tree/js/kernellist.js96
-rw-r--r--notebook/static/tree/js/main.js177
-rw-r--r--notebook/static/tree/js/newnotebook.js108
-rw-r--r--notebook/static/tree/js/notebooklist.js908
-rw-r--r--notebook/static/tree/js/sessionlist.js86
-rw-r--r--notebook/static/tree/js/terminallist.js124
-rw-r--r--notebook/static/tree/less/altuploadform.less28
-rw-r--r--notebook/static/tree/less/style.less7
-rw-r--r--notebook/static/tree/less/tree.less327
-rw-r--r--notebook/templates/404.html5
-rw-r--r--notebook/templates/edit.html105
-rw-r--r--notebook/templates/error.html31
-rw-r--r--notebook/templates/login.html57
-rw-r--r--notebook/templates/logout.html43
-rw-r--r--notebook/templates/notebook.html353
-rw-r--r--notebook/templates/page.html167
-rw-r--r--notebook/templates/terminal.html64
-rw-r--r--notebook/templates/tree.html175
-rw-r--r--notebook/terminal/__init__.py32
-rw-r--r--notebook/terminal/api_handlers.py44
-rw-r--r--notebook/terminal/handlers.py34
-rw-r--r--notebook/tests/README.md28
-rw-r--r--notebook/tests/__init__.py0
-rw-r--r--notebook/tests/base/highlight.js58
-rw-r--r--notebook/tests/base/keyboard.js91
-rw-r--r--notebook/tests/base/misc.js21
-rw-r--r--notebook/tests/base/security.js57
-rw-r--r--notebook/tests/base/utils.js44
-rw-r--r--notebook/tests/launchnotebook.py161
-rw-r--r--notebook/tests/mockextension/index.js1
-rw-r--r--notebook/tests/notebook/buffering.js56
-rw-r--r--notebook/tests/notebook/clipboard_multiselect.js41
-rw-r--r--notebook/tests/notebook/deletecell.js107
-rw-r--r--notebook/tests/notebook/display_image.js63
-rw-r--r--notebook/tests/notebook/dualmode.js116
-rw-r--r--notebook/tests/notebook/dualmode_arrows.js51
-rw-r--r--notebook/tests/notebook/dualmode_cellinsert.js82
-rw-r--r--notebook/tests/notebook/dualmode_cellmode.js41
-rw-r--r--notebook/tests/notebook/dualmode_clipboard.js55
-rw-r--r--notebook/tests/notebook/dualmode_execute.js72
-rw-r--r--notebook/tests/notebook/dualmode_markdown.js37
-rw-r--r--notebook/tests/notebook/dualmode_merge.js198
-rw-r--r--notebook/tests/notebook/empty_arrow_keys.js21
-rw-r--r--notebook/tests/notebook/execute_code.js115
-rw-r--r--notebook/tests/notebook/execute_selected_cells.js178
-rw-r--r--notebook/tests/notebook/inject_js.js23
-rw-r--r--notebook/tests/notebook/interrupt.js45
-rw-r--r--notebook/tests/notebook/isolated_svg.js90
-rw-r--r--notebook/tests/notebook/markdown.js105
-rw-r--r--notebook/tests/notebook/merge_cells_api.js43
-rw-r--r--notebook/tests/notebook/move_multiselection.js64
-rw-r--r--notebook/tests/notebook/multiselect.js100
-rw-r--r--notebook/tests/notebook/multiselect_toggle.js70
-rw-r--r--notebook/tests/notebook/notifications.js116
-rw-r--r--notebook/tests/notebook/output.js126
-rw-r--r--notebook/tests/notebook/prompt_numbers.js36
-rw-r--r--notebook/tests/notebook/roundtrip.js247
-rw-r--r--notebook/tests/notebook/safe_append_output.js32
-rw-r--r--notebook/tests/notebook/save.js112
-rw-r--r--notebook/tests/notebook/shutdown.js49
-rw-r--r--notebook/tests/notebook/undelete.js118
-rw-r--r--notebook/tests/services/kernel.js325
-rw-r--r--notebook/tests/services/serialize.js127
-rw-r--r--notebook/tests/services/session.js180
-rw-r--r--notebook/tests/test_files.py152
-rw-r--r--notebook/tests/test_hist.sqlitebin0 -> 7168 bytes
-rw-r--r--notebook/tests/test_nbextensions.py496
-rw-r--r--notebook/tests/test_notebookapp.py119
-rw-r--r--notebook/tests/test_paths.py40
-rw-r--r--notebook/tests/test_serialize.py26
-rw-r--r--notebook/tests/test_serverextensions.py89
-rw-r--r--notebook/tests/test_utils.py81
-rw-r--r--notebook/tests/tree/dashboard_nav.js47
-rw-r--r--notebook/tests/util.js864
-rw-r--r--notebook/tree/__init__.py0
-rw-r--r--notebook/tree/handlers.py74
-rw-r--r--notebook/tree/tests/__init__.py0
-rw-r--r--notebook/tree/tests/test_tree_handler.py32
-rw-r--r--notebook/utils.py207
-rw-r--r--package.json20
-rw-r--r--scripts/jupyter-nbextension6
-rw-r--r--scripts/jupyter-notebook6
-rw-r--r--scripts/jupyter-serverextension6
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py198
-rw-r--r--setupbase.py563
-rw-r--r--tools/build-main.js68
-rw-r--r--tools/secure_notebook.py139
-rw-r--r--tools/tests/ANSI Test.ipynb560
-rw-r--r--tools/tests/CSS Reference.ipynb8193
-rw-r--r--tools/tests/Confined Output.ipynb307
-rw-r--r--tools/tests/Test Output Callbacks.ipynb291
391 files changed, 60797 insertions, 0 deletions
diff --git a/.bowerrc b/.bowerrc
new file mode 100644
index 0000000..b1c953d
--- /dev/null
+++ b/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "notebook/static/components"
+} \ No newline at end of file
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..9f370b7
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+.git
+.git*
+.mailmap
+.travis.yml
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fae28d6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,31 @@
+MANIFEST
+build
+dist
+_build
+docs/man/*.gz
+docs/source/api/generated
+docs/source/config.rst
+docs/gh-pages
+notebook/static/components
+notebook/static/style/*.min.css*
+notebook/static/*/js/main.min.js*
+node_modules
+*.py[co]
+__pycache__
+*.egg-info
+*~
+*.bak
+.ipynb_checkpoints
+.tox
+.DS_Store
+\#*#
+.#*
+.coverage
+src
+
+*.swp
+*.map
+.idea/
+Read the Docs
+config.rst
+
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.gitmodules
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..bd6544e
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,149 @@
+A. J. Holyoake <a.j.holyoake@gmail.com> ajholyoake <a.j.holyoake@gmail.com>
+Aaron Culich <aculich@gmail.com> Aaron Culich <aculich@eecs.berkeley.edu>
+Aron Ahmadia <aron@ahmadia.net> ahmadia <aron@ahmadia.net>
+Benjamin Ragan-Kelley <benjaminrk@gmail.com> <minrk@Mercury.local>
+Benjamin Ragan-Kelley <benjaminrk@gmail.com> Min RK
+Benjamin Ragan-Kelley <benjaminrk@gmail.com> MinRK <benjaminrk@gmail.com>
+Barry Wark <barrywark@gmail.com> Barry Wark <barrywarkatgmaildotcom>
+Ben Edwards <bedwards@cs.unm.edu> Ben Edwards <bedwards@sausage.(none)>
+Bradley M. Froehle <brad.froehle@gmail.com> Bradley M. Froehle <bfroehle@math.berkeley.edu>
+Bradley M. Froehle <brad.froehle@gmail.com> Bradley Froehle <brad.froehle@gmail.com>
+Brandon Parsons <brandon@parsonstx.com> Brandon Parsons <brandon.parsons@hp.com>
+Brian E. Granger <ellisonbg@gmail.com> Brian Granger
+Brian E. Granger <ellisonbg@gmail.com> Brian Granger <>
+Brian E. Granger <ellisonbg@gmail.com> bgranger <>
+Brian E. Granger <ellisonbg@gmail.com> bgranger <bgranger@red>
+Christoph Gohlke <cgohlke@uci.edu> cgohlke <cgohlke@uci.edu>
+Cyrille Rossant <cyrille.rossant@gmail.com> rossant <rossant@github>
+Damián Avila <damianavila82@yahoo.com.ar> damianavila <damianavila82@yahoo.com.ar>
+Damián Avila <damianavila82@yahoo.com.ar> damianavila <damianavila@gmail.com>
+Damon Allen <damontallen@gmail.com> damontallen <damontallen@gmail.com>
+Darren Dale <dsdale24@gmail.com> darren.dale <>
+Darren Dale <dsdale24@gmail.com> Darren Dale <>
+Dav Clark <davclark@berkeley.edu> Dav Clark <>
+Dav Clark <davclark@berkeley.edu> Dav Clark <davclark@gmail.com>
+David Hirschfeld <david.hirschfeld@gazprom-mt.com> dhirschfeld <david.hirschfeld@gazprom-mt.com>
+David P. Sanders <dpsanders@gmail.com> David P. Sanders <dpsanders@ciencias.unam.mx>
+David Warde-Farley <wardefar@iro.umontreal.ca> David Warde-Farley <>
+Doug Blank <dblank@cs.brynmawr.edu> Doug Blank <doug.blank@gmail.com>
+Eugene Van den Bulke <eugene.van-den-bulke@gmail.com> Eugene Van den Bulke <eugene.vandenbulke@gmail.com>
+Evan Patterson <epatters@enthought.com> <epatters@EPattersons-MacBook-Pro.local>
+Evan Patterson <epatters@enthought.com> <epatters@evan-laptop.localdomain>
+Evan Patterson <epatters@enthought.com> <epatters@caltech.edu>
+Evan Patterson <epatters@enthought.com> <ejpatters@gmail.com>
+Evan Patterson <epatters@enthought.com> epatters <ejpatters@gmail.com>
+Evan Patterson <epatters@enthought.com> epatters <epatters@enthought.com>
+Ernie French <ernestfrench@gmail.com> Ernie French <ernie@gqpbj.com>
+Ernie French <ernestfrench@gmail.com> ernie french <ernestfrench@gmail.com>
+Ernie French <ernestfrench@gmail.com> ernop <ernestfrench@gmail.com>
+Fernando Perez <Fernando.Perez@berkeley.edu> <fperez.net@gmail.com>
+Fernando Perez <Fernando.Perez@berkeley.edu> Fernando Perez <fernando.perez@berkeley.edu>
+Fernando Perez <Fernando.Perez@berkeley.edu> fperez <>
+Fernando Perez <Fernando.Perez@berkeley.edu> fptest <>
+Fernando Perez <Fernando.Perez@berkeley.edu> fptest1 <>
+Fernando Perez <Fernando.Perez@berkeley.edu> Fernando Perez <fernando.perez@berkeley.edu>
+Fernando Perez <fernando.perez@berkeley.edu> Fernando Perez <>
+Fernando Perez <fernando.perez@berkeley.edu> Fernando Perez <fperez@maqroll>
+Frank Murphy <fpmurphy@mtu.edu> Frank Murphy <fmurphy@arbor.net>
+Gabriel Becker <gmbecker@ucdavis.edu> gmbecker <gmbecker@ucdavis.edu>
+Gael Varoquaux <gael.varoquaux@normalesup.org> gael.varoquaux <>
+Gael Varoquaux <gael.varoquaux@normalesup.org> gvaroquaux <gvaroquaux@gvaroquaux-desktop>
+Gael Varoquaux <gael.varoquaux@normalesup.org> Gael Varoquaux <>
+Ingolf Becker <ingolf.becker@googlemail.com> watercrossing <ingolf.becker@googlemail.com>
+Jake Vanderplas <jakevdp@gmail.com> Jake Vanderplas <vanderplas@astro.washington.edu>
+Jakob Gager <jakob.gager@gmail.com> jakobgager <jakob.gager@gmail.com>
+Jakob Gager <jakob.gager@gmail.com> jakobgager <gager@ilsb.tuwien.ac.at>
+Jakob Gager <jakob.gager@gmail.com> jakobgager <jakobgager@hotmail.com>
+Jason Grout <jgrout6@bloomberg.net> <jason.grout@drake.edu>
+Jason Grout <jgrout6@bloomberg.net> <jason-github@creativetrax.com>
+Jason Gors <jason.gors.work@gmail.com> jason gors <jason.gors.work@gmail.com>
+Jason Gors <jason.gors.work@gmail.com> jgors <jason.gors.work@gmail.com>
+Jens Hedegaard Nielsen <jenshnielsen@gmail.com> Jens Hedegaard Nielsen <jhn@jhn-Znote.(none)>
+Jens Hedegaard Nielsen <jenshnielsen@gmail.com> Jens H Nielsen <jenshnielsen@gmail.com>
+Jens Hedegaard Nielsen <jenshnielsen@gmail.com> Jens H. Nielsen <jenshnielsen@gmail.com>
+Jez Ng <jezreel@gmail.com> Jez Ng <me@jezng.com>
+Jonathan Frederic <jdfreder@calpoly.edu> Jonathan Frederic <jonathan@LifebookMint.(none)>
+Jonathan Frederic <jdfreder@calpoly.edu> Jonathan Frederic <jon.freder@gmail.com>
+Jonathan Frederic <jdfreder@calpoly.edu> Jonathan Frederic <xh3xx.goose@gmail.com>
+Jonathan Frederic <jdfreder@calpoly.edu> jon <jon.freder@gmail.com>
+Jonathan Frederic <jdfreder@calpoly.edu> U-Jon-PC\Jon <Jon@Jon-PC.(none)>
+Jonathan March <jmarch@enthought.com> Jonathan March <JDM@MarchRay.net>
+Jonathan March <jmarch@enthought.com> jdmarch <JDM@marchRay.net>
+Jörgen Stenarson <jorgen.stenarson@kroywen.se> Jörgen Stenarson <jorgen.stenarson@bostream.nu>
+Jörgen Stenarson <jorgen.stenarson@kroywen.se> Jorgen Stenarson <jorgen.stenarson@bostream.nu>
+Jörgen Stenarson <jorgen.stenarson@kroywen.se> Jorgen Stenarson <>
+Jörgen Stenarson <jorgen.stenarson@kroywen.se> jstenar <jorgen.stenarson@bostream.nu>
+Jörgen Stenarson <jorgen.stenarson@kroywen.se> jstenar <>
+Jörgen Stenarson <jorgen.stenarson@kroywen.se> Jörgen Stenarson <jorgen.stenarson@kroywen.se>
+Juergen Hasch <python@elbonia.de> juhasch <python@elbonia.de>
+Juergen Hasch <python@elbonia.de> juhasch <hasch@VMBOX.fritz.box>
+Julia Evans <julia@jvns.ca> Julia Evans <julia@stripe.com>
+Kester Tong <kestert@google.com> KesterTong <kestert@google.com>
+Kyle Kelley <rgbkrk@gmail.com> Kyle Kelley <kyle.kelley@rackspace.com>
+Kyle Kelley <rgbkrk@gmail.com> rgbkrk <rgbkrk@gmail.com>
+Laurent Dufréchou <laurent.dufrechou@gmail.com> <laurent.dufrechou@gmail.com>
+Laurent Dufréchou <laurent.dufrechou@gmail.com> <laurent@Pep>
+Laurent Dufréchou <laurent.dufrechou@gmail.com> laurent dufrechou <>
+Laurent Dufréchou <laurent.dufrechou@gmail.com> laurent.dufrechou <>
+Laurent Dufréchou <laurent.dufrechou@gmail.com> Laurent Dufrechou <>
+Laurent Dufréchou <laurent.dufrechou@gmail.com> laurent.dufrechou@gmail.com <>
+Laurent Dufréchou <laurent.dufrechou@gmail.com> ldufrechou <ldufrechou@PEP>
+Lorena Pantano <lorena.pantano@gmail.com> Lorena <lorena.pantano@gmail.com>
+Luis Pedro Coelho <luis@luispedro.org> Luis Pedro Coelho <lpc@cmu.edu>
+Marc Molla <marcmolla@gmail.com> marcmolla <marcmolla@gmail.com>
+Martín Gaitán <gaitan@gmail.com> Martín Gaitán <gaitan@phasety.com>
+Matthias Bussonnier <bussonniermatthias@gmail.com> Matthias BUSSONNIER <bussonniermatthias@gmail.com>
+Matthias Bussonnier <bussonniermatthias@gmail.com> Bussonnier Matthias <bussonniermatthias@gmail.com>
+Matthias Bussonnier <bussonniermatthias@gmail.com> Matthias BUSSONNIER <bussonniermatthias@umr168-curn-1-24x-6561.curie.fr>
+Matthias Bussonnier <bussonniermatthias@gmail.com> Matthias Bussonnier <carreau@Aspire.(none)>
+Michael Droettboom <mdboom@gmail.com> Michael Droettboom <mdroe@stsci.edu>
+Nicholas Bollweg <nick.bollweg@gmail.com> Nicholas Bollweg (Nick) <nick.bollweg@gmail.com>
+Nicolas Rougier <Nicolas.Rougier@inria.fr> <Nicolas.rougier@inria.fr>
+Nikolay Koldunov <koldunovn@gmail.com> Nikolay Koldunov <nikolay.koldunov@zmaw.de>
+Omar Andrés Zapata Mesa <andresete.chaos@gmail.com> Omar Andres Zapata Mesa <andresete.chaos@gmail.com>
+Omar Andrés Zapata Mesa <andresete.chaos@gmail.com> Omar Andres Zapata Mesa <omazapa@tuxhome>
+Pankaj Pandey <pankaj86@gmail.com> Pankaj Pandey <pankaj@enthought.com>
+Pascal Schetelat <pascal.schetelat@gmail.com> pascal-schetelat <pascal.schetelat@gmail.com>
+Paul Ivanov <pi@berkeley.edu> Paul Ivanov <pivanov314@gmail.com>
+Pauli Virtanen <pauli.virtanen@iki.fi> Pauli Virtanen <>
+Pauli Virtanen <pauli.virtanen@iki.fi> Pauli Virtanen <pav@iki.fi>
+Pierre Gerold <pierre.gerold@laposte.net> Pierre Gerold <gerold@crans.org>
+Pietro Berkes <pberkes@enthought.com> Pietro Berkes <pietro.berkes@googlemail.com>
+Piti Ongmongkolkul <piti118@gmail.com> piti118 <piti118@gmail.com>
+Prabhu Ramachandran <prabhu@enthought.com> Prabhu Ramachandran <>
+Puneeth Chaganti <punchagan@gmail.com> Puneeth Chaganti <punchagan@muse-amuse.in>
+Robert Kern <robert.kern@gmail.com> rkern <>
+Robert Kern <robert.kern@gmail.com> Robert Kern <rkern@enthought.com>
+Robert Kern <robert.kern@gmail.com> Robert Kern <rkern@Sacrilege.local>
+Robert Kern <robert.kern@gmail.com> Robert Kern <>
+Robert Marchman <bo.marchman@gmail.com> Robert Marchman <robert.l.marchman@dartmouth.edu>
+Satrajit Ghosh <satra@mit.edu> Satrajit Ghosh <satra@ba5.mit.edu>
+Satrajit Ghosh <satra@mit.edu> Satrajit Ghosh <satrajit.ghosh@gmail.com>
+Scott Sanderson <scoutoss@gmail.com> Scott Sanderson <ssanderson@quantopian.com>
+smithj1 <smithj1@LMC-022896.local> smithj1 <smithj1@LMC-022896.swisscom.com>
+smithj1 <smithj1@LMC-022896.local> smithj1 <smithj1@lmc-022896.local>
+Steven Johnson <steven.johnson@drake.edu> stevenJohnson <steven.johnson@drake.edu>
+Steven Silvester <steven.silvester@ieee.org> blink1073 <steven.silvester@ieee.org>
+S. Weber <s8weber@c4.usr.sh> s8weber <s8weber@c5.usr.sh>
+Stefan van der Walt <stefan@sun.ac.za> Stefan van der Walt <bzr@mentat.za.net>
+Silvia Vinyes <silvia.vinyes@gmail.com> Silvia <silvia@silvia-U44SG.(none)>
+Silvia Vinyes <silvia.vinyes@gmail.com> silviav12 <silvia.vinyes@gmail.com>
+Sylvain Corlay <scorlay@bloomberg.net> <sylvain.corlay@gmail.com>
+Sylvain Corlay <scorlay@bloomberg.net> sylvain.corlay <sylvain.corlay@gmail.com>
+Ted Drain <ted.drain@gmail.com> TD22057 <ted.drain@gmail.com>
+Théophile Studer <theo.studer@gmail.com> Théophile Studer <studer@users.noreply.github.com>
+Thomas Kluyver <takowl@gmail.com> Thomas <takowl@gmail.com>
+Thomas Spura <tomspur@fedoraproject.org> Thomas Spura <thomas.spura@gmail.com>
+Timo Paulssen <timonator@perpetuum-immobile.de> timo <timonator@perpetuum-immobile.de>
+vds <vds@VIVIAN> vds2212 <vds2212@VIVIAN>
+vds <vds@VIVIAN> vds <vds@vivian>
+Ville M. Vainio <vivainio@gmail.com> <vivainio2@WN-W0941>
+Ville M. Vainio <vivainio@gmail.com> ville <ville@VILLE-PC>
+Ville M. Vainio <vivainio@gmail.com> ville <ville@ville-desktop>
+Ville M. Vainio <vivainio@gmail.com> vivainio <>
+Ville M. Vainio <vivainio@gmail.com> Ville M. Vainio <vivainio@villev>
+Ville M. Vainio <vivainio@gmail.com> Ville M. Vainio <vivainio@ville_vmw>
+Walter Doerwald <walter@livinglogic.de> walter.doerwald <>
+Walter Doerwald <walter@livinglogic.de> Walter Doerwald <>
+W. Trevor King <wking@tremily.us> W. Trevor King <wking@drexel.edu>
+Yoval P. <yoval@gmx.com> y-p <yoval@gmx.com>
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..caa32c7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,34 @@
+# http://travis-ci.org/#!/ipython/ipython
+language: python
+group: edge
+cache:
+ directories:
+ - ~/.cache/bower
+python:
+ - 3.5
+ - 2.7
+sudo: false
+env:
+ global:
+ - PATH=$TRAVIS_BUILD_DIR/pandoc:$PATH
+ matrix:
+ - GROUP=python
+ - GROUP=js/base
+ - GROUP=js/notebook
+ - GROUP=js/services
+ - GROUP=js/tree
+before_install:
+ - 'if [[ $GROUP == js* ]]; then npm install -g casperjs@1.1.1; fi'
+ - git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
+install:
+ - pip install -f travis-wheels/wheelhouse file://$PWD#egg=notebook[test]
+script:
+ - 'if [[ $GROUP == js* ]]; then python -m notebook.jstest ${GROUP:3}; fi'
+ - 'if [[ $GROUP == python ]]; then nosetests --with-coverage --cover-package=notebook notebook; fi'
+matrix:
+ include:
+ - python: 3.3
+ env: GROUP=python
+ - python: 3.4
+ env: GROUP=python
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..f6028b6
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,69 @@
+# Contributing
+
+We follow the [IPython Contributing Guide](https://github.com/ipython/ipython/blob/master/CONTRIBUTING.md).
+
+For development installation instructions, see the [README](README.md#dev-quickstart).
+
+## Managing static files
+
+The notebook relies on static dependencies (which are installed with Bower), its own minified JavaScript, and CSS compiled from LESS.
+
+### Static dependencies
+
+To install the static dependencies, run:
+
+ python setup.py jsdeps
+
+You can also pass a `-f` flag to this command to force Bower to run, if necessary.
+
+### Minified JavaScript
+
+To compile the minified JavaScript, run:
+
+ python setup.py js
+
+You can also run the notebook server without relying on the minified JavaScript by passing the `--NotebookApp.ignore_minified_js=True` flag when launching the notebook.
+
+### Compiling CSS
+
+To compile the CSS from LESS, run:
+
+ python setup.py css
+
+### Git hooks
+
+If you want to automatically update dependencies, recompile the JavaScript, and recompile the CSS after checking out a new commit, you can install post-checkout and post-merge hooks which will do it for you:
+
+ ./git-hooks/install-hooks.sh
+
+See the [git-hooks readme](git-hooks/README.md) for more details.
+
+## Running tests
+
+### JavaScript tests
+
+To run the JavaScript tests, you will need to have PhantomJS and CasperJS installed:
+
+ npm install -g casperjs phantomjs@1.9.18
+
+Then, to run the JavaScript tests:
+
+ python -m notebook.jstest [group]
+
+where `[group]` is an optional argument that is a path relative to `notebook/tests`. For example, to run all tests in `notebook/tests/notebook`:
+
+ python -m notebook.jstest notebook
+
+or to run just `notebook/tests/notebook/deletecell.js`:
+
+ python -m notebook.jstest notebook/deletecell.js
+
+### Python tests
+
+To run Python tests, run:
+
+ nosetests notebook
+
+If you want coverage statistics as well, you can run:
+
+ nosetests --with-coverage --cover-package=notebook notebook
diff --git a/COPYING.md b/COPYING.md
new file mode 100644
index 0000000..bd6397d
--- /dev/null
+++ b/COPYING.md
@@ -0,0 +1,60 @@
+# Licensing terms
+
+This project is licensed under the terms of the Modified BSD License
+(also known as New or Revised or 3-Clause BSD), as follows:
+
+- Copyright (c) 2001-2015, IPython Development Team
+- Copyright (c) 2015-, Jupyter Development Team
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of the Jupyter Development Team nor the names of its
+contributors may be used to endorse or promote products derived from this
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+## About the Jupyter Development Team
+
+The Jupyter Development Team is the set of all contributors to the Jupyter project.
+This includes all of the Jupyter subprojects.
+
+The core team that coordinates development on GitHub can be found here:
+https://github.com/jupyter/.
+
+## Our Copyright Policy
+
+Jupyter uses a shared copyright model. Each contributor maintains copyright
+over their contributions to Jupyter. But, it is important to note that these
+contributions are typically only changes to the repositories. Thus, the Jupyter
+source code, in its entirety is not the copyright of any single person or
+institution. Instead, it is the collective copyright of the entire Jupyter
+Development Team. If individual contributors want to maintain a record of what
+changes/contributions they have specific copyright on, they should indicate
+their copyright in the commit message of the change, when they commit the
+change to one of the Jupyter repositories.
+
+With this in mind, the following banner should be used in any source code file
+to indicate the copyright and license terms:
+
+ # Copyright (c) Jupyter Development Team.
+ # Distributed under the terms of the Modified BSD License.
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..80f3d03
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,108 @@
+# Installs Jupyter Notebook and IPython kernel from the current branch
+# Another Docker container should inherit with `FROM jupyter/notebook`
+# to run actual services.
+
+FROM ubuntu:14.04
+
+MAINTAINER Project Jupyter <jupyter@googlegroups.com>
+
+# Not essential, but wise to set the lang
+# Note: Users with other languages should set this in their derivative image
+ENV LANGUAGE en_US.UTF-8
+ENV LANG en_US.UTF-8
+ENV LC_ALL en_US.UTF-8
+ENV PYTHONIOENCODING UTF-8
+
+# Remove preinstalled copy of python that blocks our ability to install development python.
+RUN DEBIAN_FRONTEND=noninteractive apt-get remove -yq \
+ python3-minimal \
+ python3.4 \
+ python3.4-minimal \
+ libpython3-stdlib \
+ libpython3.4-stdlib \
+ libpython3.4-minimal
+
+# Python binary and source dependencies
+RUN apt-get update -qq && \
+ DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
+ build-essential \
+ ca-certificates \
+ curl \
+ git \
+ language-pack-en \
+ libcurl4-openssl-dev \
+ libffi-dev \
+ libsqlite3-dev \
+ libzmq3-dev \
+ pandoc \
+ python \
+ python3 \
+ python-dev \
+ python3-dev \
+ sqlite3 \
+ texlive-fonts-recommended \
+ texlive-latex-base \
+ texlive-latex-extra \
+ zlib1g-dev && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install Tini
+RUN curl -L https://github.com/krallin/tini/releases/download/v0.6.0/tini > tini && \
+ echo "d5ed732199c36a1189320e6c4859f0169e950692f451c03e7854243b95f4234b *tini" | sha256sum -c - && \
+ mv tini /usr/local/bin/tini && \
+ chmod +x /usr/local/bin/tini
+
+# Install the recent pip release
+RUN curl -O https://bootstrap.pypa.io/get-pip.py && \
+ python2 get-pip.py && \
+ python3 get-pip.py && \
+ rm get-pip.py && \
+ pip2 --no-cache-dir install requests[security] && \
+ pip3 --no-cache-dir install requests[security] && \
+ rm -rf /root/.cache
+
+# Install some dependencies.
+RUN pip2 --no-cache-dir install ipykernel && \
+ pip3 --no-cache-dir install ipykernel && \
+ \
+ python2 -m ipykernel.kernelspec && \
+ python3 -m ipykernel.kernelspec && \
+ rm -rf /root/.cache
+
+# Move notebook contents into place.
+ADD . /usr/src/jupyter-notebook
+
+# Install dependencies and run tests.
+RUN BUILD_DEPS="nodejs-legacy npm" && \
+ apt-get update -qq && \
+ DEBIAN_FRONTEND=noninteractive apt-get install -yq $BUILD_DEPS && \
+ \
+ pip3 install --no-cache-dir /usr/src/jupyter-notebook && \
+ pip3 install ipywidgets && \
+ \
+ npm cache clean && \
+ apt-get clean && \
+ rm -rf /root/.npm && \
+ rm -rf /root/.cache && \
+ rm -rf /root/.config && \
+ rm -rf /root/.local && \
+ rm -rf /root/tmp && \
+ rm -rf /var/lib/apt/lists/* && \
+ apt-get purge -y --auto-remove \
+ -o APT::AutoRemove::RecommendsImportant=false -o APT::AutoRemove::SuggestsImportant=false $BUILD_DEPS
+
+# Run tests.
+RUN pip3 install --no-cache-dir notebook[test] && nosetests notebook
+
+# Add a notebook profile.
+RUN mkdir -p -m 700 /root/.jupyter/ && \
+ echo "c.NotebookApp.ip = '*'" >> /root/.jupyter/jupyter_notebook_config.py
+
+VOLUME /notebooks
+WORKDIR /notebooks
+
+EXPOSE 8888
+
+ENTRYPOINT ["tini", "--"]
+CMD ["jupyter", "notebook"]
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..6e377a7
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,28 @@
+include COPYING.md
+include CONTRIBUTING.md
+include README.md
+include package.json
+include bower.json
+include .bowerrc
+include setupbase.py
+include Dockerfile
+graft tools
+
+# Documentation
+graft docs
+exclude docs/\#*
+
+# Examples
+graft examples
+
+# docs subdirs we want to skip
+prune docs/build
+prune docs/gh-pages
+prune docs/dist
+
+# Patterns to exclude from any directory
+global-exclude *~
+global-exclude *.pyc
+global-exclude *.pyo
+global-exclude .git
+global-exclude .ipynb_checkpoints
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8fa416b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,128 @@
+# Jupyter Notebook
+
+[![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter)
+[![Build Status](https://travis-ci.org/jupyter/notebook.svg?branch=master)](https://travis-ci.org/jupyter/notebook)
+[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](http://jupyter-notebook.readthedocs.org/en/latest/?badge=latest)
+
+The Jupyter notebook is a web-based notebook environment for interactive
+computing.
+
+![Jupyter notebook example](docs/resources/running_code_med.png "Jupyter notebook example")
+
+### Jupyter notebook, the language-agnostic evolution of IPython notebook
+Jupyter notebook is the language-agnostic HTML notebook application for
+Project Jupyter. In 2015, Jupyter notebook was released as part of
+The Big Split™ of the IPython codebase. IPython 3 was the last major monolithic
+release containing both language-agnostic code, such as the *IPython notebook*,
+and language specific code, such as the *IPython kernel for Python*. As
+computing spans many languages, Project Jupyter will continue to develop the
+language-agnostic **Jupyter notebook** in this repo and with the help of the
+community develop language specific kernels which are found in their own
+discrete repos.
+[[The Big Split™ announcement](https://blog.jupyter.org/2015/04/15/the-big-split/)]
+[[Jupyter Ascending blog post](http://blog.jupyter.org/2015/08/12/first-release-of-jupyter/)]
+
+## Installation
+You can find the installation documentation for the
+[Jupyter platform, on ReadTheDocs](http://jupyter.readthedocs.org/en/latest/install.html).
+The documentation for advanced usage of Jupyter notebook can be found
+[here](http://jupyter-notebook.readthedocs.org/en/latest).
+
+For a local installation, make sure you have
+[pip installed](https://pip.readthedocs.org/en/stable/installing/) and run:
+
+ $ pip install notebook
+
+## Usage - Running Jupyter notebook
+
+### Running in a local installation
+
+Launch with:
+
+ $ jupyter notebook
+
+### Running in a Docker container
+
+If you are using **Linux** and have a
+[Docker daemon running](https://docs.docker.com/installation/),
+e.g. reachable on `localhost`, start a container with:
+
+ $ docker run --rm -it -p 8888:8888 -v "$(pwd):/notebooks" jupyter/notebook
+
+In your browser, open the URL `http://localhost:8888/`.
+All notebooks from your session will be saved in the current directory.
+
+On other platforms, such as **Windows and OS X**, that use
+[`docker-machine`](https://docs.docker.com/machine/install-machine/) with `docker`, a container can be started using
+`docker-machine`. In the browser, open the URL `http://ip:8888/` where `ip` is
+the IP address returned from the command [`docker-machine ip <MACHINE>`](https://docs.docker.com/machine/reference/ip/):
+
+ $ docker-machine ip <MACHINE>
+
+For example,
+
+ $ docker-machine ip myjupytermachine
+ 192.168.99.104
+
+In browser, open `http://192.168.99.104:8888`.
+
+NOTE: With the deprecated `boot2docker`, use the command `boot2docker ip` to
+determine the URL.
+
+## Development Installation Quickstart
+Detailed [Developer Documentation](http://jupyter-notebook.readthedocs.org/en/latest)
+is available on ReadTheDocs.
+
+* Ensure that you have node/npm installed (e.g. `brew install node` on OS X)
+* Clone this repo and `cd` into it
+* `pip install --pre -e .`
+
+NOTE: For **Debian/Ubuntu** systems, if you're installing the system node you
+need to use the 'nodejs-legacy' package and not the 'node' package.
+
+For more detailed development install instructions (e.g. recompiling javascript
+and css, running tests), see the
+[Developer Documentation](http://jupyter-notebook.readthedocs.org/en/latest)
+on ReadTheDocs and the [contributing guide](CONTRIBUTING.md).
+
+### Ubuntu Trusty
+
+```
+sudo apt-get install nodejs-legacy npm python-virtualenv python-dev
+# ensure setuptools/pip are up-to-date
+pip install --upgrade setuptools pip
+git clone https://github.com/jupyter/notebook.git
+cd notebook
+pip install --pre -e .
+jupyter notebook
+```
+
+### FreeBSD
+
+```
+cd /usr/ports/www/npm
+sudo make install # (Be sure to select the "NODE" option)
+cd /usr/ports/devel/py-pip
+sudo make install
+cd /usr/ports/devel/py-virtualenv
+sudo make install
+cd /usr/ports/shells/bash
+sudo make install
+mkdir -p ~/.virtualenvs
+python2.7 -m virtualenv ~/.virtualenvs/notebook
+bash
+source ~/.virtualenvs/notebook/bin/activate
+pip install --upgrade setuptools pip pycurl
+git clone https://github.com/jupyter/notebook.git
+cd notebook
+pip install -r requirements.txt -e .
+jupyter notebook
+```
+
+## Resources
+- [Project Jupyter website](https://jupyter.org)
+- [Online Demo at try.jupyter.org](https://try.jupyter.org)
+- [Documentation for Jupyter notebook](http://jupyter-notebook.readthedocs.org/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyter-notebook/latest/jupyter-notebook.pdf)]
+- [Documentation for Project Jupyter](http://jupyter.readthedocs.org/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)]
+- [Issues](https://github.com/jupyter/notebook/issues)
+- [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter)
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..114b4ab
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,23 @@
+{
+ "name": "jupyter-notebook-deps",
+ "version": "0.0.1",
+ "dependencies": {
+ "backbone": "components/backbone#~1.2",
+ "bootstrap": "components/bootstrap#~3.3",
+ "bootstrap-tour": "0.9.0",
+ "codemirror": "~5.8",
+ "es6-promise": "~1.0",
+ "font-awesome": "components/font-awesome#~4.2.0",
+ "google-caja": "5669",
+ "jquery": "components/jquery#~2.0",
+ "jquery-ui": "components/jqueryui#~1.10",
+ "marked": "~0.3",
+ "MathJax": "components/MathJax#~2.6",
+ "moment": "~2.8.4",
+ "requirejs": "~2.1",
+ "term.js": "chjj/term.js#~0.0.7",
+ "text-encoding": "~0.1",
+ "underscore": "components/underscore#~1.5",
+ "jquery-typeahead": "~2.0.0"
+ }
+}
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..0722c49
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,201 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext ipynb2rst
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " applehelp to make an Apple Help Book"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " coverage to run coverage check of the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+ rm -rf config.rst
+
+html: source/config.rst ipynb2rst
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+source/config.rst:
+ python3 autogen_config.py
+ @echo "Created docs for config options"
+
+ipynb2rst:
+ jupyter nbconvert --to rst source/examples/Notebook/*.ipynb --template=source/template --FilesWriter.build_directory=source/examples/Notebook/rstversions
+ @echo "Converted notebooks to rst"
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/JupyterNotebook.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/JupyterNotebook.qhc"
+
+applehelp:
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+ @echo
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+ @echo "N.B. You won't be able to view it unless you put it in" \
+ "~/Library/Documentation/Help or install it in your application" \
+ "bundle."
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/JupyterNotebook"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/JupyterNotebook"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+coverage:
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+ @echo "Testing of coverage in the sources finished, look at the " \
+ "results in $(BUILDDIR)/coverage/python.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/autogen_config.py b/docs/autogen_config.py
new file mode 100644
index 0000000..f06efe9
--- /dev/null
+++ b/docs/autogen_config.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+import os
+from notebook.notebookapp import NotebookApp
+
+header = """\
+.. _config:
+
+
+Config file and command line options
+====================================
+
+The notebook server can be run with a variety of command line arguments.
+A list of available options can be found below in the :ref:`options section
+<options>`.
+
+Defaults for these options can also be set by creating a file named
+``jupyter_notebook_config.py`` in your Jupyter folder. The Jupyter
+folder is in your home directory, ``~/.jupyter``.
+
+To create a ``jupyter_notebook_config.py`` file, with all the defaults
+commented out, you can use the following command line::
+
+ $ jupyter notebook --generate-config
+
+
+.. _options:
+
+Options
+-------
+
+This list of options can be generated by running the following and hitting
+enter::
+
+ $ jupyter notebook --help
+
+"""
+try:
+ destination = os.path.join(os.path.dirname(__file__), 'source/config.rst')
+except:
+ destination = os.path.join(os.getcwd(), 'config.rst')
+
+with open(destination, 'w') as f:
+ f.write(header)
+ f.write(NotebookApp().document_config_options())
diff --git a/docs/jsdoc_config.json b/docs/jsdoc_config.json
new file mode 100644
index 0000000..4da2e3d
--- /dev/null
+++ b/docs/jsdoc_config.json
@@ -0,0 +1,21 @@
+{
+ "markdown": {
+ "parser": "gfm"
+ },
+ "plugins": [
+ "plugins/markdown" ,
+ "jsdoc_plugin.js"
+ ],
+ "source": {
+ "include": [
+ "../notebook/static/notebook/js/notebook.js"
+ ]
+ },
+ "tags": {
+ "allowUnknownTags": true
+ },
+ "templates": {
+ "cleverLinks": false,
+ "monospaceLinks": false
+ }
+}
diff --git a/docs/jsdoc_plugin.js b/docs/jsdoc_plugin.js
new file mode 100644
index 0000000..3fa3035
--- /dev/null
+++ b/docs/jsdoc_plugin.js
@@ -0,0 +1,12 @@
+exports.handlers = {
+ newDoclet: function(e) {
+ // e.doclet will refer to the newly created doclet
+ // you can read and modify properties of that doclet if you wish
+ if (typeof e.doclet.name === 'string') {
+ if (e.doclet.name[0] == '_') {
+ console.log('Private method "' + e.doclet.longname + '" not documented.');
+ e.doclet.memberof = '<anonymous>';
+ }
+ }
+ }
+}; \ No newline at end of file
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..741c9f6
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,263 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
+set I18NSPHINXOPTS=%SPHINXOPTS% source
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ echo. coverage to run coverage check of the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+REM Check if sphinx-build is available and fallback to Python version if any
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 goto sphinx_python
+goto sphinx_ok
+
+:sphinx_python
+
+set SPHINXBUILD=python -m sphinx.__init__
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+:sphinx_ok
+
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\JupyterNotebook.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\JupyterNotebook.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "coverage" (
+ %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of coverage in the sources finished, look at the ^
+results in %BUILDDIR%/coverage/python.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..32ca6a6
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,11 @@
+jupyter
+sphinx_rtd_theme
+jinja2
+tornado
+-e git+https://github.com/ipython/ipython_genutils.git#egg=ipython_genutils
+-e git+https://github.com/ipython/traitlets.git#egg=traitlets
+-e git+https://github.com/jupyter/jupyter_core.git#egg=jupyter_core
+-e git+https://github.com/jupyter/nbformat.git#egg=nbformat
+-e git+https://github.com/jupyter/jupyter_client.git#egg=jupyter_client
+-e git+https://github.com/ipython/ipython.git#egg=ipython
+-e git+https://github.com/ipython/ipykernel.git#egg=ipykernel
diff --git a/docs/resources/Info.plist.example b/docs/resources/Info.plist.example
new file mode 100644
index 0000000..a6c7e17
--- /dev/null
+++ b/docs/resources/Info.plist.example
@@ -0,0 +1,20 @@
+ # Add this into the info.plist file of an application
+ # and the icns icon in Contents/Resources
+ # then move the application twice :
+ # http://superuser.com/questions/178316/how-to-set-an-icon-for-a-file-type-on-mac
+
+ <key>CFBundleDocumentTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeExtensions</key>
+ <array>
+ <string>ipynb</string>
+ </array>
+ <key>CFBundleTypeIconFile</key>
+ <string>ipynb_mac_icon</string>
+ <key>CFBundleTypeName</key>
+ <string>IPython notebook file</string>
+ <key>CFBundleTypeRole</key>
+ <string>None</string>
+ </dict>
+ <array>
diff --git a/docs/resources/generate_icons.sh b/docs/resources/generate_icons.sh
new file mode 100755
index 0000000..a72b2d8
--- /dev/null
+++ b/docs/resources/generate_icons.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+INKSCAPE=inkscape
+
+${INKSCAPE} -z -C --file=ipynb_icon_16x16.svg --export-png=ipynb_icon_16x16_uncrush.png
+${INKSCAPE} -z -C --file=ipynb_icon_24x24.svg --export-png=ipynb_icon_24x24_uncrush.png
+${INKSCAPE} -z -C --file=ipynb_icon_32x32.svg --export-png=ipynb_icon_32x32_uncrush.png
+${INKSCAPE} -z -C --file=ipynb_icon_512x512.svg --export-png=ipynb_icon_64x64_uncrush.png -w 64 -h 64
+${INKSCAPE} -z -C --file=ipynb_icon_512x512.svg --export-png=ipynb_icon_128x128_uncrush.png -w 128 -h 128
+${INKSCAPE} -z -C --file=ipynb_icon_512x512.svg --export-png=ipynb_icon_256x256_uncrush.png -w 256 -h 256
+${INKSCAPE} -z -C --file=ipynb_icon_512x512.svg --export-png=ipynb_icon_512x512_uncrush.png -w 512 -h 512
+
+
+for file in `ls *_uncrush.png`; do
+ pngcrush -brute -l 9 -reduce -rem alla -rem text -rem time -rem gAMA -rem cHRM -rem iCCP -rem sRGB $file `basename $file _uncrush.png`.png
+ rm $file
+done
diff --git a/docs/resources/icon_16x16.svg b/docs/resources/icon_16x16.svg
new file mode 100644
index 0000000..29145e8
--- /dev/null
+++ b/docs/resources/icon_16x16.svg
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="ipynb_icon_16x16.svg"
+ width="16"
+ height="16"
+ inkscape:export-filename="/Users/bussonniermatthias/dev/ipython/docs/resources/ipynb_icon_16x16.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <metadata
+ id="metadata371">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ inkscape:window-height="755"
+ inkscape:window-width="1343"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ inkscape:zoom="32.924658"
+ inkscape:cx="4.028552"
+ inkscape:cy="6.0286893"
+ inkscape:window-x="67"
+ inkscape:window-y="65"
+ inkscape:current-layer="text4040"
+ width="210mm"
+ height="40mm"
+ units="px"
+ showgrid="true"
+ inkscape:window-maximized="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid3926"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4689">
+ <stop
+ id="stop4157"
+ offset="0"
+ style="stop-color:#5a9fd4;stop-opacity:1;" />
+ <stop
+ id="stop4159"
+ offset="1"
+ style="stop-color:#306998;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4161">
+ <stop
+ style="stop-color:#fab434;stop-opacity:1"
+ offset="0"
+ id="stop4691" />
+ <stop
+ style="stop-color:#fb9143;stop-opacity:1"
+ offset="1"
+ id="stop4693" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4161"
+ id="linearGradient3941"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.07938588,0,0,0.07938588,-5.0860229,1.6938337)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4161"
+ id="linearGradient3943"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.06968992,0,0,0.06968992,-2.9157072,3.1465468)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689"
+ id="linearGradient3945"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.06968992,0,0,0.06968992,-2.9157072,3.1465468)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ </defs>
+ <path
+ style="fill:#ffffff;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 2,15 2,1 11,1 14,4 14,15 2,15"
+ id="path3956"
+ inkscape:connector-curvature="0" />
+ <path
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#7d7d7d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99363834;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+ d="m 1,0 0,16 14,0 0.03125,-12.4375 -3.6875,-3.53125 z m 9,1 0,4 4,0 0,10 L 2,15 2,1 z m 1,0 3,3 -3,0 z"
+ id="path4338"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccccccc" />
+ <g
+ style="font-size:9.22902393px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3945);fill-opacity:1;stroke:none;font-family:Monospace;-inkscape-font-specification:Monospace"
+ id="text4040"
+ transform="translate(0,-1)">
+ <path
+ d="M 6.3936117,14 4,7.4375 4,14 3,14 3,6 4.4316382,6 7,13 7,6 8,6 8,14"
+ style="fill:url(#linearGradient3943);font-family:Droid Sans;-inkscape-font-specification:Droid Sans;fill-opacity:1.0"
+ id="path3937"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccc" />
+ <path
+ d="m 11.611865,8.3182664 c 0.321685,5.7e-6 0.614285,0.061605 0.8778,0.1847998 0.263506,0.1232054 0.487661,0.3080051 0.672466,0.5543996 0.188217,0.2429824 0.333661,0.5458484 0.436333,0.9085998 0.102658,0.3627584 0.153994,0.7836904 0.154,1.2627984 -6e-6,0.482536 -0.05134,0.90689 -0.154,1.273066 -0.10267,0.362757 -0.248116,0.667334 -0.436333,0.913732 -0.184805,0.246401 -0.40896,0.432912 -0.672466,0.559533 -0.263515,0.1232 -0.556115,0.1848 -0.8778,0.1848 -0.201914,0 -0.385003,-0.02225 -0.549265,-0.06673 -0.16427,-0.04449 -0.313137,-0.10267 -0.446601,-0.174534 -0.130046,-0.07528 -0.246402,-0.162555 -0.349066,-0.2618 -0.09924,-0.09924 -0.188224,-0.203622 -0.2669332,-0.313133 L 10,13.454283 10,14 9,14 9,6 l 1,0 -4e-6,3.1601324 c 0.078718,-0.1197727 0.167691,-0.2309947 0.266937,-0.3336664 0.09924,-0.1026607 0.213887,-0.1916389 0.343933,-0.2669332 0.133464,-0.075283 0.28233,-0.1334609 0.4466,-0.1745332 0.164263,-0.044483 0.349063,-0.066728 0.554399,-0.066734 M 11.4322,9.0933982 c -0.273781,5.1e-6 -0.50307,0.044493 -0.687868,0.1334667 -0.181379,0.08556 -0.328535,0.2173155 -0.441466,0.3952668 -0.109516,0.1779595 -0.188223,0.4004033 -0.236133,0.6673323 -0.04449,0.266937 -0.06673,0.58007 -0.06673,0.9394 -10e-7,0.345646 0.02225,0.653646 0.06673,0.923999 0.04791,0.266935 0.12662,0.4928 0.236133,0.677599 0.112933,0.181379 0.261798,0.319979 0.446601,0.4158 0.184796,0.09239 0.415796,0.138601 0.692998,0.1386 0.461996,10e-7 0.800796,-0.18651 1.016399,-0.559533 0.219019,-0.37302 0.328529,-0.908597 0.328534,-1.606732 -5e-6,-0.711818 -0.109516,-1.2439729 -0.328534,-1.5964653 C 12.243261,9.2696478 11.901039,9.0934037 11.4322,9.0933986"
+ style="fill:url(#linearGradient3941);font-family:Droid Sans;-inkscape-font-specification:Droid Sans;fill-opacity:1.0"
+ id="path3939"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccsccccccccccccccccccscccscccc" />
+ </g>
+</svg>
diff --git a/docs/resources/icon_24x24.svg b/docs/resources/icon_24x24.svg
new file mode 100644
index 0000000..c50ef1f
--- /dev/null
+++ b/docs/resources/icon_24x24.svg
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="ipynb_icon_24x24.svg"
+ width="24"
+ height="24"
+ inkscape:export-filename="/Users/bussonniermatthias/dev/ipython/docs/resources/ipynb_icon_24x24.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <metadata
+ id="metadata371">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ inkscape:window-height="677"
+ inkscape:window-width="1280"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#d9d9d9"
+ id="base"
+ inkscape:zoom="22.627417"
+ inkscape:cx="14.369924"
+ inkscape:cy="14.833103"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:current-layer="svg2"
+ width="210mm"
+ height="40mm"
+ units="px"
+ showgrid="true"
+ inkscape:window-maximized="0"
+ inkscape:snap-grids="true"
+ inkscape:snap-global="true"
+ showguides="true"
+ inkscape:guide-bbox="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid3926"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4167">
+ <stop
+ id="stop4169"
+ offset="0"
+ style="stop-color:#fab434;stop-opacity:1" />
+ <stop
+ id="stop4171"
+ offset="1"
+ style="stop-color:#fb9143;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4689-7">
+ <stop
+ style="stop-color:#5a9fd4;stop-opacity:1;"
+ offset="0"
+ id="stop4691-6" />
+ <stop
+ style="stop-color:#306998;stop-opacity:1;"
+ offset="1"
+ id="stop4693-4" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4689-3">
+ <stop
+ style="stop-color:#5a9fd4;stop-opacity:1;"
+ offset="0"
+ id="stop4691-8" />
+ <stop
+ style="stop-color:#306998;stop-opacity:1;"
+ offset="1"
+ id="stop4693-0" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4167"
+ id="linearGradient3382"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.10331077,0,0,0.10331077,-4.3899917,5.891698)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4167"
+ id="linearGradient3385"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.0933059,0,0,0.0933059,-2.6120135,7.4686107)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ </defs>
+ <path
+ style="fill:#ffffff;stroke:none"
+ d="M 3,23 3,1 19,1 22,4 22,23 3,23"
+ id="path3956"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;color:#000000;fill:#7d7d7d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99363834;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+ d="M 2,0 2,24 23,24 23.03125,3.5625 19.34375,0.03125 2,0 z m 16,1 0,4 4,0 0,18 L 3,23 3,1 18,1 z m 1,0 3,3 -3,0 0,-3 z"
+ id="path4338"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccccccc" />
+ <path
+ style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd"
+ d="m 9.0631018,3.9984663 0,3.9686017 -1.0252408,0 0.00592,-1.0012742 -1.0164801,-0.010113 0,2.0113872 3.0000001,0 0,-4.9686017 z"
+ id="_92110424"
+ sodipodi:nodetypes="ccccccccc"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 11.019015,6.0000004 0,4.9999996 1,0 0,-2 2,0 0,-2.9999996 -3,0 z m 1,1 1,0 0,1 -1,0 0,-1 z"
+ class="fil1"
+ id="_92100232"
+ inkscape:connector-curvature="0"
+ style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd"
+ sodipodi:nodetypes="cccccccccccc" />
+ <path
+ sodipodi:nodetypes="cccccccccccc"
+ id="path3329"
+ style="font-size:12.35648537px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3385);fill-opacity:1.0;stroke:none;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
+ d="m 5.969491,13 0.9967434,0.990704 4.0032566,6.537137 0,-7.527841 L 12,12.9921 12,22.000026 10.969491,22 9.9775096,21.079451 5.969491,14.435247 l 0,7.564753 -1,0 0,-9" />
+ <path
+ sodipodi:nodetypes="csssssssccssssssscccccc"
+ id="path3331"
+ style="font-size:12.35648537px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3382);fill-opacity:1;stroke:none;font-family:Droid Sans;-inkscape-font-specification:Droid Sans"
+ d="m 19.387284,18.246903 c -7e-6,-0.904074 -0.187059,-1.612194 -0.561153,-2.124362 -0.369654,-0.51661 -0.879589,-0.774918 -1.529808,-0.774925 -0.650228,7e-6 -1.162391,0.258315 -1.536489,0.774925 -0.369651,0.512168 -0.554474,1.220288 -0.554472,2.124362 -2e-6,0.904082 0.184821,1.614428 0.554472,2.131043 0.374098,0.512164 0.886261,0.768246 1.536489,0.768244 0.650219,2e-6 1.160154,-0.25608 1.529808,-0.768244 0.374094,-0.516615 0.561146,-1.226961 0.561153,-2.131043 M 14.969491,16 c 0.258306,-0.578575 0.819288,-1.140045 1.211207,-1.353824 0.396366,-0.218219 0.868446,-0.327332 1.416242,-0.32734 0.908526,8e-6 1.645595,0.360749 2.211207,1.082222 0.570052,0.721488 0.855082,1.670102 0.85509,2.845845 -8e-6,1.175751 -0.285038,2.124364 -0.85509,2.845844 -0.565612,0.721482 -1.302681,1.082223 -2.211207,1.082223 -0.547796,0 -1.019876,-0.106881 -1.416242,-0.320658 C 15.788779,21.636086 15.227797,21.44536 14.969491,21 l 0,1 -1,0 0,-10 1,0 0,3" />
+ <path
+ style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd"
+ d="m 15.016466,5.983534 0,2.0072866 0,0.9927134 1,0 1,0 0,1 -2,0 0,1 1,0 1,0 1,0 0,-1 0,-1 0,-1 0,-1 0,-1 -1,0 0,2 -1,0 0,-2 -1,0 z m 2,5 0,0 z"
+ id="path3433"
+ sodipodi:nodetypes="ccccccccccccccccccccccc"
+ inkscape:connector-curvature="0" />
+</svg>
diff --git a/docs/resources/icon_32x32.svg b/docs/resources/icon_32x32.svg
new file mode 100644
index 0000000..7a19362
--- /dev/null
+++ b/docs/resources/icon_32x32.svg
@@ -0,0 +1,311 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="icon_32x32.svg"
+ width="32"
+ height="32"
+ inkscape:export-filename="/Users/bussonniermatthias/dev/ipython/docs/resources/ipynb_icon_32x32@2x.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <metadata
+ id="metadata371">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ inkscape:window-height="677"
+ inkscape:window-width="1280"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ inkscape:zoom="16"
+ inkscape:cx="1.844338"
+ inkscape:cy="15.013138"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:current-layer="svg2"
+ width="210mm"
+ height="40mm"
+ units="mm"
+ showgrid="true"
+ inkscape:window-maximized="0"
+ inkscape:snap-global="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2928"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4689">
+ <stop
+ style="stop-color:#5a9fd4;stop-opacity:1;"
+ offset="0"
+ id="stop4691" />
+ <stop
+ style="stop-color:#306998;stop-opacity:1;"
+ offset="1"
+ id="stop4693" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689"
+ id="linearGradient4355"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.94210934,0,0,0.94210934,1.2341389,2.7751101)"
+ x1="12.796725"
+ y1="13.227233"
+ x2="27.51895"
+ y2="31.016586" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689"
+ id="linearGradient4357"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.94210934,0,0,0.94210934,1.2341389,2.7751101)"
+ x1="12.796725"
+ y1="13.227233"
+ x2="27.51895"
+ y2="31.016586" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689"
+ id="linearGradient3720"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.04569196,0,0,0.04569196,1.6101549,-10.796096)"
+ x1="187.33029"
+ y1="618.22144"
+ x2="393.25586"
+ y2="867.04816" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3266"
+ gradientUnits="userSpaceOnUse"
+ x1="323.06018"
+ y1="147.10051"
+ x2="147.68851"
+ y2="293.00339" />
+ <linearGradient
+ id="linearGradient4689-6">
+ <stop
+ style="stop-color:#5a9fd4;stop-opacity:1"
+ offset="0"
+ id="stop4691-3" />
+ <stop
+ style="stop-color:#306998;stop-opacity:1"
+ offset="1"
+ id="stop4693-8" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3256"
+ gradientUnits="userSpaceOnUse"
+ x1="486.50031"
+ y1="184.54053"
+ x2="496.16876"
+ y2="248.36336" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3254"
+ gradientUnits="userSpaceOnUse"
+ x1="486.50031"
+ y1="184.54053"
+ x2="496.16876"
+ y2="248.36336" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3260"
+ gradientUnits="userSpaceOnUse"
+ x1="485.7803"
+ y1="185.98055"
+ x2="496.88876"
+ y2="249.08336" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3258"
+ gradientUnits="userSpaceOnUse"
+ x1="485.7803"
+ y1="185.98055"
+ x2="496.88876"
+ y2="249.08336" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3264"
+ gradientUnits="userSpaceOnUse"
+ x1="484.3403"
+ y1="182.38054"
+ x2="495.44876"
+ y2="243.32335" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3262"
+ gradientUnits="userSpaceOnUse"
+ x1="484.3403"
+ y1="182.38054"
+ x2="495.44876"
+ y2="243.32335" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3174"
+ gradientUnits="userSpaceOnUse"
+ x1="486.50031"
+ y1="184.54053"
+ x2="496.16876"
+ y2="248.36336" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3176"
+ gradientUnits="userSpaceOnUse"
+ x1="486.50031"
+ y1="184.54053"
+ x2="496.16876"
+ y2="248.36336" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3178"
+ gradientUnits="userSpaceOnUse"
+ x1="485.7803"
+ y1="185.98055"
+ x2="496.88876"
+ y2="249.08336" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3180"
+ gradientUnits="userSpaceOnUse"
+ x1="485.7803"
+ y1="185.98055"
+ x2="496.88876"
+ y2="249.08336" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3182"
+ gradientUnits="userSpaceOnUse"
+ x1="484.3403"
+ y1="182.38054"
+ x2="495.44876"
+ y2="243.32335" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3184"
+ gradientUnits="userSpaceOnUse"
+ x1="484.3403"
+ y1="182.38054"
+ x2="495.44876"
+ y2="243.32335" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4689-6"
+ id="linearGradient3186"
+ gradientUnits="userSpaceOnUse"
+ x1="323.06018"
+ y1="147.10051"
+ x2="147.68851"
+ y2="293.00339" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4328"
+ id="linearGradient3314"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.14130513,0,0,0.14130513,-7.8428899,8.0251755)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ <linearGradient
+ id="linearGradient4328">
+ <stop
+ id="stop4330"
+ offset="0"
+ style="stop-color:#fab434;stop-opacity:1" />
+ <stop
+ id="stop4332"
+ offset="1"
+ style="stop-color:#fb9143;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4328"
+ id="linearGradient3316"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.14130513,0,0,0.14130513,-5.8855851,8.0251755)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ </defs>
+ <path
+ style="fill:#ffffff;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 4,31 4,1 21.957779,1.0144872 28,7 28,31 4,31 z"
+ id="path2939"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;color:#000000;fill:#7d7d7d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99363834;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+ d="M 3,0 3,32 29,32 29,6.5881566 22.494212,0 3,0 z m 18,1 0,7 7,0 0,23 L 4,31 4,1 21,1 z m 1,0 6,6 -6,0 0,-6 z"
+ id="path4338-1"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3277"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:267.69692993px;line-height:125%;font-family:'Droid Sans';-inkscape-font-specification:'Droid Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3316);fill-opacity:1;stroke:#000000;stroke-width:0.08421666;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 5.9182142,16.390066 2.4853226,0 6.0488362,11.412381 0,-11.412381 1.790894,0 0,13.641861 -2.485322,0 -6.0488362,-11.412381 0,11.412381 -1.7908946,0 0,-13.641861" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3279"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:267.69692993px;line-height:125%;font-family:'Droid Sans';-inkscape-font-specification:'Droid Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3314);fill-opacity:1;stroke:#000000;stroke-width:0.08421666;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 25.232346,24.924225 c -9e-6,-1.236564 -0.255851,-2.205107 -0.767526,-2.905635 -0.505601,-0.706602 -1.203074,-1.059908 -2.092423,-1.059917 -0.889361,9e-6 -1.589879,0.353315 -2.101559,1.059917 -0.505597,0.700528 -0.758392,1.669071 -0.758389,2.905635 -3e-6,1.236573 0.252792,2.208163 0.758389,2.914771 0.51168,0.700521 1.212198,1.050781 2.101559,1.05078 0.889349,10e-7 1.586822,-0.350259 2.092423,-1.05078 0.511675,-0.706608 0.767517,-1.678198 0.767526,-2.914771 m -5.719897,-3.572652 c 0.353302,-0.609138 0.797979,-1.059907 1.334033,-1.352307 0.542137,-0.298472 1.187833,-0.447713 1.93709,-0.447724 1.242653,1.1e-5 2.250792,0.49342 3.024418,1.480229 0.779699,0.986827 1.169553,2.284311 1.169564,3.892454 -1.1e-5,1.608153 -0.389865,2.905636 -1.169564,3.892453 -0.773626,0.98682 -1.781765,1.480229 -3.024418,1.480229 -0.749257,0 -1.394953,-0.146195 -1.93709,-0.438586 -0.536054,-0.298482 -0.980731,-0.752296 -1.334033,-1.361445 l 0,1.535051 -1.690385,0 0,-14.217506 1.690385,0 0,5.537152" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4334"
+ y="13.263572"
+ x="5.858809"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.92461967px;line-height:125%;font-family:'Myriad Pro';-inkscape-font-specification:'Myriad Pro';letter-spacing:0px;word-spacing:0px;fill:#4d4d4d;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ xml:space="preserve"><tspan
+ y="13.263572"
+ x="5.858809"
+ id="tspan4336"
+ sodipodi:role="line">Jupyter</tspan></text>
+</svg>
diff --git a/docs/resources/icon_512x512.svg b/docs/resources/icon_512x512.svg
new file mode 100644
index 0000000..f7f0281
--- /dev/null
+++ b/docs/resources/icon_512x512.svg
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="icon_512x512.svg"
+ width="512"
+ height="512"
+ inkscape:export-filename="/Users/bussonniermatthias/dev/ipython/docs/resources/icon_1024x1024.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <metadata
+ id="metadata371">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ inkscape:window-height="855"
+ inkscape:window-width="1440"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ inkscape:zoom="1.0730821"
+ inkscape:cx="-301.17043"
+ inkscape:cy="112.5086"
+ inkscape:window-x="-7"
+ inkscape:window-y="6"
+ inkscape:current-layer="svg2"
+ width="210mm"
+ height="40mm"
+ units="mm"
+ showgrid="false"
+ inkscape:window-maximized="0"
+ inkscape:snap-global="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2928"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4328">
+ <stop
+ id="stop4330"
+ offset="0"
+ style="stop-color:#fab434;stop-opacity:1" />
+ <stop
+ id="stop4332"
+ offset="1"
+ style="stop-color:#fb9143;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3919">
+ <stop
+ style="stop-color:#e4e4e4;stop-opacity:1;"
+ offset="0"
+ id="stop3921" />
+ <stop
+ id="stop3927"
+ offset="1"
+ style="stop-color:#ffffff;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4689">
+ <stop
+ style="stop-color:#5a9fd4;stop-opacity:1;"
+ offset="0"
+ id="stop4691" />
+ <stop
+ style="stop-color:#306998;stop-opacity:1;"
+ offset="1"
+ id="stop4693" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3871"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.9338986"
+ id="feGaussianBlur3873" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3913"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.75310748"
+ id="feGaussianBlur3915" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919"
+ id="linearGradient3925"
+ x1="209.00497"
+ y1="-22.631527"
+ x2="200.9668"
+ y2="-2.0393581"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.2092365,0,0,2.2092365,-68.579768,91.189025)" />
+ <linearGradient
+ id="linearGradient4689-6">
+ <stop
+ style="stop-color:#5a9fd4;stop-opacity:1"
+ offset="0"
+ id="stop4691-3" />
+ <stop
+ style="stop-color:#306998;stop-opacity:1"
+ offset="1"
+ id="stop4693-8" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4328"
+ id="linearGradient3314"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.181239,0,0,2.181239,-111.61182,113.06547)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4328"
+ id="linearGradient3316"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.181239,0,0,2.181239,-81.398128,113.06547)"
+ x1="116.74316"
+ y1="62.91114"
+ x2="190.06432"
+ y2="149.74373" />
+ </defs>
+ <path
+ inkscape:export-ydpi="90"
+ inkscape:export-xdpi="90"
+ inkscape:export-filename="/Users/matthiasbussonnier/Desktop/ipython-python.png"
+ style="fill:#666666;fill-opacity:0.99033813;stroke:#7d7d7d;stroke-width:1.08476496;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter3871)"
+ d="m 20,279.27442 88.35063,0 c 33.9881,0 77.49135,12.81451 77.49135,29.1526 L 185.84198,492 20,492 Z"
+ id="path3077"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc"
+ transform="matrix(2.2092365,0,0,2.2092365,35.81527,-594.94434)" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4338"
+ d="m 79.999519,21.999517 194.983301,0 c 61.89823,0 171.01766,39.636924 171.01766,64.410449 l 0,405.590514 -366.000961,0 z"
+ style="fill:#ffffff;fill-opacity:0.99033813;stroke:#7d7d7d;stroke-width:2;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ inkscape:export-filename="/Users/matthiasbussonnier/Desktop/ipython-python.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90" />
+ <path
+ sodipodi:nodetypes="ccc"
+ inkscape:connector-curvature="0"
+ id="path3875"
+ d="m 172.44062,-29.205599 c 12.43851,5.333061 24.39875,11.169395 29.07043,28.60434895 8.5133,-4.59641195 30.96516,-11.88953595 31.36664,-1.19791745"
+ style="fill:#ffffff;stroke:#7d7d7d;stroke-width:1.08476496;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3913)"
+ transform="matrix(2.2092365,0,0,2.2092365,-68.579768,91.189025)" />
+ <path
+ style="fill:url(#linearGradient3925);fill-opacity:1;stroke:#7d7d7d;stroke-width:2;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 274.98635,21.999991 c 32.90208,0 87.16499,12.824171 101.61943,66.768899 18.80789,-10.154561 68.40936,-26.266797 69.29632,-2.646483 0,-40.095253 -127.94367,-64.122416 -170.91575,-64.122416 z"
+ id="path4384"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3277"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:267.69692993px;line-height:125%;font-family:'Droid Sans';-inkscape-font-specification:'Droid Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3316);fill-opacity:1;stroke:#000000;stroke-opacity:1;stroke-width:1.3;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 100.80974,242.18906 38.36437,0 93.3721,176.16579 0,-176.16579 27.64492,0 0,210.58088 -38.36437,0 -93.3721,-176.16579 0,176.16579 -27.64492,0 0,-210.58088" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3279"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:267.69692993px;line-height:125%;font-family:'Droid Sans';-inkscape-font-specification:'Droid Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3314);fill-opacity:1;stroke:#000000;stroke-opacity:1;stroke-width:1.3;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 398.94991,373.92553 c -1.4e-4,-19.08806 -3.94941,-34.03886 -11.84782,-44.85247 -7.80464,-10.90738 -18.57109,-16.36113 -32.29942,-16.36127 -13.7285,1.4e-4 -24.54197,5.45389 -32.44045,16.36127 -7.80458,10.81361 -11.70683,25.76441 -11.70677,44.85247 -6e-5,19.08821 3.90219,34.08603 11.70677,44.9935 7.89848,10.81351 18.71195,16.22025 32.44045,16.22023 13.72833,2e-5 24.49478,-5.40672 32.29942,-16.22023 7.89841,-10.90747 11.84768,-25.90529 11.84782,-44.9935 m -88.29446,-55.14879 c 5.4537,-9.40288 12.3179,-16.36112 20.59263,-20.87472 8.36862,-4.60733 18.33583,-6.91107 29.90165,-6.91123 19.18205,1.6e-4 34.74406,7.61661 46.68605,22.84936 12.03572,15.23304 18.05365,35.26148 18.05382,60.08538 -1.7e-4,24.82405 -6.0181,44.85249 -18.05382,60.08537 -11.94199,15.23291 -27.504,22.84936 -46.68605,22.84936 -11.56582,0 -21.53303,-2.25672 -29.90165,-6.77017 -8.27473,-4.60748 -15.13893,-11.61273 -20.59263,-21.01578 l 0,23.69563 -26.09342,0 0,-219.46675 26.09342,0 0,85.47355" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4334"
+ y="164.01935"
+ x="99.892746"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:106.89102173px;line-height:125%;font-family:'Myriad Pro';-inkscape-font-specification:'Myriad Pro';letter-spacing:0px;word-spacing:0px;fill:#4d4d4d;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
+ xml:space="preserve"><tspan
+ y="164.01935"
+ x="99.892746"
+ id="tspan4336"
+ sodipodi:role="line">Jupyter</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ x="-919.78046"
+ y="540.88873"
+ id="text4228"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4230"
+ x="-919.78046"
+ y="540.88873" /></text>
+</svg>
diff --git a/docs/resources/ipynb.icns b/docs/resources/ipynb.icns
new file mode 100644
index 0000000..7f3898c
--- /dev/null
+++ b/docs/resources/ipynb.icns
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_1024x1024.png b/docs/resources/ipynb.iconset/icon_1024x1024.png
new file mode 100644
index 0000000..b2c8f07
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_1024x1024.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_128x128.png b/docs/resources/ipynb.iconset/icon_128x128.png
new file mode 100644
index 0000000..5e50eee
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_128x128.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_128x128@2x.png b/docs/resources/ipynb.iconset/icon_128x128@2x.png
new file mode 100644
index 0000000..3acf858
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_128x128@2x.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_16x16.png b/docs/resources/ipynb.iconset/icon_16x16.png
new file mode 100644
index 0000000..d2a5c7a
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_16x16.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_16x16@2x.png b/docs/resources/ipynb.iconset/icon_16x16@2x.png
new file mode 100644
index 0000000..46b7e1c
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_16x16@2x.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_24x24.png b/docs/resources/ipynb.iconset/icon_24x24.png
new file mode 100644
index 0000000..caaa785
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_24x24.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_24x24@2x.png b/docs/resources/ipynb.iconset/icon_24x24@2x.png
new file mode 100644
index 0000000..d35831d
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_24x24@2x.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_256x256.png b/docs/resources/ipynb.iconset/icon_256x256.png
new file mode 100644
index 0000000..3acf858
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_256x256.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_256x256@2x.png b/docs/resources/ipynb.iconset/icon_256x256@2x.png
new file mode 100644
index 0000000..6b65de3
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_256x256@2x.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_32x32.png b/docs/resources/ipynb.iconset/icon_32x32.png
new file mode 100644
index 0000000..a4dfcfd
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_32x32.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_32x32@2x.png b/docs/resources/ipynb.iconset/icon_32x32@2x.png
new file mode 100644
index 0000000..cefed98
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_32x32@2x.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_48x48.png b/docs/resources/ipynb.iconset/icon_48x48.png
new file mode 100644
index 0000000..0f57dd0
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_48x48.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_512x512.png b/docs/resources/ipynb.iconset/icon_512x512.png
new file mode 100644
index 0000000..6b65de3
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_512x512.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_512x512@2x.png b/docs/resources/ipynb.iconset/icon_512x512@2x.png
new file mode 100644
index 0000000..83e1650
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_512x512@2x.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_64x64.png b/docs/resources/ipynb.iconset/icon_64x64.png
new file mode 100644
index 0000000..e21b457
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_64x64.png
Binary files differ
diff --git a/docs/resources/ipynb.iconset/icon_64x64@2x.png b/docs/resources/ipynb.iconset/icon_64x64@2x.png
new file mode 100644
index 0000000..5e50eee
--- /dev/null
+++ b/docs/resources/ipynb.iconset/icon_64x64@2x.png
Binary files differ
diff --git a/docs/resources/notebook_basics.png b/docs/resources/notebook_basics.png
new file mode 100644
index 0000000..d75ce11
--- /dev/null
+++ b/docs/resources/notebook_basics.png
Binary files differ
diff --git a/docs/resources/running_code.png b/docs/resources/running_code.png
new file mode 100644
index 0000000..c74ee40
--- /dev/null
+++ b/docs/resources/running_code.png
Binary files differ
diff --git a/docs/resources/running_code_med.png b/docs/resources/running_code_med.png
new file mode 100644
index 0000000..6f80194
--- /dev/null
+++ b/docs/resources/running_code_med.png
Binary files differ
diff --git a/docs/source/_static/.gitkeep b/docs/source/_static/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/source/_static/.gitkeep
diff --git a/docs/source/_static/images/cell-toolbar-41.png b/docs/source/_static/images/cell-toolbar-41.png
new file mode 100644
index 0000000..8a93e64
--- /dev/null
+++ b/docs/source/_static/images/cell-toolbar-41.png
Binary files differ
diff --git a/docs/source/_static/images/command-palette-41.png b/docs/source/_static/images/command-palette-41.png
new file mode 100644
index 0000000..272f11f
--- /dev/null
+++ b/docs/source/_static/images/command-palette-41.png
Binary files differ
diff --git a/docs/source/_static/images/find-replace-41.png b/docs/source/_static/images/find-replace-41.png
new file mode 100644
index 0000000..73e57a6
--- /dev/null
+++ b/docs/source/_static/images/find-replace-41.png
Binary files differ
diff --git a/docs/source/_static/images/jupyter-file-editor.png b/docs/source/_static/images/jupyter-file-editor.png
new file mode 100644
index 0000000..1447ed8
--- /dev/null
+++ b/docs/source/_static/images/jupyter-file-editor.png
Binary files differ
diff --git a/docs/source/_static/images/jupyter-notebook-dashboard.png b/docs/source/_static/images/jupyter-notebook-dashboard.png
new file mode 100644
index 0000000..812545c
--- /dev/null
+++ b/docs/source/_static/images/jupyter-notebook-dashboard.png
Binary files differ
diff --git a/docs/source/_static/images/jupyter-notebook-default.png b/docs/source/_static/images/jupyter-notebook-default.png
new file mode 100644
index 0000000..aa90447
--- /dev/null
+++ b/docs/source/_static/images/jupyter-notebook-default.png
Binary files differ
diff --git a/docs/source/_static/images/jupyter-notebook-edit.png b/docs/source/_static/images/jupyter-notebook-edit.png
new file mode 100644
index 0000000..81e0fca
--- /dev/null
+++ b/docs/source/_static/images/jupyter-notebook-edit.png
Binary files differ
diff --git a/docs/source/_static/images/multi-select-41.png b/docs/source/_static/images/multi-select-41.png
new file mode 100644
index 0000000..3c8cd51
--- /dev/null
+++ b/docs/source/_static/images/multi-select-41.png
Binary files differ
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
new file mode 100644
index 0000000..3a52be5
--- /dev/null
+++ b/docs/source/changelog.rst
@@ -0,0 +1,173 @@
+.. _changelog:
+
+Jupyter notebook changelog
+==========================
+
+A summary of changes in the Jupyter notebook.
+For more detailed information, see `GitHub <https://github.com/jupyter/notebook>`__.
+
+.. tip::
+
+ Use ``pip install notebook --upgrade`` or ``conda upgrade notebook`` to
+ upgrade to the latest release.
+
+.. _release-4.2.3:
+
+4.2.3
+-----
+
+4.2.3 is a small bugfix release on 4.2.
+
+ Highlights:
+
+- Fix regression in 4.2.2 that delayed loading custom.js
+ until after ``notebook_loaded`` and ``app_initialized`` events have fired.
+- Fix some outdated docs and links.
+
+.. seealso::
+
+ 4.2.3 `on GitHub <https://github.com/jupyter/notebook/milestones/4.2.3>`__.
+
+.. _release-4.2.2:
+
+4.2.2
+-----
+
+4.2.2 is a small bugfix release on 4.2, with an important security fix.
+All users are strongly encouraged to upgrade to 4.2.2.
+
+ Highlights:
+
+- **Security fix**: CVE-2016-6524, where untrusted latex output
+ could be added to the page in a way that could execute javascript.
+- Fix missing POST in OPTIONS responses.
+- Fix for downloading non-ascii filenames.
+- Avoid clobbering ssl_options, so that users can specify more detailed SSL configuration.
+- Fix inverted load order in nbconfig, so user config has highest priority.
+- Improved error messages here and there.
+
+.. seealso::
+
+ 4.2.2 `on GitHub <https://github.com/jupyter/notebook/milestones/4.2.2>`__.
+
+.. _release-4.2.1:
+
+4.2.1
+-----
+
+4.2.1 is a small bugfix release on 4.2. Highlights:
+
+- Compatibility fixes for some versions of ipywidgets
+- Fix for ignored CSS on Windows
+- Fix specifying destination when installing nbextensions
+
+.. seealso::
+
+ 4.2.1 `on GitHub <https://github.com/jupyter/notebook/milestones/4.2.1>`__.
+
+.. _release-4.2.0:
+
+4.2.0
+-----
+
+Release 4.2 adds a new API for enabling and installing extensions.
+Extensions can now be enabled at the system-level, rather than just per-user.
+An API is defined for installing directly from a Python package, as well.
+
+.. seealso::
+
+ :doc:`./examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages`
+
+
+Highlighted changes:
+
+- Upgrade MathJax to 2.6 to fix vertical-bar appearing on some equations.
+- Restore ability for notebook directory to be root (4.1 regression)
+- Large outputs are now throttled, reducing the ability of output floods to
+ kill the browser.
+- Fix the notebook ignoring cell executions while a kernel is starting by queueing the messages.
+- Fix handling of url prefixes (e.g. JupyterHub) in terminal and edit pages.
+- Support nested SVGs in output.
+
+And various other fixes and improvements.
+
+.. _release-4.1.0:
+
+4.1.0
+-----
+
+Bug fixes:
+
+- Properly reap zombie subprocesses
+- Fix cross-origin problems
+- Fix double-escaping of the base URL prefix
+- Handle invalid unicode filenames more gracefully
+- Fix ANSI color-processing
+- Send keepalive messages for web terminals
+- Fix bugs in the notebook tour
+
+UI changes:
+
+- Moved the cell toolbar selector into the *View* menu. Added a button that triggers a "hint" animation to the main toolbar so users can find the new location. (Click here to see a `screencast <https://cloud.githubusercontent.com/assets/335567/10711889/59665a5a-7a3e-11e5-970f-86b89592880c.gif>`__ )
+
+ .. image:: /_static/images/cell-toolbar-41.png
+
+- Added *Restart & Run All* to the *Kernel* menu. Users can also bind it to a keyboard shortcut on action ``restart-kernel-and-run-all-cells``.
+- Added multiple-cell selection. Users press ``Shift-Up/Down`` or ``Shift-K/J`` to extend selection in command mode. Various actions such as cut/copy/paste, execute, and cell type conversions apply to all selected cells.
+
+ .. image:: /_static/images/multi-select-41.png
+
+- Added a command palette for executing Jupyter actions by name. Users press ``Cmd/Ctrl-Shift-P`` or click the new command palette icon on the toolbar.
+
+ .. image:: /_static/images/command-palette-41.png
+
+- Added a *Find and Replace* dialog to the *Edit* menu. Users can also press ``F`` in command mode to show the dialog.
+
+ .. image:: /_static/images/find-replace-41.png
+
+Other improvements:
+
+- Custom KernelManager methods can be Tornado coroutines, allowing async operations.
+- Make clearing output optional when rewriting input with ``set_next_input(replace=True)``.
+- Added support for TLS client authentication via ``--NotebookApp.client-ca``.
+- Added tags to ``jupyter/notebook`` releases on DockerHub. ``latest`` continues to track the master branch.
+
+See the 4.1 milestone on GitHub for a complete list of `issues <https://github.com/jupyter/notebook/issues?page=3&q=milestone%3A4.1+is%3Aclosed+is%3Aissue&utf8=%E2%9C%93>`__ and `pull requests <https://github.com/jupyter/notebook/pulls?q=milestone%3A4.1+is%3Aclosed+is%3Apr>`__ handled.
+
+4.0.x
+-----
+
+4.0.6
+*****
+
+- fix installation of mathjax support files
+- fix some double-escape regressions in 4.0.5
+- fix a couple of cases where errors could prevent opening a notebook
+
+4.0.5
+*****
+
+Security fixes for maliciously crafted files.
+
+- `CVE-2015-6938 <http://www.openwall.com/lists/oss-security/2015/09/02/3>`__: malicious filenames
+- `CVE-2015-7337 <http://www.openwall.com/lists/oss-security/2015/09/16/3>`__: malicious binary files in text editor.
+
+Thanks to Jonathan Kamens at Quantopian and Juan Broullón for the reports.
+
+
+4.0.4
+*****
+
+- Fix inclusion of mathjax-safe extension
+
+4.0.2
+*****
+
+- Fix launching the notebook on Windows
+- Fix the path searched for frontend config
+
+
+4.0.0
+*****
+
+First release of the notebook as a standalone package.
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 0000000..d32d0da
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,334 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Jupyter Notebook documentation build configuration file, created by
+# sphinx-quickstart on Mon Apr 13 09:51:11 2015.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+import shlex
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+
+# DEBUG for RTD
+print("DEBUG:: sys.path")
+print("================")
+for item in sys.path:
+ print(item)
+
+print("os.path.abspath('..')")
+print("=====================")
+print(os.path.abspath('..'))
+
+# Insert absolute path into system path
+sys.path.insert(0, os.path.abspath('..'))
+
+# DEBUG for post insert on RTD
+print("DEBUG:: Post insert to sys.path")
+print("===============================")
+for item in sys.path:
+ print(item)
+
+# Check if docs are being built by ReadTheDocs
+# If so, generate a config.rst file and populate it with documentation about
+# configuration options
+
+if os.environ.get('READTHEDOCS', ''):
+
+ # Readthedocs doesn't run our Makefile, so we do this to force it to generate
+ # the config docs.
+ with open('../autogen_config.py') as f:
+ exec(compile(f.read(), '../autogen_config.py', 'exec'), {})
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.autosummary',
+ 'sphinx.ext.mathjax',
+ 'IPython.sphinxext.ipython_console_highlighting',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'Jupyter Notebook'
+copyright = '2015, Jupyter Team, https://jupyter.org'
+author = 'The Jupyter Team'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+_version_py = '../../notebook/_version.py'
+version_ns = {}
+exec(compile(open(_version_py).read(), _version_py, 'exec'), version_ns)
+# The short X.Y version.
+version = '%i.%i' % version_ns['version_info'][:2]
+# The full version, including alpha/beta/rc tags.
+release = version_ns['__version__']
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+# NOTE: Sphinx's 'make html' builder will throw a warning about an unfound
+# _static directory. Do not remove or comment out html_static_path
+# since it is needed to properly generate _static in the build directory
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr'
+#html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# Now only 'ja' uses this config value
+#html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'JupyterNotebookdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+
+# Latex figure (float) alignment
+#'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'JupyterNotebook.tex', 'Jupyter Notebook Documentation',
+ 'https://jupyter.org', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'jupyternotebook', 'Jupyter Notebook Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'JupyterNotebook', 'Jupyter Notebook Documentation',
+ author, 'JupyterNotebook', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+intersphinx_mapping = {
+ 'ipython': ('http://ipython.org/ipython-doc/dev/', None),
+ 'nbconvert': ('http://nbconvert.readthedocs.org/en/latest/', None),
+ 'nbformat': ('http://nbformat.readthedocs.org/en/latest/', None),
+ 'jupyter': ('http://jupyter.readthedocs.org/en/latest/', None),
+}
diff --git a/docs/source/development_faq.rst b/docs/source/development_faq.rst
new file mode 100644
index 0000000..89fc4ae
--- /dev/null
+++ b/docs/source/development_faq.rst
@@ -0,0 +1,8 @@
+.. _development_faq:
+
+Developer FAQ
+=============
+
+1. How do I install a pre-release version such as a beta or release candidate?
+
+ ``python -m pip install notebook --pre --upgrade``
diff --git a/docs/source/development_js.rst b/docs/source/development_js.rst
new file mode 100644
index 0000000..f687307
--- /dev/null
+++ b/docs/source/development_js.rst
@@ -0,0 +1,81 @@
+.. _development_js:
+
+Installing Javascript machinery
+===============================
+
+Running the Notebook from the source code on GitHub requires some JavaScript
+tools to build/minify the CSS and JavaScript components. We do these steps when
+making releases, so there's no need for these tools when installing released
+versions of the Notebook.
+
+First, install `Node.js <https://nodejs.org/>`_. The installers on the
+Node.js website also include Node's package manager, *npm*. Alternatively,
+install both of these from your package manager. For example, on Ubuntu or Debian::
+
+ sudo apt-get install nodejs-legacy npm
+
+You can then build the JavaScript and CSS by running::
+
+ python setup.py css js
+
+This will automatically fetch the remaining dependencies (bower, less) and
+install them in a subdirectory.
+
+For quick iteration on the Notebook's JavaScript you can deactivate the use of
+the bundled and minified JavaScript by using the option
+``--NotebookApp.ignore_minified_js=True``. This might though highly increase the
+number of requests that the browser make to the server, but can allow to test
+JavaScript file modification without going through the compilation step that
+can take up to 30 sec.
+
+
+Making a notebook release
+-------------------------
+
+Make sure you have followed the step above and have all the tools to generate
+the minified JavaScript and CSS files.
+
+Make sure the repository is clean of any file that could be problematic.
+You can remove all non-tracked files with:
+
+.. code::
+
+ $ git clean -xfdi
+
+This would ask you for confirmation before removing all untracked files. Make
+sure the ``dist/`` folder is clean and avoid stale build from
+previous attempts.
+
+1. Update version number in ``notebook/_version.py``.
+
+2. Run ``$ python setup.py jsversion``. It will modify (at least)
+``notebook/static/base/js/namespace.js`` to make the notebook version available
+from within JavaScript.
+
+3 . Commit and tag the release with the current version number:
+
+.. code::
+
+ git commit -am "release $VERSION"
+ git tag $VERSION
+
+
+4. You are now ready to build the ``sdist`` and ``wheel``:
+
+.. code::
+
+ $ python setup.py sdist --formats=zip,gztar
+ $ python setup.py bdist_wheel
+
+
+5. You can now test the ``wheel`` and the ``sdist`` locally before uploading to PyPI.
+Make sure to use `twine <https://github.com/pypa/twine>`_ to upload the archives over SSL.
+
+.. code::
+
+ $ twine upload dist/*
+
+6. If all went well, change the ``notebook/_version.py`` back adding the ``.dev`` suffix.
+
+7. Push directly on master, not forgetting to push ``--tags``.
+
diff --git a/docs/source/examples/Notebook/Connecting with the Qt Console.ipynb b/docs/source/examples/Notebook/Connecting with the Qt Console.ipynb
new file mode 100644
index 0000000..df0ac50
--- /dev/null
+++ b/docs/source/examples/Notebook/Connecting with the Qt Console.ipynb
@@ -0,0 +1,132 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Connecting to an existing IPython kernel using the Qt Console"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The Frontend/Kernel Model"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The traditional IPython (`ipython`) consists of a single process that combines a terminal based UI with the process that runs the users code.\n",
+ "\n",
+ "While this traditional application still exists, the modern Jupyter consists of two processes:\n",
+ "\n",
+ "* Kernel: this is the process that runs the users code.\n",
+ "* Frontend: this is the process that provides the user interface where the user types code and sees results.\n",
+ "\n",
+ "Jupyter currently has 3 frontends:\n",
+ "\n",
+ "* Terminal Console (`ipython console`)\n",
+ "* Qt Console (`ipython qtconsole`)\n",
+ "* Notebook (`ipython notebook`)\n",
+ "\n",
+ "The Kernel and Frontend communicate over a ZeroMQ/JSON based messaging protocol, which allows multiple Frontends (even of different types) to communicate with a single Kernel. This opens the door for all sorts of interesting things, such as connecting a Console or Qt Console to a Notebook's Kernel. For example, you may want to connect a Qt console to your Notebook's Kernel and use it as a help\n",
+ "browser, calling `??` on objects in the Qt console (whose pager is more flexible than the\n",
+ "one in the notebook). \n",
+ "\n",
+ "This Notebook describes how you would connect another Frontend to a Kernel that is associated with a Notebook."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Manual connection"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To connect another Frontend to a Kernel manually, you first need to find out the connection information for the Kernel using the `%connect_info` magic:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%connect_info"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can see that this magic displays everything you need to connect to this Notebook's Kernel."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Automatic connection using a new Qt Console"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can also start a new Qt Console connected to your current Kernel by using the `%qtconsole` magic. This will detect the necessary connection\n",
+ "information and start the Qt Console for you automatically."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "a = 10"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%qtconsole"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/Custom Keyboard Shortcuts.ipynb b/docs/source/examples/Notebook/Custom Keyboard Shortcuts.ipynb
new file mode 100644
index 0000000..c38a05c
--- /dev/null
+++ b/docs/source/examples/Notebook/Custom Keyboard Shortcuts.ipynb
@@ -0,0 +1,142 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Keyboard Shortcut Customization"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Starting with IPython 2.0 keyboard shortcuts in command and edit mode are fully customizable. These customizations are made using the Jupyter JavaScript API. Here is an example that makes the `r` key available for running a cell:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%javascript\n",
+ "\n",
+ "Jupyter.keyboard_manager.command_shortcuts.add_shortcut('r', {\n",
+ " help : 'run cell',\n",
+ " help_index : 'zz',\n",
+ " handler : function (event) {\n",
+ " IPython.notebook.execute_cell();\n",
+ " return false;\n",
+ " }}\n",
+ ");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\"By default the keypress `r`, while in command mode, changes the type of the selected cell to `raw`. This shortcut is overridden by the code in the previous cell, and thus the action no longer be available via the keypress `r`.\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "There are a couple of points to mention about this API:\n",
+ "\n",
+ "* The `help_index` field is used to sort the shortcuts in the Keyboard Shortcuts help dialog. It defaults to `zz`.\n",
+ "* When a handler returns `false` it indicates that the event should stop propagating and the default action should not be performed. For further details about the `event` object or event handling, see the jQuery docs.\n",
+ "* If you don't need a `help` or `help_index` field, you can simply pass a function as the second argument to `add_shortcut`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%javascript\n",
+ "\n",
+ "Jupyter.keyboard_manager.command_shortcuts.add_shortcut('r', function (event) {\n",
+ " IPython.notebook.execute_cell();\n",
+ " return false;\n",
+ "});"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Likewise, to remove a shortcut, use `remove_shortcut`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%javascript\n",
+ "\n",
+ "Jupyter.keyboard_manager.command_shortcuts.remove_shortcut('r');"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If you want your keyboard shortcuts to be active for all of your notebooks, put the above API calls into your `custom.js` file."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": true
+ },
+ "source": [
+ "Of course we provide name for majority of existing action so that you do not have to re-write everything, here is for example how to bind `r` back to it's initial behavior:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%javascript\n",
+ "\n",
+ "Jupyter.keyboard_manager.command_shortcuts.add_shortcut('r', 'jupyter-notebook:change-cell-to-raw');"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb b/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb
new file mode 100644
index 0000000..d14cb4f
--- /dev/null
+++ b/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb
@@ -0,0 +1,281 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Distributing Jupyter Extensions as Python Packages"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Overview\n",
+ "### How can the notebook be extended?\n",
+ "The Jupyter Notebook client and server application are both deeply customizable. Their behavior can be extended by creating, respectively:\n",
+ "\n",
+ "- nbextension: a notebook extension\n",
+ " - a single JS file, or directory of JavaScript, Cascading StyleSheets, etc. that contain at\n",
+ " minimum a JavaScript module packaged as an\n",
+ " [AMD modules](https://en.wikipedia.org/wiki/Asynchronous_module_definition)\n",
+ " that exports a function `load_ipython_extension`\n",
+ "- server extension: an importable Python module\n",
+ " - that implements `load_jupyter_server_extension`"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Why create a Python package for Jupyter extensions?\n",
+ "Since it is rare to have a server extension that does not have any frontend components (an nbextension), for convenience and consistency, all these client and server extensions with their assets can be packaged and versioned together as a Python package with a few simple commands. This makes installing the package of extensions easier and less error-prone for the user. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Installation of Jupyter Extensions\n",
+ "### Install a Python package containing Jupyter Extensions\n",
+ "There are several ways that you may get a Python package containing Jupyter Extensions. Commonly, you will use a package manager for your system:\n",
+ "```shell\n",
+ "pip install helpful_package\n",
+ "# or\n",
+ "conda install helpful_package\n",
+ "# or\n",
+ "apt-get install helpful_package\n",
+ "\n",
+ "# where 'helpful_package' is a Python package containing one or more Jupyter Extensions\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Enable a Server Extension\n",
+ "\n",
+ "The simplest case would be to enable a server extension which has no frontend components. \n",
+ "\n",
+ "A `pip` user that wants their configuration stored in their home directory would type the following command:\n",
+ "```shell\n",
+ "jupyter serverextension enable --py helpful_package\n",
+ "```\n",
+ "\n",
+ "Alternatively, a `virtualenv` or `conda` user can pass `--sys-prefix` which keeps their environment isolated and reproducible. For example:\n",
+ "```shell\n",
+ "# Make sure that your virtualenv or conda environment is activated\n",
+ "[source] activate my-environment\n",
+ "\n",
+ "jupyter serverextension enable --py helpful_package --sys-prefix\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Install the nbextension assets"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If a package also has an nbextension with frontend assets that must be available (but not neccessarily enabled by default), install these assets with the following command:\n",
+ "```shell\n",
+ "jupyter nbextension install --py helpful_package # or --sys-prefix if using virtualenv or conda\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Enable nbextension assets\n",
+ "If a package has assets that should be loaded every time a Jupyter app (e.g. lab, notebook, dashboard, terminal) is loaded in the browser, the following command can be used to enable the nbextension:\n",
+ "```shell\n",
+ "jupyter nbextension enable --py helpful_package # or --sys-prefix if using virtualenv or conda\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Did it work? Check by listing Jupyter Extensions.\n",
+ "After running one or more extension installation steps, you can list what is presently known about nbextensions or server extension. The following commands will list which extensions are available, whether they are enabled, and other extension details:\n",
+ "\n",
+ "```shell\n",
+ "jupyter nbextension list\n",
+ "jupyter serverextension list\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Additional resources on creating and distributing packages \n",
+ "\n",
+ "> Of course, in addition to the files listed, there are number of other files one needs to build a proper package. Here are some good resources:\n",
+ "- [The Hitchhiker's Guide to Packaging](http://the-hitchhikers-guide-to-packaging.readthedocs.org/en/latest/quickstart.html)\n",
+ "- [Repository Structure and Python](http://www.kennethreitz.org/essays/repository-structure-and-python) by Kenneth Reitz\n",
+ "\n",
+ "> How you distribute them, too, is important:\n",
+ "- [Packaging and Distributing Projects](http://python-packaging-user-guide.readthedocs.org/en/latest/distributing/)\n",
+ "- [conda: Building packages](http://conda.pydata.org/docs/building/build.html)\n",
+ "\n",
+ "> Here are some tools to get you started:\n",
+ "- [generator-nbextension](https://github.com/Anaconda-Server/generator-nbextension)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example - Server extension"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Creating a Python package with a server extension\n",
+ "\n",
+ "Here is an example of a python module which contains a server extension directly on itself. It has this directory structure:\n",
+ "```\n",
+ "- setup.py\n",
+ "- MANIFEST.in\n",
+ "- my_module/\n",
+ " - __init__.py\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Defining the server extension\n",
+ "This example shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file.\n",
+ "\n",
+ "#### `my_module/__init__.py`\n",
+ "\n",
+ "```python\n",
+ "def _jupyter_server_extension_paths():\n",
+ " return [{\n",
+ " \"module\": \"my_module\"\n",
+ " }]\n",
+ "\n",
+ "\n",
+ "def load_jupyter_server_extension(nbapp):\n",
+ " nbapp.log.info(\"my module enabled!\")\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Install and enable the server extension\n",
+ "Which a user can install with:\n",
+ "```\n",
+ "jupyter serverextension enable --py my_module [--sys-prefix]\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example - Server extension and nbextension"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Creating a Python package with a server extension and nbextension\n",
+ "Here is another server extension, with a front-end module. It assumes this directory structure:\n",
+ "\n",
+ "```\n",
+ "- setup.py\n",
+ "- MANIFEST.in\n",
+ "- my_fancy_module/\n",
+ " - __init__.py\n",
+ " - static/\n",
+ " index.js\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": true
+ },
+ "source": [
+ "### Defining the server extension and nbextension\n",
+ "This example again shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file. This time, there is also a function `_jupyter_nbextension_path` for the nbextension.\n",
+ "\n",
+ "#### `my_fancy_module/__init__.py`\n",
+ "\n",
+ "```python\n",
+ "def _jupyter_server_extension_paths():\n",
+ " return [{\n",
+ " \"module\": \"my_fancy_module\"\n",
+ " }]\n",
+ "\n",
+ "# Jupyter Extension points\n",
+ "def _jupyter_nbextension_paths():\n",
+ " return [dict(\n",
+ " section=\"notebook\",\n",
+ " # the path is relative to the `my_fancy_module` directory\n",
+ " src=\"static\",\n",
+ " # directory in the `nbextension/` namespace\n",
+ " dest=\"my_fancy_module\",\n",
+ " # _also_ in the `nbextension/` namespace\n",
+ " require=\"my_fancy_module/index\")]\n",
+ "\n",
+ "def load_jupyter_server_extension(nbapp):\n",
+ " nbapp.log.info(\"my module enabled!\")\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Install and enable the server extension and nbextension\n",
+ "\n",
+ "The user can install and enable the extensions with the following set of commands:\n",
+ "```\n",
+ "jupyter nbextension install --py my_fancy_module [--sys-prefix|--user]\n",
+ "jupyter nbextension enable --py my_fancy_module [--sys-prefix|--system]\n",
+ "jupyter serverextension enable --py my_fancy_module [--sys-prefix|--system]\n",
+ "```"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.5.1"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/Examples and Tutorials Index.ipynb b/docs/source/examples/Notebook/Examples and Tutorials Index.ipynb
new file mode 100644
index 0000000..1a4a212
--- /dev/null
+++ b/docs/source/examples/Notebook/Examples and Tutorials Index.ipynb
@@ -0,0 +1,81 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "<img src=\"../images/jupyter_logo.png\">"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Examples and Tutorials"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This portion of the documentation was generated from notebook files. You can download the original interactive notebook files using the links at the tops and bottoms of the pages."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Tutorials"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "* [What is the Jupyter Notebook](What is the Jupyter Notebook.ipynb)\n",
+ "* [Notebook Basics](Notebook Basics.ipynb)\n",
+ "* [Running Code](Running Code.ipynb)\n",
+ "* [Working With Markdown Cells](Working With Markdown Cells.ipynb)\n",
+ "* [Custom Keyboard Shortcuts](Custom Keyboard Shortcuts.ipynb)\n",
+ "* [JavaScript Notebook Extensions](JavaScript Notebook Extensions.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Examples"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "* [Importing Notebooks](Importing Notebooks.ipynb)\n",
+ "* [Connecting with the Qt Console](Connecting with the Qt Console.ipynb)\n",
+ "* [Typesetting Equations](Typesetting Equations.ipynb)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.5.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/Importing Notebooks.ipynb b/docs/source/examples/Notebook/Importing Notebooks.ipynb
new file mode 100644
index 0000000..14d91d2
--- /dev/null
+++ b/docs/source/examples/Notebook/Importing Notebooks.ipynb
@@ -0,0 +1,523 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Importing Jupyter Notebooks as Modules"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "It is a common problem that people want to import code from Jupyter Notebooks.\n",
+ "This is made difficult by the fact that Notebooks are not plain Python files,\n",
+ "and thus cannot be imported by the regular Python machinery.\n",
+ "\n",
+ "Fortunately, Python provides some fairly sophisticated [hooks](http://www.python.org/dev/peps/pep-0302/) into the import machinery,\n",
+ "so we can actually make Jupyter notebooks importable without much difficulty,\n",
+ "and only using public APIs."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "import io, os, sys, types"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from IPython import get_ipython\n",
+ "from IPython.nbformat import current\n",
+ "from IPython.core.interactiveshell import InteractiveShell"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Import hooks typically take the form of two objects:\n",
+ "\n",
+ "1. a Module **Loader**, which takes a module name (e.g. `'IPython.display'`), and returns a Module\n",
+ "2. a Module **Finder**, which figures out whether a module might exist, and tells Python what **Loader** to use"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "def find_notebook(fullname, path=None):\n",
+ " \"\"\"find a notebook, given its fully qualified name and an optional path\n",
+ " \n",
+ " This turns \"foo.bar\" into \"foo/bar.ipynb\"\n",
+ " and tries turning \"Foo_Bar\" into \"Foo Bar\" if Foo_Bar\n",
+ " does not exist.\n",
+ " \"\"\"\n",
+ " name = fullname.rsplit('.', 1)[-1]\n",
+ " if not path:\n",
+ " path = ['']\n",
+ " for d in path:\n",
+ " nb_path = os.path.join(d, name + \".ipynb\")\n",
+ " if os.path.isfile(nb_path):\n",
+ " return nb_path\n",
+ " # let import Notebook_Name find \"Notebook Name.ipynb\"\n",
+ " nb_path = nb_path.replace(\"_\", \" \")\n",
+ " if os.path.isfile(nb_path):\n",
+ " return nb_path\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebook Loader"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here we have our Notebook Loader.\n",
+ "It's actually quite simple - once we figure out the filename of the module,\n",
+ "all it does is:\n",
+ "\n",
+ "1. load the notebook document into memory\n",
+ "2. create an empty Module\n",
+ "3. execute every cell in the Module namespace\n",
+ "\n",
+ "Since IPython cells can have extended syntax,\n",
+ "the IPython transform is applied to turn each of these cells into their pure-Python counterparts before executing them.\n",
+ "If all of your notebook cells are pure-Python,\n",
+ "this step is unnecessary."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "class NotebookLoader(object):\n",
+ " \"\"\"Module Loader for Jupyter Notebooks\"\"\"\n",
+ " def __init__(self, path=None):\n",
+ " self.shell = InteractiveShell.instance()\n",
+ " self.path = path\n",
+ " \n",
+ " def load_module(self, fullname):\n",
+ " \"\"\"import a notebook as a module\"\"\"\n",
+ " path = find_notebook(fullname, self.path)\n",
+ " \n",
+ " print (\"importing Jupyter notebook from %s\" % path)\n",
+ " \n",
+ " # load the notebook object\n",
+ " with io.open(path, 'r', encoding='utf-8') as f:\n",
+ " nb = current.read(f, 'json')\n",
+ " \n",
+ " \n",
+ " # create the module and add it to sys.modules\n",
+ " # if name in sys.modules:\n",
+ " # return sys.modules[name]\n",
+ " mod = types.ModuleType(fullname)\n",
+ " mod.__file__ = path\n",
+ " mod.__loader__ = self\n",
+ " mod.__dict__['get_ipython'] = get_ipython\n",
+ " sys.modules[fullname] = mod\n",
+ " \n",
+ " # extra work to ensure that magics that would affect the user_ns\n",
+ " # actually affect the notebook module's ns\n",
+ " save_user_ns = self.shell.user_ns\n",
+ " self.shell.user_ns = mod.__dict__\n",
+ " \n",
+ " try:\n",
+ " for cell in nb.worksheets[0].cells:\n",
+ " if cell.cell_type == 'code' and cell.language == 'python':\n",
+ " # transform the input to executable Python\n",
+ " code = self.shell.input_transformer_manager.transform_cell(cell.input)\n",
+ " # run the code in themodule\n",
+ " exec(code, mod.__dict__)\n",
+ " finally:\n",
+ " self.shell.user_ns = save_user_ns\n",
+ " return mod\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The Module Finder"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The finder is a simple object that tells you whether a name can be imported,\n",
+ "and returns the appropriate loader.\n",
+ "All this one does is check, when you do:\n",
+ "\n",
+ "```python\n",
+ "import mynotebook\n",
+ "```\n",
+ "\n",
+ "it checks whether `mynotebook.ipynb` exists.\n",
+ "If a notebook is found, then it returns a NotebookLoader.\n",
+ "\n",
+ "Any extra logic is just for resolving paths within packages."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "class NotebookFinder(object):\n",
+ " \"\"\"Module finder that locates Jupyter Notebooks\"\"\"\n",
+ " def __init__(self):\n",
+ " self.loaders = {}\n",
+ " \n",
+ " def find_module(self, fullname, path=None):\n",
+ " nb_path = find_notebook(fullname, path)\n",
+ " if not nb_path:\n",
+ " return\n",
+ " \n",
+ " key = path\n",
+ " if path:\n",
+ " # lists aren't hashable\n",
+ " key = os.path.sep.join(path)\n",
+ " \n",
+ " if key not in self.loaders:\n",
+ " self.loaders[key] = NotebookLoader(path)\n",
+ " return self.loaders[key]\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Register the hook"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now we register the `NotebookFinder` with `sys.meta_path`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "sys.meta_path.append(NotebookFinder())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "After this point, my notebooks should be importable.\n",
+ "\n",
+ "Let's look at what we have in the CWD:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "ls nbpackage"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So I should be able to `import nbimp.mynotebook`.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Aside: displaying notebooks"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here is some simple code to display the contents of a notebook\n",
+ "with syntax highlighting, etc."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from pygments import highlight\n",
+ "from pygments.lexers import PythonLexer\n",
+ "from pygments.formatters import HtmlFormatter\n",
+ "\n",
+ "from IPython.display import display, HTML\n",
+ "\n",
+ "formatter = HtmlFormatter()\n",
+ "lexer = PythonLexer()\n",
+ "\n",
+ "# publish the CSS for pygments highlighting\n",
+ "display(HTML(\"\"\"\n",
+ "<style type='text/css'>\n",
+ "%s\n",
+ "</style>\n",
+ "\"\"\" % formatter.get_style_defs()\n",
+ "))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "def show_notebook(fname):\n",
+ " \"\"\"display a short summary of the cells of a notebook\"\"\"\n",
+ " with io.open(fname, 'r', encoding='utf-8') as f:\n",
+ " nb = current.read(f, 'json')\n",
+ " html = []\n",
+ " for cell in nb.worksheets[0].cells:\n",
+ " html.append(\"<h4>%s cell</h4>\" % cell.cell_type)\n",
+ " if cell.cell_type == 'code':\n",
+ " html.append(highlight(cell.input, lexer, formatter))\n",
+ " else:\n",
+ " html.append(\"<pre>%s</pre>\" % cell.source)\n",
+ " display(HTML('\\n'.join(html)))\n",
+ "\n",
+ "show_notebook(os.path.join(\"nbpackage\", \"mynotebook.ipynb\"))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So my notebook has a heading cell and some code cells,\n",
+ "one of which contains some IPython syntax.\n",
+ "\n",
+ "Let's see what happens when we import it"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from nbpackage import mynotebook"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Hooray, it imported! Does it work?"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "mynotebook.foo()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Hooray again!\n",
+ "\n",
+ "Even the function that contains IPython syntax works:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "mynotebook.has_ip_syntax()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Notebooks in packages"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We also have a notebook inside the `nb` package,\n",
+ "so let's make sure that works as well."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "ls nbpackage/nbs"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that the `__init__.py` is necessary for `nb` to be considered a package,\n",
+ "just like usual."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "show_notebook(os.path.join(\"nbpackage\", \"nbs\", \"other.ipynb\"))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from nbpackage.nbs import other\n",
+ "other.bar(5)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So now we have importable notebooks, from both the local directory and inside packages.\n",
+ "\n",
+ "I can even put a notebook inside IPython, to further demonstrate that this is working properly:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "import shutil\n",
+ "from IPython.utils.path import get_ipython_package_dir\n",
+ "\n",
+ "utils = os.path.join(get_ipython_package_dir(), 'utils')\n",
+ "shutil.copy(os.path.join(\"nbpackage\", \"mynotebook.ipynb\"),\n",
+ " os.path.join(utils, \"inside_ipython.ipynb\")\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "and import the notebook from `IPython.utils`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.utils import inside_ipython\n",
+ "inside_ipython.whatsmyname()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This approach can even import functions and classes that are defined in a notebook using the `%%cython` magic."
+ ]
+ }
+ ],
+ "metadata": {
+ "gist_id": "6011986",
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb b/docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb
new file mode 100644
index 0000000..6987f4b
--- /dev/null
+++ b/docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb
@@ -0,0 +1,594 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Embracing web standards"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "One of the main reasons why we developed the current notebook web application \n",
+ "was to embrace the web technology. \n",
+ "\n",
+ "By being a pure web application using HTML, Javascript, and CSS, the Notebook can get \n",
+ "all the web technology improvement for free. Thus, as browser support for different \n",
+ "media extend, the notebook web app should be able to be compatible without modification. \n",
+ "\n",
+ "This is also true with performance of the User Interface as the speed of Javascript VM increases. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The other advantage of using only web technology is that the code of the interface is fully accessible to the end user and is modifiable live.\n",
+ "Even if this task is not always easy, we strive to keep our code as accessible and reusable as possible.\n",
+ "This should allow us - with minimum effort - development of small extensions that customize the behavior of the web interface. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Tampering with the Notebook application"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The first tool that is available to you and that you should be aware of are browser \"developers tool\". The exact naming can change across browser and might require the installation of extensions. But basically they can allow you to inspect/modify the DOM, and interact with the javascript code that runs the frontend.\n",
+ "\n",
+ " - In Chrome and Safari, Developer tools are in the menu `View > Developer > Javascript Console` \n",
+ " - In Firefox you might need to install [Firebug](http://getfirebug.com/)\n",
+ " \n",
+ "Those will be your best friends to debug and try different approaches for your extensions."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Injecting JS"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Using magics"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The above tools can be tedious for editing edit long JavaScript files. Therefore we provide the `%%javascript` magic. This allows you to quickly inject JavaScript into the notebook. Still the javascript injected this way will not survive reloading. Hence, it is a good tool for testing an refining a script.\n",
+ "\n",
+ "You might see here and there people modifying css and injecting js into the notebook by reading file(s) and publishing them into the notebook.\n",
+ "Not only does this often break the flow of the notebook and make the re-execution of the notebook broken, but it also means that you need to execute those cells in the entire notebook every time you need to update the code.\n",
+ "\n",
+ "This can still be useful in some cases, like the `%autosave` magic that allows you to control the time between each save. But this can be replaced by a JavaScript dropdown menu to select the save interval."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "## you can inspect the autosave code to see what it does.\n",
+ "%autosave??"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### custom.js"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To inject Javascript we provide an entry point: `custom.js` that allows the user to execute and load other resources into the notebook.\n",
+ "Javascript code in `custom.js` will be executed when the notebook app starts and can then be used to customize almost anything in the UI and in the behavior of the notebook.\n",
+ "\n",
+ "`custom.js` can be found in the Jupyter dir. You can share your custom.js with others."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "##### Back to theory"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from jupyter_core.paths import jupyter_config_dir\n",
+ "jupyter_dir = jupyter_config_dir()\n",
+ "jupyter_dir"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "and custom js is in "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "import os.path\n",
+ "custom_js_path = os.path.join(jupyter_dir, 'custom', 'custom.js')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "# my custom js\n",
+ "if os.path.isfile(custom_js_path):\n",
+ " with open(custom_js_path) as f:\n",
+ " print(f.read())\n",
+ "else:\n",
+ " print(\"You don't have a custom.js file\") "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that `custom.js` is meant to be modified by user. When writing a script, you can define it in a separate file and add a line of configuration into `custom.js` that will fetch and execute the file."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Warning** : even if modification of `custom.js` takes effect immediately after browser refresh (except if browser cache is aggressive), *creating* a file in `static/` directory needs a **server restart**."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Exercise :"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ " - Create a `custom.js` in the right location with the following content:\n",
+ "```javascript\n",
+ "alert(\"hello world from custom.js\")\n",
+ "```\n",
+ "\n",
+ " - Restart your server and open any notebook.\n",
+ " - Be greeted by custom.js"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Have a look at [default custom.js](https://github.com/jupyter/notebook/blob/4.0.x/notebook/static/custom/custom.js), to see it's content and for more explanation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### For the quick ones : "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We've seen above that you can change the autosave rate by using a magic. This is typically something I don't want to type every time, and that I don't like to embed into my workflow and documents. (readers don't care what my autosave time is). Let's build an extension that allows us to do it. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "foo": true
+ },
+ "source": [
+ "Create a dropdown element in the toolbar (DOM `Jupyter.toolbar.element`), you will need \n",
+ "\n",
+ "- `Jupyter.notebook.set_autosave_interval(miliseconds)`\n",
+ "- know that 1 min = 60 sec, and 1 sec = 1000 ms"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```javascript\n",
+ "\n",
+ "var label = jQuery('<label/>').text('AutoScroll Limit:');\n",
+ "var select = jQuery('<select/>')\n",
+ " //.append(jQuery('<option/>').attr('value', '2').text('2min (default)'))\n",
+ " .append(jQuery('<option/>').attr('value', undefined).text('disabled'))\n",
+ "\n",
+ " // TODO:\n",
+ " //the_toolbar_element.append(label)\n",
+ " //the_toolbar_element.append(select);\n",
+ " \n",
+ "select.change(function() {\n",
+ " var val = jQuery(this).val() // val will be the value in [2]\n",
+ " // TODO\n",
+ " // this will be called when dropdown changes\n",
+ "\n",
+ "});\n",
+ "\n",
+ "var time_m = [1,5,10,15,30];\n",
+ "for (var i=0; i < time_m.length; i++) {\n",
+ " var ts = time_m[i];\n",
+ " //[2] ____ this will be `val` on [1] \n",
+ " // | \n",
+ " // v \n",
+ " select.append($('<option/>').attr('value', ts).text(thr+'min'));\n",
+ " // this will fill up the dropdown `select` with\n",
+ " // 1 min\n",
+ " // 5 min\n",
+ " // 10 min\n",
+ " // 10 min\n",
+ " // ...\n",
+ "}\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### A non-interactive example first"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "I like my cython to be nicely highlighted\n",
+ "\n",
+ "```javascript\n",
+ "Jupyter.config.cell_magic_highlight['magic_text/x-cython'] = {}\n",
+ "Jupyter.config.cell_magic_highlight['magic_text/x-cython'].reg = [/^%%cython/]\n",
+ "```\n",
+ "\n",
+ "`text/x-cython` is the name of CodeMirror mode name, `magic_` prefix will just patch the mode so that the first line that contains a magic does not screw up the highlighting. `reg`is a list or regular expression that will trigger the change of mode."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Get more documentation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Sadly, you will have to read the js source file (but there are lots of comments) and/or build the JavaScript documentation using yuidoc.\n",
+ "If you have `node` and `yui-doc` installed:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```bash\n",
+ "$ cd ~/jupyter/notebook/notebook/static/notebook/js/\n",
+ "$ yuidoc . --server\n",
+ "warn: (yuidoc): Failed to extract port, setting to the default :3000\n",
+ "info: (yuidoc): Starting YUIDoc@0.3.45 using YUI@3.9.1 with NodeJS@0.10.15\n",
+ "info: (yuidoc): Scanning for yuidoc.json file.\n",
+ "info: (yuidoc): Starting YUIDoc with the following options:\n",
+ "info: (yuidoc):\n",
+ "{ port: 3000,\n",
+ " nocode: false,\n",
+ " paths: [ '.' ],\n",
+ " server: true,\n",
+ " outdir: './out' }\n",
+ "info: (yuidoc): Scanning for yuidoc.json file.\n",
+ "info: (server): Starting server: http://127.0.0.1:3000\n",
+ "```\n",
+ "\n",
+ "and browse http://127.0.0.1:3000 to get documentation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "foo": true
+ },
+ "source": [
+ "#### Some convenience methods"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "By browsing the documentation you will see that we have some convenience methods that allows us to avoid re-inventing the UI every time :\n",
+ "```javascript\n",
+ "Jupyter.toolbar.add_buttons_group([\n",
+ " {\n",
+ " 'label' : 'run qtconsole',\n",
+ " 'icon' : 'icon-terminal', // select your icon from \n",
+ " // http://fortawesome.github.io/Font-Awesome/icons/\n",
+ " 'callback': function(){Jupyter.notebook.kernel.execute('%qtconsole')}\n",
+ " }\n",
+ " // add more button here if needed.\n",
+ " ]);\n",
+ "```\n",
+ "with a [lot of icons] you can select from. \n",
+ "\n",
+ "[lot of icons]: http://fortawesome.github.io/Font-Awesome/icons/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "foo": true
+ },
+ "source": [
+ "## Cell Metadata"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "foo": true
+ },
+ "source": [
+ "The most requested feature is generally to be able to distinguish an individual cell in the notebook, or run a specific action with them.\n",
+ "To do so, you can either use `Jupyter.notebook.get_selected_cell()`, or rely on `CellToolbar`. This allows you to register a set of actions and graphical elements that will be attached to individual cells."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Cell Toolbar"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can see some example of what can be done by toggling the `Cell Toolbar` selector in the toolbar on top of the notebook. It provides two default `presets` that are `Default` and `slideshow`. Default allows the user to edit the metadata attached to each cell manually."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "First we define a function that takes at first parameter an element on the DOM in which to inject UI element. The second element is the cell this element wis registered with. Then we will need to register that function and give it a name.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Register a callback"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%javascript\n",
+ "var CellToolbar = Jupyter.CellToolbar\n",
+ "var toggle = function(div, cell) {\n",
+ " var button_container = $(div)\n",
+ "\n",
+ " // let's create a button that shows the current value of the metadata\n",
+ " var button = $('<button/>').addClass('btn btn-mini').text(String(cell.metadata.foo));\n",
+ "\n",
+ " // On click, change the metadata value and update the button label\n",
+ " button.click(function(){\n",
+ " var v = cell.metadata.foo;\n",
+ " cell.metadata.foo = !v;\n",
+ " button.text(String(!v));\n",
+ " })\n",
+ "\n",
+ " // add the button to the DOM div.\n",
+ " button_container.append(button);\n",
+ "}\n",
+ "\n",
+ " // now we register the callback under the name foo to give the\n",
+ " // user the ability to use it later\n",
+ " CellToolbar.register_callback('tuto.foo', toggle);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Registering a preset"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This function can now be part of many `preset` of the CellToolBar."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false,
+ "foo": true,
+ "slideshow": {
+ "slide_type": "subslide"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "%%javascript\n",
+ "Jupyter.CellToolbar.register_preset('Tutorial 1',['tuto.foo','default.rawedit'])\n",
+ "Jupyter.CellToolbar.register_preset('Tutorial 2',['slideshow.select','tuto.foo'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You should now have access to two presets :\n",
+ "\n",
+ " - Tutorial 1\n",
+ " - Tutorial 2\n",
+ " \n",
+ "And check that the buttons you defined share state when you toggle preset. \n",
+ "Also check that the metadata of the cell is modified when you click the button, and that when saved on reloaded the metadata is still available."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Exercise:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Try to wrap the all code in a file, put this file in `{jupyter_dir}/custom/<a-name>.js`, and add \n",
+ "\n",
+ "```\n",
+ "require(['custom/<a-name>']);\n",
+ "```\n",
+ "\n",
+ "in `custom.js` to have this script automatically loaded in all your notebooks.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`require` is provided by a [javascript library](http://requirejs.org/) that allow you to express dependency. For simple extension like the previous one we directly mute the global namespace, but for more complex extension you could pass a callback to `require([...], <callback>)` call, to allow the user to pass configuration information to your plugin.\n",
+ "\n",
+ "In Python lang, \n",
+ "\n",
+ "```javascript\n",
+ "require(['a/b', 'c/d'], function( e, f){\n",
+ " e.something()\n",
+ " f.something()\n",
+ "})\n",
+ "```\n",
+ "\n",
+ "could be read as\n",
+ "```python\n",
+ "import a.b as e\n",
+ "import c.d as f\n",
+ "e.something()\n",
+ "f.something()\n",
+ "```\n",
+ "\n",
+ "\n",
+ "See for example @damianavila [\"ZenMode\" plugin](https://github.com/ipython-contrib/IPython-notebook-extensions/blob/master/custom.example.js#L34) :\n",
+ "\n",
+ "```javascript\n",
+ "\n",
+ "// read that as\n",
+ "// import custom.zenmode.main as zenmode\n",
+ "require(['custom/zenmode/main'],function(zenmode){\n",
+ " zenmode.background('images/back12.jpg');\n",
+ "})\n",
+ "```\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### For the quickest"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Try to use [the following](https://github.com/ipython/ipython/blob/1.x/IPython/html/static/notebook/js/celltoolbar.js#L367) to bind a dropdown list to `cell.metadata.difficulty.select`. \n",
+ "\n",
+ "It should be able to take the 4 following values :\n",
+ "\n",
+ " - `<None>`\n",
+ " - `Easy`\n",
+ " - `Medium`\n",
+ " - `Hard`\n",
+ " \n",
+ "We will use it to customiZe the output of the converted notebook depending on the tag on each cell"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%load soln/celldiff.js"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/Notebook Basics.ipynb b/docs/source/examples/Notebook/Notebook Basics.ipynb
new file mode 100644
index 0000000..06375c8
--- /dev/null
+++ b/docs/source/examples/Notebook/Notebook Basics.ipynb
@@ -0,0 +1,253 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Notebook Basics"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The Notebook dashboard"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "When you first start the notebook server, your browser will open to the notebook dashboard. The dashboard serves as a home page for the notebook. Its main purpose is to display the notebooks and files in the current directory. For example, here is a screenshot of the dashboard page for the `examples` directory in the Jupyter repository:\n",
+ "\n",
+ "<img src=\"images/dashboard_files_tab.png\" width=\"791px\"/>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The top of the notebook list displays clickable breadcrumbs of the current directory. By clicking on these breadcrumbs or on sub-directories in the notebook list, you can navigate your file system.\n",
+ "\n",
+ "To create a new notebook, click on the \"New\" button at the top of the list and select a kernel from the dropdown (as seen below). Which kernels are listed depend on what's installed on the server. Some of the kernels in the screenshot below may not exist as an option to you.\n",
+ "\n",
+ "<img src=\"images/dashboard_files_tab_new.png\" width=\"202px\" />"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Notebooks and files can be uploaded to the current directory by dragging a notebook file onto the notebook list or by the \"click here\" text above the list.\n",
+ "\n",
+ "The notebook list shows green \"Running\" text and a green notebook icon next to running notebooks (as seen below). Notebooks remain running until you explicitly shut them down; closing the notebook's page is not sufficient.\n",
+ "\n",
+ "<img src=\"images/dashboard_files_tab_run.png\" width=\"777px\"/>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To shutdown, delete, duplicate, or rename a notebook check the checkbox next to it and an array of controls will appear at the top of the notebook list (as seen below). You can also use the same operations on directories and files when applicable.\n",
+ "\n",
+ "<img src=\"images/dashboard_files_tab_btns.png\" width=\"301px\" />"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To see all of your running notebooks along with their directories, click on the \"Running\" tab:\n",
+ "\n",
+ "<img src=\"images/dashboard_running_tab.png\" width=\"786px\" />\n",
+ "\n",
+ "This view provides a convenient way to track notebooks that you start as you navigate the file system in a long running notebook server."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Overview of the Notebook UI"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If you create a new notebook or open an existing one, you will be taken to the notebook user interface (UI). This UI allows you to run code and author notebook documents interactively. The notebook UI has the following main areas:\n",
+ "\n",
+ "* Menu\n",
+ "* Toolbar\n",
+ "* Notebook area and cells\n",
+ "\n",
+ "The notebook has an interactive tour of these elements that can be started in the \"Help:User Interface Tour\" menu item."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Modal editor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Starting with IPython 2.0, the Jupyter Notebook has a modal user interface. This means that the keyboard does different things depending on which mode the Notebook is in. There are two modes: edit mode and command mode."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Edit mode"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Edit mode is indicated by a green cell border and a prompt showing in the editor area:\n",
+ "\n",
+ "<img src=\"images/edit_mode.png\">\n",
+ "\n",
+ "When a cell is in edit mode, you can type into the cell, like a normal text editor."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "<div class=\"alert alert-success\">\n",
+ "Enter edit mode by pressing `Enter` or using the mouse to click on a cell's editor area.\n",
+ "</div>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Command mode"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Command mode is indicated by a grey cell border with a blue left margin:\n",
+ "\n",
+ "<img src=\"images/command_mode.png\">\n",
+ "\n",
+ "When you are in command mode, you are able to edit the notebook as a whole, but not type into individual cells. Most importantly, in command mode, the keyboard is mapped to a set of shortcuts that let you perform notebook and cell actions efficiently. For example, if you are in command mode and you press `c`, you will copy the current cell - no modifier is needed."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "<div class=\"alert alert-error\">\n",
+ "Don't try to type into a cell in command mode; unexpected things will happen!\n",
+ "</div>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "<div class=\"alert alert-success\">\n",
+ "Enter command mode by pressing `Esc` or using the mouse to click *outside* a cell's editor area.\n",
+ "</div>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Mouse navigation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "All navigation and actions in the Notebook are available using the mouse through the menubar and toolbar, which are both above the main Notebook area:\n",
+ "\n",
+ "<img src=\"images/menubar_toolbar.png\" width=\"786px\" />"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The first idea of mouse based navigation is that **cells can be selected by clicking on them.** The currently selected cell gets a grey or green border depending on whether the notebook is in edit or command mode. If you click inside a cell's editor area, you will enter edit mode. If you click on the prompt or output area of a cell you will enter command mode.\n",
+ "\n",
+ "If you are running this notebook in a live session (not on http://nbviewer.jupyter.org) try selecting different cells and going between edit and command mode. Try typing into a cell."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The second idea of mouse based navigation is that **cell actions usually apply to the currently selected cell**. Thus if you want to run the code in a cell, you would select it and click the <button class='btn btn-default btn-xs'><i class=\"fa fa-step-forward icon-step-forward\"></i></button> button in the toolbar or the \"Cell:Run\" menu item. Similarly, to copy a cell you would select it and click the <button class='btn btn-default btn-xs'><i class=\"fa fa-copy icon-copy\"></i></button> button in the toolbar or the \"Edit:Copy\" menu item. With this simple pattern, you should be able to do most everything you need with the mouse.\n",
+ "\n",
+ "Markdown and heading cells have one other state that can be modified with the mouse. These cells can either be rendered or unrendered. When they are rendered, you will see a nice formatted representation of the cell's contents. When they are unrendered, you will see the raw text source of the cell. To render the selected cell with the mouse, click the <button class='btn btn-default btn-xs'><i class=\"fa fa-step-forward icon-step-forward\"></i></button> button in the toolbar or the \"Cell:Run\" menu item. To unrender the selected cell, double click on the cell."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Keyboard Navigation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The modal user interface of the Jupyter Notebook has been optimized for efficient keyboard usage. This is made possible by having two different sets of keyboard shortcuts: one set that is active in edit mode and another in command mode.\n",
+ "\n",
+ "The most important keyboard shortcuts are `Enter`, which enters edit mode, and `Esc`, which enters command mode.\n",
+ "\n",
+ "In edit mode, most of the keyboard is dedicated to typing into the cell's editor. Thus, in edit mode there are relatively few shortcuts. In command mode, the entire keyboard is available for shortcuts, so there are many more. The `Help`->`Keyboard Shortcuts` dialog lists the available shortcuts."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We recommend learning the command mode shortcuts in the following rough order:\n",
+ "\n",
+ "1. Basic navigation: `enter`, `shift-enter`, `up/k`, `down/j`\n",
+ "2. Saving the notebook: `s`\n",
+ "2. Change Cell types: `y`, `m`, `1-6`, `t`\n",
+ "3. Cell creation: `a`, `b`\n",
+ "4. Cell editing: `x`, `c`, `v`, `d`, `z`\n",
+ "5. Kernel operations: `i`, `0` (press twice)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 2",
+ "language": "python",
+ "name": "python2"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2",
+ "version": "2.7.11"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/Running Code.ipynb b/docs/source/examples/Notebook/Running Code.ipynb
new file mode 100644
index 0000000..d56065c
--- /dev/null
+++ b/docs/source/examples/Notebook/Running Code.ipynb
@@ -0,0 +1,289 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Running Code"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "First and foremost, the Jupyter Notebook is an interactive environment for writing and running code. The notebook is capable of running code in a wide range of languages. However, each notebook is associated with a single kernel. This notebook is associated with the IPython kernel, therefor runs Python code."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Code cells allow you to enter and run code"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Run a code cell using `Shift-Enter` or pressing the <button class='btn btn-default btn-xs'><i class=\"icon-step-forward fa fa-step-forward\"></i></button> button in the toolbar above:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "a = 10"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "print(a)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "There are two other keyboard shortcuts for running code:\n",
+ "\n",
+ "* `Alt-Enter` runs the current cell and inserts a new one below.\n",
+ "* `Ctrl-Enter` run the current cell and enters command mode."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Managing the Kernel"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Code is run in a separate process called the Kernel. The Kernel can be interrupted or restarted. Try running the following cell and then hit the <button class='btn btn-default btn-xs'><i class='icon-stop fa fa-stop'></i></button> button in the toolbar above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "import time\n",
+ "time.sleep(10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If the Kernel dies you will be prompted to restart it. Here we call the low-level system libc.time routine with the wrong argument via\n",
+ "ctypes to segfault the Python interpreter:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "from ctypes import CDLL\n",
+ "# This will crash a Linux or Mac system\n",
+ "# equivalent calls can be made on Windows\n",
+ "dll = 'dylib' if sys.platform == 'darwin' else 'so.6'\n",
+ "libc = CDLL(\"libc.%s\" % dll) \n",
+ "libc.time(-1) # BOOM!!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Cell menu"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The \"Cell\" menu has a number of menu items for running code in different ways. These includes:\n",
+ "\n",
+ "* Run and Select Below\n",
+ "* Run and Insert Below\n",
+ "* Run All\n",
+ "* Run All Above\n",
+ "* Run All Below"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Restarting the kernels"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The kernel maintains the state of a notebook's computations. You can reset this state by restarting the kernel. This is done by clicking on the <button class='btn btn-default btn-xs'><i class='fa fa-repeat icon-repeat'></i></button> in the toolbar above."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## sys.stdout and sys.stderr"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The stdout and stderr streams are displayed as text in the output area."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "print(\"hi, stdout\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from __future__ import print_function\n",
+ "print('hi, stderr', file=sys.stderr)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Output is asynchronous"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "All output is displayed asynchronously as it is generated in the Kernel. If you execute the next cell, you will see the output one piece at a time, not all at the end."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "import time, sys\n",
+ "for i in range(8):\n",
+ " print(i)\n",
+ " time.sleep(0.5)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Large outputs"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To better handle large outputs, the output area can be collapsed. Run the following cell and then single- or double- click on the active area to the left of the output:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "for i in range(50):\n",
+ " print(i)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Beyond a certain point, output will scroll automatically:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "for i in range(500):\n",
+ " print(2**i - 1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/Typesetting Equations.ipynb b/docs/source/examples/Notebook/Typesetting Equations.ipynb
new file mode 100644
index 0000000..1808c6e
--- /dev/null
+++ b/docs/source/examples/Notebook/Typesetting Equations.ipynb
@@ -0,0 +1,274 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The Markdown parser included in the Jupyter Notebook is MathJax-aware. This means that you can freely mix in mathematical expressions using the [MathJax subset of Tex and LaTeX](http://docs.mathjax.org/en/latest/tex.html#tex-support). [Some examples from the MathJax site](http://www.mathjax.org/demos/tex-samples/) are reproduced below, as well as the Markdown+TeX source."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Motivating Examples\n",
+ "\n",
+ "## The Lorenz Equations\n",
+ "### Source\n",
+ "```\n",
+ "\\begin{align}\n",
+ "\\dot{x} & = \\sigma(y-x) \\\\\n",
+ "\\dot{y} & = \\rho x - y - xz \\\\\n",
+ "\\dot{z} & = -\\beta z + xy\n",
+ "\\end{align}\n",
+ "```\n",
+ "### Display\n",
+ "\\begin{align}\n",
+ "\\dot{x} & = \\sigma(y-x) \\\\\n",
+ "\\dot{y} & = \\rho x - y - xz \\\\\n",
+ "\\dot{z} & = -\\beta z + xy\n",
+ "\\end{align}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The Cauchy-Schwarz Inequality\n",
+ "### Source\n",
+ "```\n",
+ "\\begin{equation*}\n",
+ "\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)\n",
+ "\\end{equation*}\n",
+ "```\n",
+ "### Display\n",
+ "\\begin{equation*}\n",
+ "\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)\n",
+ "\\end{equation*}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## A Cross Product Formula\n",
+ "### Source\n",
+ "```\n",
+ "\\begin{equation*}\n",
+ "\\mathbf{V}_1 \\times \\mathbf{V}_2 = \\begin{vmatrix}\n",
+ "\\mathbf{i} & \\mathbf{j} & \\mathbf{k} \\\\\n",
+ "\\frac{\\partial X}{\\partial u} & \\frac{\\partial Y}{\\partial u} & 0 \\\\\n",
+ "\\frac{\\partial X}{\\partial v} & \\frac{\\partial Y}{\\partial v} & 0\n",
+ "\\end{vmatrix} \n",
+ "\\end{equation*}\n",
+ "```\n",
+ "### Display\n",
+ "\\begin{equation*}\n",
+ "\\mathbf{V}_1 \\times \\mathbf{V}_2 = \\begin{vmatrix}\n",
+ "\\mathbf{i} & \\mathbf{j} & \\mathbf{k} \\\\\n",
+ "\\frac{\\partial X}{\\partial u} & \\frac{\\partial Y}{\\partial u} & 0 \\\\\n",
+ "\\frac{\\partial X}{\\partial v} & \\frac{\\partial Y}{\\partial v} & 0\n",
+ "\\end{vmatrix} \n",
+ "\\end{equation*}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## The probability of getting \\(k\\) heads when flipping \\(n\\) coins is\n",
+ "### Source\n",
+ "```\n",
+ "\\begin{equation*}\n",
+ "P(E) = {n \\choose k} p^k (1-p)^{ n-k} \n",
+ "\\end{equation*}\n",
+ "```\n",
+ "### Display\n",
+ "\\begin{equation*}\n",
+ "P(E) = {n \\choose k} p^k (1-p)^{ n-k} \n",
+ "\\end{equation*}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## An Identity of Ramanujan\n",
+ "### Source\n",
+ "```\n",
+ "\\begin{equation*}\n",
+ "\\frac{1}{\\Bigl(\\sqrt{\\phi \\sqrt{5}}-\\phi\\Bigr) e^{\\frac25 \\pi}} =\n",
+ "1+\\frac{e^{-2\\pi}} {1+\\frac{e^{-4\\pi}} {1+\\frac{e^{-6\\pi}}\n",
+ "{1+\\frac{e^{-8\\pi}} {1+\\ldots} } } } \n",
+ "\\end{equation*}\n",
+ "```\n",
+ "### Display\n",
+ "\\begin{equation*}\n",
+ "\\frac{1}{\\Bigl(\\sqrt{\\phi \\sqrt{5}}-\\phi\\Bigr) e^{\\frac25 \\pi}} =\n",
+ "1+\\frac{e^{-2\\pi}} {1+\\frac{e^{-4\\pi}} {1+\\frac{e^{-6\\pi}}\n",
+ "{1+\\frac{e^{-8\\pi}} {1+\\ldots} } } } \n",
+ "\\end{equation*}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## A Rogers-Ramanujan Identity\n",
+ "### Source\n",
+ "```\n",
+ "\\begin{equation*}\n",
+ "1 + \\frac{q^2}{(1-q)}+\\frac{q^6}{(1-q)(1-q^2)}+\\cdots =\n",
+ "\\prod_{j=0}^{\\infty}\\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},\n",
+ "\\quad\\quad \\text{for $|q|<1$}. \n",
+ "\\end{equation*}\n",
+ "```\n",
+ "### Display\n",
+ "\\begin{equation*}\n",
+ "1 + \\frac{q^2}{(1-q)}+\\frac{q^6}{(1-q)(1-q^2)}+\\cdots =\n",
+ "\\prod_{j=0}^{\\infty}\\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},\n",
+ "\\quad\\quad \\text{for $|q|<1$}. \n",
+ "\\end{equation*}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Maxwell's Equations\n",
+ "### Source\n",
+ "```\n",
+ "\\begin{align}\n",
+ "\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} & = \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\\\ \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n",
+ "\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n",
+ "\\nabla \\cdot \\vec{\\mathbf{B}} & = 0 \n",
+ "\\end{align}\n",
+ "```\n",
+ "### Display\n",
+ "\\begin{align}\n",
+ "\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} & = \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\\\ \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n",
+ "\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n",
+ "\\nabla \\cdot \\vec{\\mathbf{B}} & = 0 \n",
+ "\\end{align}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Equation Numbering and References\n",
+ "\n",
+ "Equation numbering and referencing will be available in a future version of the Jupyter notebook."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Inline Typesetting (Mixing Markdown and TeX)\n",
+ "\n",
+ "While display equations look good for a page of samples, the ability to mix math and *formatted* **text** in a paragraph is also important.\n",
+ "\n",
+ "### Source\n",
+ "```\n",
+ "This expression $\\sqrt{3x-1}+(1+x)^2$ is an example of a TeX inline equation in a [Markdown-formatted](http://daringfireball.net/projects/markdown/) sentence. \n",
+ "```\n",
+ "\n",
+ "### Display\n",
+ "This expression $\\sqrt{3x-1}+(1+x)^2$ is an example of a TeX inline equation in a [Markdown-formatted](http://daringfireball.net/projects/markdown/) sentence. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Other Syntax\n",
+ "\n",
+ "You will notice in other places on the web that `$$` are needed explicitly to begin and end MathJax typesetting. This is **not** required if you will be using TeX environments, but the Jupyter notebook will accept this syntax on legacy notebooks. \n",
+ "\n",
+ "## Source\n",
+ "\n",
+ "```\n",
+ "$$\n",
+ "\\begin{array}{c}\n",
+ "y_1 \\\\\\\n",
+ "y_2 \\mathtt{t}_i \\\\\\\n",
+ "z_{3,4}\n",
+ "\\end{array}\n",
+ "$$\n",
+ "```\n",
+ "\n",
+ "```\n",
+ "$$\n",
+ "\\begin{array}{c}\n",
+ "y_1 \\cr\n",
+ "y_2 \\mathtt{t}_i \\cr\n",
+ "y_{3}\n",
+ "\\end{array}\n",
+ "$$\n",
+ "```\n",
+ "\n",
+ "```\n",
+ "$$\\begin{eqnarray} \n",
+ "x' &=& &x \\sin\\phi &+& z \\cos\\phi \\\\\n",
+ "z' &=& - &x \\cos\\phi &+& z \\sin\\phi \\\\\n",
+ "\\end{eqnarray}$$\n",
+ "```\n",
+ "\n",
+ "```\n",
+ "$$\n",
+ "x=4\n",
+ "$$\n",
+ "```\n",
+ "\n",
+ "## Display\n",
+ "\n",
+ "$$\n",
+ "\\begin{array}{c}\n",
+ "y_1 \\\\\\\n",
+ "y_2 \\mathtt{t}_i \\\\\\\n",
+ "z_{3,4}\n",
+ "\\end{array}\n",
+ "$$\n",
+ "\n",
+ "$$\n",
+ "\\begin{array}{c}\n",
+ "y_1 \\cr\n",
+ "y_2 \\mathtt{t}_i \\cr\n",
+ "y_{3}\n",
+ "\\end{array}\n",
+ "$$\n",
+ "\n",
+ "$$\\begin{eqnarray} \n",
+ "x' &=& &x \\sin\\phi &+& z \\cos\\phi \\\\\n",
+ "z' &=& - &x \\cos\\phi &+& z \\sin\\phi \\\\\n",
+ "\\end{eqnarray}$$\n",
+ "\n",
+ "$$\n",
+ "x=4\n",
+ "$$"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/What is the Jupyter Notebook.ipynb b/docs/source/examples/Notebook/What is the Jupyter Notebook.ipynb
new file mode 100644
index 0000000..a5edb80
--- /dev/null
+++ b/docs/source/examples/Notebook/What is the Jupyter Notebook.ipynb
@@ -0,0 +1,182 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "# What is the Jupyter Notebook?"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Introduction"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The Jupyter Notebook is an **interactive computing environment** that enables users to author notebook documents that include: \n",
+ "- Live code\n",
+ "- Interactive widgets\n",
+ "- Plots\n",
+ "- Narrative text\n",
+ "- Equations\n",
+ "- Images\n",
+ "- Video\n",
+ "\n",
+ "These documents provide a **complete and self-contained record of a computation** that can be converted to various formats and shared with others using email, [Dropbox](http://dropbox.com), version control systems (like git/[GitHub](http://github.com)) or [nbviewer.jupyter.org](http://nbviewer.jupyter.org)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "### Components"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The Jupyter Notebook combines three components:\n",
+ "\n",
+ "* **The notebook web application**: An interactive web application for writing and running code interactively and authoring notebook documents.\n",
+ "* **Kernels**: Separate processes started by the notebook web application that runs users' code in a given language and returns output back to the notebook web application. The kernel also handles things like computations for interactive widgets, tab completion and introspection. \n",
+ "* **Notebook documents**: Self-contained documents that contain a representation of all content visible in the notebook web application, including inputs and outputs of the computations, narrative\n",
+ "text, equations, images, and rich media representations of objects. Each notebook document has its own kernel."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "## Notebook web application"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The notebook web application enables users to:\n",
+ "\n",
+ "* **Edit code in the browser**, with automatic syntax highlighting, indentation, and tab completion/introspection.\n",
+ "* **Run code from the browser**, with the results of computations attached to the code which generated them.\n",
+ "* See the results of computations with **rich media representations**, such as HTML, LaTeX, PNG, SVG, PDF, etc.\n",
+ "* Create and use **interactive JavaScript widgets**, which bind interactive user interface controls and visualizations to reactive kernel side computations.\n",
+ "* Author **narrative text** using the [Markdown](https://daringfireball.net/projects/markdown/) markup language.\n",
+ "* Build **hierarchical documents** that are organized into sections with different levels of headings.\n",
+ "* Include mathematical equations using **LaTeX syntax in Markdown**, which are rendered in-browser by [MathJax](http://www.mathjax.org/)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "## Kernels"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Through Jupyter's kernel and messaging architecture, the Notebook allows code to be run in a range of different programming languages. For each notebook document that a user opens, the web application starts a kernel that runs the code for that notebook. Each kernel is capable of running code in a single programming language and there are kernels available in the following languages:\n",
+ "\n",
+ "* Python(https://github.com/ipython/ipython)\n",
+ "* Julia (https://github.com/JuliaLang/IJulia.jl)\n",
+ "* R (https://github.com/takluyver/IRkernel)\n",
+ "* Ruby (https://github.com/minrk/iruby)\n",
+ "* Haskell (https://github.com/gibiansky/IHaskell)\n",
+ "* Scala (https://github.com/Bridgewater/scala-notebook)\n",
+ "* node.js (https://gist.github.com/Carreau/4279371)\n",
+ "* Go (https://github.com/takluyver/igo)\n",
+ "\n",
+ "The default kernel runs Python code. The notebook provides a simple way for users to pick which of these kernels is used for a given notebook. \n",
+ "\n",
+ "Each of these kernels communicate with the notebook web application and web browser using a JSON over ZeroMQ/WebSockets message protocol that is described [here](http://ipython.org/ipython-doc/dev/development/messaging.html). Most users don't need to know about these details, but it helps to understand that \"kernels run code.\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "## Notebook documents"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Notebook documents contain the **inputs and outputs** of an interactive session as well as **narrative text** that accompanies the code but is not meant for execution. **Rich output** generated by running code, including HTML, images, video, and plots, is embeddeed in the notebook, which makes it a complete and self-contained record of a computation. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "When you run the notebook web application on your computer, notebook documents are just **files on your local filesystem with a `.ipynb` extension**. This allows you to use familiar workflows for organizing your notebooks into folders and sharing them with others."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Notebooks consist of a **linear sequence of cells**. There are four basic cell types:\n",
+ "\n",
+ "* **Code cells:** Input and output of live code that is run in the kernel\n",
+ "* **Markdown cells:** Narrative text with embedded LaTeX equations\n",
+ "* **Heading cells:** 6 levels of hierarchical organization and formatting\n",
+ "* **Raw cells:** Unformatted text that is included, without modification, when notebooks are converted to different formats using nbconvert\n",
+ "\n",
+ "Internally, notebook documents are **[JSON](http://en.wikipedia.org/wiki/JSON) data** with **binary values [base64](http://en.wikipedia.org/wiki/Base64)** encoded. This allows them to be **read and manipulated programmatically** by any programming language. Because JSON is a text format, notebook documents are version control friendly.\n",
+ "\n",
+ "**Notebooks can be exported** to different static formats including HTML, reStructeredText, LaTeX, PDF, and slide shows ([reveal.js](http://lab.hakim.se/reveal-js/#/)) using Jupyter's `nbconvert` utility.\n",
+ "\n",
+ "Furthermore, any notebook document available from a **public URL on or GitHub can be shared** via [nbviewer](http://nbviewer.ipython.org). This service loads the notebook document from the URL and renders it as a static web page. The resulting web page may thus be shared with others **without their needing to install the Jupyter Notebook**."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/Working With Markdown Cells.ipynb b/docs/source/examples/Notebook/Working With Markdown Cells.ipynb
new file mode 100644
index 0000000..aabccf1
--- /dev/null
+++ b/docs/source/examples/Notebook/Working With Markdown Cells.ipynb
@@ -0,0 +1,322 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Markdown Cells"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Text can be added to Jupyter Notebooks using Markdown cells. Markdown is a popular markup language that is a superset of HTML. Its specification can be found here:\n",
+ "\n",
+ "<http://daringfireball.net/projects/markdown/>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Markdown basics"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can make text *italic* or **bold**."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can build nested itemized or enumerated lists:\n",
+ "\n",
+ "* One\n",
+ " - Sublist\n",
+ " - This\n",
+ " - Sublist\n",
+ " - That\n",
+ " - The other thing\n",
+ "* Two\n",
+ " - Sublist\n",
+ "* Three\n",
+ " - Sublist\n",
+ "\n",
+ "Now another list:\n",
+ "\n",
+ "1. Here we go\n",
+ " 1. Sublist\n",
+ " 2. Sublist\n",
+ "2. There we go\n",
+ "3. Now this"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can add horizontal rules:\n",
+ "\n",
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here is a blockquote:\n",
+ "\n",
+ "> Beautiful is better than ugly.\n",
+ "> Explicit is better than implicit.\n",
+ "> Simple is better than complex.\n",
+ "> Complex is better than complicated.\n",
+ "> Flat is better than nested.\n",
+ "> Sparse is better than dense.\n",
+ "> Readability counts.\n",
+ "> Special cases aren't special enough to break the rules.\n",
+ "> Although practicality beats purity.\n",
+ "> Errors should never pass silently.\n",
+ "> Unless explicitly silenced.\n",
+ "> In the face of ambiguity, refuse the temptation to guess.\n",
+ "> There should be one-- and preferably only one --obvious way to do it.\n",
+ "> Although that way may not be obvious at first unless you're Dutch.\n",
+ "> Now is better than never.\n",
+ "> Although never is often better than *right* now.\n",
+ "> If the implementation is hard to explain, it's a bad idea.\n",
+ "> If the implementation is easy to explain, it may be a good idea.\n",
+ "> Namespaces are one honking great idea -- let's do more of those!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "And shorthand for links:\n",
+ "\n",
+ "[Jupyter's website](http://jupyter.org)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Headings"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can add headings by starting a line with one (or multiple) `#` followed by a space, as in the following example:\n",
+ "\n",
+ "# Heading 1\n",
+ "# Heading 2\n",
+ "## Heading 2.1\n",
+ "## Heading 2.2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Embedded code"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can embed code meant for illustration instead of execution in Python:\n",
+ "\n",
+ " def f(x):\n",
+ " \"\"\"a docstring\"\"\"\n",
+ " return x**2\n",
+ "\n",
+ "or other languages:\n",
+ "\n",
+ " if (i=0; i<n; i++) {\n",
+ " printf(\"hello %d\\n\", i);\n",
+ " x += 4;\n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## LaTeX equations"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Courtesy of MathJax, you can include mathematical expressions both inline: \n",
+ "$e^{i\\pi} + 1 = 0$ and displayed:\n",
+ "\n",
+ "$$e^x=\\sum_{i=0}^\\infty \\frac{1}{i!}x^i$$\n",
+ "Inline expressions can be added by surrounding the latex code with `$`:\n",
+ "```\n",
+ "$e^{i\\pi} + 1 = 0$\n",
+ "```\n",
+ "\n",
+ "Expressions on their own line are surrounded by `$$`:\n",
+ "```latex\n",
+ "$$e^x=\\sum_{i=0}^\\infty \\frac{1}{i!}x^i$$\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Github flavored markdown (GFM)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The Notebook webapp support Github flavored markdown meaning that you can use triple backticks for code blocks \n",
+ "<pre>\n",
+ "```python\n",
+ "print \"Hello World\"\n",
+ "```\n",
+ "\n",
+ "```javascript\n",
+ "console.log(\"Hello World\")\n",
+ "```\n",
+ "</pre>\n",
+ "\n",
+ "Gives \n",
+ "```python\n",
+ "print \"Hello World\"\n",
+ "```\n",
+ "\n",
+ "```javascript\n",
+ "console.log(\"Hello World\")\n",
+ "```\n",
+ "\n",
+ "And a table like this : \n",
+ "\n",
+ "<pre>\n",
+ "| This | is |\n",
+ "|------|------|\n",
+ "| a | table| \n",
+ "</pre>\n",
+ "\n",
+ "A nice Html Table\n",
+ "\n",
+ "| This | is |\n",
+ "|------|------|\n",
+ "| a | table| "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## General HTML"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Because Markdown is a superset of HTML you can even add things like HTML tables:\n",
+ "\n",
+ "<table>\n",
+ "<tr>\n",
+ "<th>Header 1</th>\n",
+ "<th>Header 2</th>\n",
+ "</tr>\n",
+ "<tr>\n",
+ "<td>row 1, cell 1</td>\n",
+ "<td>row 1, cell 2</td>\n",
+ "</tr>\n",
+ "<tr>\n",
+ "<td>row 2, cell 1</td>\n",
+ "<td>row 2, cell 2</td>\n",
+ "</tr>\n",
+ "</table>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Local files"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If you have local files in your Notebook directory, you can refer to these files in Markdown cells directly:\n",
+ "\n",
+ " [subdirectory/]<filename>\n",
+ "\n",
+ "For example, in the images folder, we have the Python logo:\n",
+ "\n",
+ " <img src=\"../images/python_logo.svg\" />\n",
+ "\n",
+ "<img src=\"../images/python_logo.svg\" />\n",
+ "\n",
+ "and a video with the HTML5 video tag:\n",
+ "\n",
+ " <video controls src=\"images/animation.m4v\" />\n",
+ "\n",
+ "<video controls src=\"images/animation.m4v\" />\n",
+ "\n",
+ "These do not embed the data into the notebook file, and require that the files exist when you are viewing the notebook."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Security of local files"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that this means that the Jupyter notebook server also acts as a generic file server\n",
+ "for files inside the same tree as your notebooks. Access is not granted outside the\n",
+ "notebook folder so you have strict control over what files are visible, but for this\n",
+ "reason it is highly recommended that you do not run the notebook server with a notebook\n",
+ "directory at a high level in your filesystem (e.g. your home directory).\n",
+ "\n",
+ "When you run the notebook in a password-protected manner, local file access is restricted\n",
+ "to authenticated users unless read-only views are active."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/images/command_mode.png b/docs/source/examples/Notebook/images/command_mode.png
new file mode 100644
index 0000000..4482de3
--- /dev/null
+++ b/docs/source/examples/Notebook/images/command_mode.png
Binary files differ
diff --git a/docs/source/examples/Notebook/images/dashboard_files_tab.png b/docs/source/examples/Notebook/images/dashboard_files_tab.png
new file mode 100644
index 0000000..1809d7c
--- /dev/null
+++ b/docs/source/examples/Notebook/images/dashboard_files_tab.png
Binary files differ
diff --git a/docs/source/examples/Notebook/images/dashboard_files_tab_btns.png b/docs/source/examples/Notebook/images/dashboard_files_tab_btns.png
new file mode 100644
index 0000000..b76af88
--- /dev/null
+++ b/docs/source/examples/Notebook/images/dashboard_files_tab_btns.png
Binary files differ
diff --git a/docs/source/examples/Notebook/images/dashboard_files_tab_new.png b/docs/source/examples/Notebook/images/dashboard_files_tab_new.png
new file mode 100644
index 0000000..d0d02f8
--- /dev/null
+++ b/docs/source/examples/Notebook/images/dashboard_files_tab_new.png
Binary files differ
diff --git a/docs/source/examples/Notebook/images/dashboard_files_tab_run.png b/docs/source/examples/Notebook/images/dashboard_files_tab_run.png
new file mode 100644
index 0000000..65c7259
--- /dev/null
+++ b/docs/source/examples/Notebook/images/dashboard_files_tab_run.png
Binary files differ
diff --git a/docs/source/examples/Notebook/images/dashboard_running_tab.png b/docs/source/examples/Notebook/images/dashboard_running_tab.png
new file mode 100644
index 0000000..54f2bd2
--- /dev/null
+++ b/docs/source/examples/Notebook/images/dashboard_running_tab.png
Binary files differ
diff --git a/docs/source/examples/Notebook/images/edit_mode.png b/docs/source/examples/Notebook/images/edit_mode.png
new file mode 100644
index 0000000..9d52aaa
--- /dev/null
+++ b/docs/source/examples/Notebook/images/edit_mode.png
Binary files differ
diff --git a/docs/source/examples/Notebook/images/menubar_toolbar.png b/docs/source/examples/Notebook/images/menubar_toolbar.png
new file mode 100644
index 0000000..c22d960
--- /dev/null
+++ b/docs/source/examples/Notebook/images/menubar_toolbar.png
Binary files differ
diff --git a/docs/source/examples/Notebook/images/nbconvert_arch.png b/docs/source/examples/Notebook/images/nbconvert_arch.png
new file mode 100644
index 0000000..27cd69b
--- /dev/null
+++ b/docs/source/examples/Notebook/images/nbconvert_arch.png
Binary files differ
diff --git a/docs/source/examples/Notebook/nbpackage/__init__.py b/docs/source/examples/Notebook/nbpackage/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/source/examples/Notebook/nbpackage/__init__.py
diff --git a/docs/source/examples/Notebook/nbpackage/mynotebook.ipynb b/docs/source/examples/Notebook/nbpackage/mynotebook.ipynb
new file mode 100644
index 0000000..fd3920c
--- /dev/null
+++ b/docs/source/examples/Notebook/nbpackage/mynotebook.ipynb
@@ -0,0 +1,69 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# My Notebook"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "def foo():\n",
+ " return \"foo\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "def has_ip_syntax():\n",
+ " listing = !ls\n",
+ " return listing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "def whatsmyname():\n",
+ " return __name__"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/nbpackage/nbs/__init__.py b/docs/source/examples/Notebook/nbpackage/nbs/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/source/examples/Notebook/nbpackage/nbs/__init__.py
diff --git a/docs/source/examples/Notebook/nbpackage/nbs/other.ipynb b/docs/source/examples/Notebook/nbpackage/nbs/other.ipynb
new file mode 100644
index 0000000..dad7e73
--- /dev/null
+++ b/docs/source/examples/Notebook/nbpackage/nbs/other.ipynb
@@ -0,0 +1,44 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This notebook just defines `bar`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "def bar(x):\n",
+ " return \"bar\" * x"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/source/examples/Notebook/rstversions/Connecting with the Qt Console.rst b/docs/source/examples/Notebook/rstversions/Connecting with the Qt Console.rst
new file mode 100644
index 0000000..906b254
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Connecting with the Qt Console.rst
@@ -0,0 +1,67 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Connecting%20with%20the%20Qt%20Console.ipynb>`__
+
+Connecting to an existing IPython kernel using the Qt Console
+=============================================================
+
+The Frontend/Kernel Model
+-------------------------
+
+The traditional IPython (``ipython``) consists of a single process that
+combines a terminal based UI with the process that runs the users code.
+
+While this traditional application still exists, the modern Jupyter
+consists of two processes:
+
+- Kernel: this is the process that runs the users code.
+- Frontend: this is the process that provides the user interface where
+ the user types code and sees results.
+
+Jupyter currently has 3 frontends:
+
+- Terminal Console (``ipython console``)
+- Qt Console (``ipython qtconsole``)
+- Notebook (``ipython notebook``)
+
+The Kernel and Frontend communicate over a ZeroMQ/JSON based messaging
+protocol, which allows multiple Frontends (even of different types) to
+communicate with a single Kernel. This opens the door for all sorts of
+interesting things, such as connecting a Console or Qt Console to a
+Notebook's Kernel. For example, you may want to connect a Qt console to
+your Notebook's Kernel and use it as a help browser, calling ``??`` on
+objects in the Qt console (whose pager is more flexible than the one in
+the notebook).
+
+This Notebook describes how you would connect another Frontend to a
+Kernel that is associated with a Notebook.
+
+Manual connection
+-----------------
+
+To connect another Frontend to a Kernel manually, you first need to find
+out the connection information for the Kernel using the
+``%connect_info`` magic:
+
+.. code:: python
+
+ %connect_info
+
+You can see that this magic displays everything you need to connect to
+this Notebook's Kernel.
+
+Automatic connection using a new Qt Console
+-------------------------------------------
+
+You can also start a new Qt Console connected to your current Kernel by
+using the ``%qtconsole`` magic. This will detect the necessary
+connection information and start the Qt Console for you automatically.
+
+.. code:: python
+
+ a = 10
+
+.. code:: python
+
+ %qtconsole
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Connecting%20with%20the%20Qt%20Console.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/Custom Keyboard Shortcuts.rst b/docs/source/examples/Notebook/rstversions/Custom Keyboard Shortcuts.rst
new file mode 100644
index 0000000..6cf95e2
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Custom Keyboard Shortcuts.rst
@@ -0,0 +1,71 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Custom%20Keyboard%20Shortcuts.ipynb>`__
+
+Keyboard Shortcut Customization
+===============================
+
+Starting with IPython 2.0 keyboard shortcuts in command and edit mode
+are fully customizable. These customizations are made using the Jupyter
+JavaScript API. Here is an example that makes the ``r`` key available
+for running a cell:
+
+.. code:: python
+
+ %%javascript
+
+ Jupyter.keyboard_manager.command_shortcuts.add_shortcut('r', {
+ help : 'run cell',
+ help_index : 'zz',
+ handler : function (event) {
+ IPython.notebook.execute_cell();
+ return false;
+ }}
+ );
+
+"By default the keypress ``r``, while in command mode, changes the type
+of the selected cell to ``raw``. This shortcut is overridden by the code
+in the previous cell, and thus the action no longer be available via the
+keypress ``r``."
+
+There are a couple of points to mention about this API:
+
+- The ``help_index`` field is used to sort the shortcuts in the
+ Keyboard Shortcuts help dialog. It defaults to ``zz``.
+- When a handler returns ``false`` it indicates that the event should
+ stop propagating and the default action should not be performed. For
+ further details about the ``event`` object or event handling, see the
+ jQuery docs.
+- If you don't need a ``help`` or ``help_index`` field, you can simply
+ pass a function as the second argument to ``add_shortcut``.
+
+.. code:: python
+
+ %%javascript
+
+ Jupyter.keyboard_manager.command_shortcuts.add_shortcut('r', function (event) {
+ IPython.notebook.execute_cell();
+ return false;
+ });
+
+Likewise, to remove a shortcut, use ``remove_shortcut``:
+
+.. code:: python
+
+ %%javascript
+
+ Jupyter.keyboard_manager.command_shortcuts.remove_shortcut('r');
+
+If you want your keyboard shortcuts to be active for all of your
+notebooks, put the above API calls into your ``custom.js`` file.
+
+Of course we provide name for majority of existing action so that you do
+not have to re-write everything, here is for example how to bind ``r``
+back to it's initial behavior:
+
+.. code:: python
+
+ %%javascript
+
+ Jupyter.keyboard_manager.command_shortcuts.add_shortcut('r', 'jupyter-notebook:change-cell-to-raw');
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Custom%20Keyboard%20Shortcuts.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages.rst b/docs/source/examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages.rst
new file mode 100644
index 0000000..98c977d
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages.rst
@@ -0,0 +1,245 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.ipynb>`__
+
+Distributing Jupyter Extensions as Python Packages
+==================================================
+
+Overview
+--------
+
+How can the notebook be extended?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The Jupyter Notebook client and server application are both deeply
+customizable. Their behavior can be extended by creating, respectively:
+
+- nbextension: a notebook extension
+
+ - a single JS file, or directory of JavaScript, Cascading
+ StyleSheets, etc. that contain at minimum a JavaScript module
+ packaged as an `AMD
+ modules <https://en.wikipedia.org/wiki/Asynchronous_module_definition>`__
+ that exports a function ``load_ipython_extension``
+
+- server extension: an importable Python module
+
+ - that implements ``load_jupyter_server_extension``
+
+Why create a Python package for Jupyter extensions?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Since it is rare to have a server extension that does not have any
+frontend components (an nbextension), for convenience and consistency,
+all these client and server extensions with their assets can be packaged
+and versioned together as a Python package with a few simple commands.
+This makes installing the package of extensions easier and less
+error-prone for the user.
+
+Installation of Jupyter Extensions
+----------------------------------
+
+Install a Python package containing Jupyter Extensions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are several ways that you may get a Python package containing
+Jupyter Extensions. Commonly, you will use a package manager for your
+system:
+
+.. code:: shell
+
+ pip install helpful_package
+ # or
+ conda install helpful_package
+ # or
+ apt-get install helpful_package
+
+ # where 'helpful_package' is a Python package containing one or more Jupyter Extensions
+
+Enable a Server Extension
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The simplest case would be to enable a server extension which has no
+frontend components.
+
+A ``pip`` user that wants their configuration stored in their home
+directory would type the following command:
+
+.. code:: shell
+
+ jupyter serverextension enable --py helpful_package
+
+Alternatively, a ``virtualenv`` or ``conda`` user can pass
+``--sys-prefix`` which keeps their environment isolated and
+reproducible. For example:
+
+.. code:: shell
+
+ # Make sure that your virtualenv or conda environment is activated
+ [source] activate my-environment
+
+ jupyter serverextension enable --py helpful_package --sys-prefix
+
+Install the nbextension assets
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If a package also has an nbextension with frontend assets that must be
+available (but not neccessarily enabled by default), install these
+assets with the following command:
+
+.. code:: shell
+
+ jupyter nbextension install --py helpful_package # or --sys-prefix if using virtualenv or conda
+
+Enable nbextension assets
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If a package has assets that should be loaded every time a Jupyter app
+(e.g. lab, notebook, dashboard, terminal) is loaded in the browser, the
+following command can be used to enable the nbextension:
+
+.. code:: shell
+
+ jupyter nbextension enable --py helpful_package # or --sys-prefix if using virtualenv or conda
+
+Did it work? Check by listing Jupyter Extensions.
+-------------------------------------------------
+
+After running one or more extension installation steps, you can list
+what is presently known about nbextensions or server extension. The
+following commands will list which extensions are available, whether
+they are enabled, and other extension details:
+
+.. code:: shell
+
+ jupyter nbextension list
+ jupyter serverextension list
+
+Additional resources on creating and distributing packages
+----------------------------------------------------------
+
+ Of course, in addition to the files listed, there are number of
+ other files one needs to build a proper package. Here are some good
+ resources: - `The Hitchhiker's Guide to
+ Packaging <http://the-hitchhikers-guide-to-packaging.readthedocs.org/en/latest/quickstart.html>`__
+ - `Repository Structure and
+ Python <http://www.kennethreitz.org/essays/repository-structure-and-python>`__
+ by Kenneth Reitz
+
+ How you distribute them, too, is important: - `Packaging and
+ Distributing
+ Projects <http://python-packaging-user-guide.readthedocs.org/en/latest/distributing/>`__
+ - `conda: Building
+ packages <http://conda.pydata.org/docs/building/build.html>`__
+
+ Here are some tools to get you started: -
+ `generator-nbextension <https://github.com/Anaconda-Server/generator-nbextension>`__
+
+Example - Server extension
+--------------------------
+
+Creating a Python package with a server extension
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here is an example of a python module which contains a server extension
+directly on itself. It has this directory structure:
+
+::
+
+ - setup.py
+ - MANIFEST.in
+ - my_module/
+ - __init__.py
+
+Defining the server extension
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example shows that the server extension and its
+``load_jupyter_server_extension`` function are defined in the
+``__init__.py`` file.
+
+``my_module/__init__.py``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code:: python
+
+ def _jupyter_server_extension_paths():
+ return [{
+ "module": "my_module"
+ }]
+
+
+ def load_jupyter_server_extension(nbapp):
+ nbapp.log.info("my module enabled!")
+
+Install and enable the server extension
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Which a user can install with:
+
+::
+
+ jupyter serverextension enable --py my_module [--sys-prefix]
+
+Example - Server extension and nbextension
+------------------------------------------
+
+Creating a Python package with a server extension and nbextension
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here is another server extension, with a front-end module. It assumes
+this directory structure:
+
+::
+
+ - setup.py
+ - MANIFEST.in
+ - my_fancy_module/
+ - __init__.py
+ - static/
+ index.js
+
+Defining the server extension and nbextension
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This example again shows that the server extension and its
+``load_jupyter_server_extension`` function are defined in the
+``__init__.py`` file. This time, there is also a function
+``_jupyter_nbextension_path`` for the nbextension.
+
+``my_fancy_module/__init__.py``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code:: python
+
+ def _jupyter_server_extension_paths():
+ return [{
+ "module": "my_fancy_module"
+ }]
+
+ # Jupyter Extension points
+ def _jupyter_nbextension_paths():
+ return [dict(
+ section="notebook",
+ # the path is relative to the `my_fancy_module` directory
+ src="static",
+ # directory in the `nbextension/` namespace
+ dest="my_fancy_module",
+ # _also_ in the `nbextension/` namespace
+ require="my_fancy_module/index")]
+
+ def load_jupyter_server_extension(nbapp):
+ nbapp.log.info("my module enabled!")
+
+Install and enable the server extension and nbextension
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The user can install and enable the extensions with the following set of
+commands:
+
+::
+
+ jupyter nbextension install --py my_fancy_module [--sys-prefix|--user]
+ jupyter nbextension enable --py my_fancy_module [--sys-prefix|--system]
+ jupyter serverextension enable --py my_fancy_module [--sys-prefix|--system]
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/Examples and Tutorials Index.rst b/docs/source/examples/Notebook/rstversions/Examples and Tutorials Index.rst
new file mode 100644
index 0000000..5c1dd4c
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Examples and Tutorials Index.rst
@@ -0,0 +1,34 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Examples%20and%20Tutorials%20Index.ipynb>`__
+
+
+
+Examples and Tutorials
+======================
+
+This portion of the documentation was generated from notebook files. You
+can download the original interactive notebook files using the links at
+the tops and bottoms of the pages.
+
+Tutorials
+---------
+
+- `What is the Jupyter
+ Notebook <What%20is%20the%20Jupyter%20Notebook.html>`__
+- `Notebook Basics <Notebook%20Basics.html>`__
+- `Running Code <Running%20Code.html>`__
+- `Working With Markdown
+ Cells <Working%20With%20Markdown%20Cells.html>`__
+- `Custom Keyboard Shortcuts <Custom%20Keyboard%20Shortcuts.html>`__
+- `JavaScript Notebook
+ Extensions <JavaScript%20Notebook%20Extensions.html>`__
+
+Examples
+--------
+
+- `Importing Notebooks <Importing%20Notebooks.html>`__
+- `Connecting with the Qt
+ Console <Connecting%20with%20the%20Qt%20Console.html>`__
+- `Typesetting Equations <Typesetting%20Equations.html>`__
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Examples%20and%20Tutorials%20Index.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/Importing Notebooks.rst b/docs/source/examples/Notebook/rstversions/Importing Notebooks.rst
new file mode 100644
index 0000000..390ae99
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Importing Notebooks.rst
@@ -0,0 +1,286 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Importing%20Notebooks.ipynb>`__
+
+Importing Jupyter Notebooks as Modules
+======================================
+
+It is a common problem that people want to import code from Jupyter
+Notebooks. This is made difficult by the fact that Notebooks are not
+plain Python files, and thus cannot be imported by the regular Python
+machinery.
+
+Fortunately, Python provides some fairly sophisticated
+`hooks <http://www.python.org/dev/peps/pep-0302/>`__ into the import
+machinery, so we can actually make Jupyter notebooks importable without
+much difficulty, and only using public APIs.
+
+.. code:: python
+
+ import io, os, sys, types
+
+.. code:: python
+
+ from IPython import get_ipython
+ from IPython.nbformat import current
+ from IPython.core.interactiveshell import InteractiveShell
+
+Import hooks typically take the form of two objects:
+
+1. a Module **Loader**, which takes a module name (e.g.
+ ``'IPython.display'``), and returns a Module
+2. a Module **Finder**, which figures out whether a module might exist,
+ and tells Python what **Loader** to use
+
+.. code:: python
+
+ def find_notebook(fullname, path=None):
+ """find a notebook, given its fully qualified name and an optional path
+
+ This turns "foo.bar" into "foo/bar.ipynb"
+ and tries turning "Foo_Bar" into "Foo Bar" if Foo_Bar
+ does not exist.
+ """
+ name = fullname.rsplit('.', 1)[-1]
+ if not path:
+ path = ['']
+ for d in path:
+ nb_path = os.path.join(d, name + ".ipynb")
+ if os.path.isfile(nb_path):
+ return nb_path
+ # let import Notebook_Name find "Notebook Name.ipynb"
+ nb_path = nb_path.replace("_", " ")
+ if os.path.isfile(nb_path):
+ return nb_path
+
+
+Notebook Loader
+---------------
+
+Here we have our Notebook Loader. It's actually quite simple - once we
+figure out the filename of the module, all it does is:
+
+1. load the notebook document into memory
+2. create an empty Module
+3. execute every cell in the Module namespace
+
+Since IPython cells can have extended syntax, the IPython transform is
+applied to turn each of these cells into their pure-Python counterparts
+before executing them. If all of your notebook cells are pure-Python,
+this step is unnecessary.
+
+.. code:: python
+
+ class NotebookLoader(object):
+ """Module Loader for Jupyter Notebooks"""
+ def __init__(self, path=None):
+ self.shell = InteractiveShell.instance()
+ self.path = path
+
+ def load_module(self, fullname):
+ """import a notebook as a module"""
+ path = find_notebook(fullname, self.path)
+
+ print ("importing Jupyter notebook from %s" % path)
+
+ # load the notebook object
+ with io.open(path, 'r', encoding='utf-8') as f:
+ nb = current.read(f, 'json')
+
+
+ # create the module and add it to sys.modules
+ # if name in sys.modules:
+ # return sys.modules[name]
+ mod = types.ModuleType(fullname)
+ mod.__file__ = path
+ mod.__loader__ = self
+ mod.__dict__['get_ipython'] = get_ipython
+ sys.modules[fullname] = mod
+
+ # extra work to ensure that magics that would affect the user_ns
+ # actually affect the notebook module's ns
+ save_user_ns = self.shell.user_ns
+ self.shell.user_ns = mod.__dict__
+
+ try:
+ for cell in nb.worksheets[0].cells:
+ if cell.cell_type == 'code' and cell.language == 'python':
+ # transform the input to executable Python
+ code = self.shell.input_transformer_manager.transform_cell(cell.input)
+ # run the code in themodule
+ exec(code, mod.__dict__)
+ finally:
+ self.shell.user_ns = save_user_ns
+ return mod
+
+
+The Module Finder
+-----------------
+
+The finder is a simple object that tells you whether a name can be
+imported, and returns the appropriate loader. All this one does is
+check, when you do:
+
+.. code:: python
+
+ import mynotebook
+
+it checks whether ``mynotebook.ipynb`` exists. If a notebook is found,
+then it returns a NotebookLoader.
+
+Any extra logic is just for resolving paths within packages.
+
+.. code:: python
+
+ class NotebookFinder(object):
+ """Module finder that locates Jupyter Notebooks"""
+ def __init__(self):
+ self.loaders = {}
+
+ def find_module(self, fullname, path=None):
+ nb_path = find_notebook(fullname, path)
+ if not nb_path:
+ return
+
+ key = path
+ if path:
+ # lists aren't hashable
+ key = os.path.sep.join(path)
+
+ if key not in self.loaders:
+ self.loaders[key] = NotebookLoader(path)
+ return self.loaders[key]
+
+
+Register the hook
+-----------------
+
+Now we register the ``NotebookFinder`` with ``sys.meta_path``
+
+.. code:: python
+
+ sys.meta_path.append(NotebookFinder())
+
+After this point, my notebooks should be importable.
+
+Let's look at what we have in the CWD:
+
+.. code:: python
+
+ ls nbpackage
+
+So I should be able to ``import nbimp.mynotebook``.
+
+Aside: displaying notebooks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here is some simple code to display the contents of a notebook with
+syntax highlighting, etc.
+
+.. code:: python
+
+ from pygments import highlight
+ from pygments.lexers import PythonLexer
+ from pygments.formatters import HtmlFormatter
+
+ from IPython.display import display, HTML
+
+ formatter = HtmlFormatter()
+ lexer = PythonLexer()
+
+ # publish the CSS for pygments highlighting
+ display(HTML("""
+ <style type='text/css'>
+ %s
+ </style>
+ """ % formatter.get_style_defs()
+ ))
+
+.. code:: python
+
+ def show_notebook(fname):
+ """display a short summary of the cells of a notebook"""
+ with io.open(fname, 'r', encoding='utf-8') as f:
+ nb = current.read(f, 'json')
+ html = []
+ for cell in nb.worksheets[0].cells:
+ html.append("<h4>%s cell</h4>" % cell.cell_type)
+ if cell.cell_type == 'code':
+ html.append(highlight(cell.input, lexer, formatter))
+ else:
+ html.append("<pre>%s</pre>" % cell.source)
+ display(HTML('\n'.join(html)))
+
+ show_notebook(os.path.join("nbpackage", "mynotebook.ipynb"))
+
+So my notebook has a heading cell and some code cells, one of which
+contains some IPython syntax.
+
+Let's see what happens when we import it
+
+.. code:: python
+
+ from nbpackage import mynotebook
+
+Hooray, it imported! Does it work?
+
+.. code:: python
+
+ mynotebook.foo()
+
+Hooray again!
+
+Even the function that contains IPython syntax works:
+
+.. code:: python
+
+ mynotebook.has_ip_syntax()
+
+Notebooks in packages
+---------------------
+
+We also have a notebook inside the ``nb`` package, so let's make sure
+that works as well.
+
+.. code:: python
+
+ ls nbpackage/nbs
+
+Note that the ``__init__.py`` is necessary for ``nb`` to be considered a
+package, just like usual.
+
+.. code:: python
+
+ show_notebook(os.path.join("nbpackage", "nbs", "other.ipynb"))
+
+.. code:: python
+
+ from nbpackage.nbs import other
+ other.bar(5)
+
+So now we have importable notebooks, from both the local directory and
+inside packages.
+
+I can even put a notebook inside IPython, to further demonstrate that
+this is working properly:
+
+.. code:: python
+
+ import shutil
+ from IPython.utils.path import get_ipython_package_dir
+
+ utils = os.path.join(get_ipython_package_dir(), 'utils')
+ shutil.copy(os.path.join("nbpackage", "mynotebook.ipynb"),
+ os.path.join(utils, "inside_ipython.ipynb")
+ )
+
+and import the notebook from ``IPython.utils``
+
+.. code:: python
+
+ from IPython.utils import inside_ipython
+ inside_ipython.whatsmyname()
+
+This approach can even import functions and classes that are defined in
+a notebook using the ``%%cython`` magic.
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Importing%20Notebooks.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/JavaScript Notebook Extensions.rst b/docs/source/examples/Notebook/rstversions/JavaScript Notebook Extensions.rst
new file mode 100644
index 0000000..826eaf5
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/JavaScript Notebook Extensions.rst
@@ -0,0 +1,389 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/JavaScript%20Notebook%20Extensions.ipynb>`__
+
+Embracing web standards
+=======================
+
+One of the main reasons why we developed the current notebook web
+application was to embrace the web technology.
+
+By being a pure web application using HTML, Javascript, and CSS, the
+Notebook can get all the web technology improvement for free. Thus, as
+browser support for different media extend, the notebook web app should
+be able to be compatible without modification.
+
+This is also true with performance of the User Interface as the speed of
+Javascript VM increases.
+
+The other advantage of using only web technology is that the code of the
+interface is fully accessible to the end user and is modifiable live.
+Even if this task is not always easy, we strive to keep our code as
+accessible and reusable as possible. This should allow us - with minimum
+effort - development of small extensions that customize the behavior of
+the web interface.
+
+Tampering with the Notebook application
+---------------------------------------
+
+The first tool that is available to you and that you should be aware of
+are browser "developers tool". The exact naming can change across
+browser and might require the installation of extensions. But basically
+they can allow you to inspect/modify the DOM, and interact with the
+javascript code that runs the frontend.
+
+- In Chrome and Safari, Developer tools are in the menu
+ ``View > Developer > Javascript Console``
+- In Firefox you might need to install
+ `Firebug <http://getfirebug.com/>`__
+
+Those will be your best friends to debug and try different approaches
+for your extensions.
+
+Injecting JS
+~~~~~~~~~~~~
+
+Using magics
+^^^^^^^^^^^^
+
+The above tools can be tedious for editing edit long JavaScript files.
+Therefore we provide the ``%%javascript`` magic. This allows you to
+quickly inject JavaScript into the notebook. Still the javascript
+injected this way will not survive reloading. Hence, it is a good tool
+for testing an refining a script.
+
+You might see here and there people modifying css and injecting js into
+the notebook by reading file(s) and publishing them into the notebook.
+Not only does this often break the flow of the notebook and make the
+re-execution of the notebook broken, but it also means that you need to
+execute those cells in the entire notebook every time you need to update
+the code.
+
+This can still be useful in some cases, like the ``%autosave`` magic
+that allows you to control the time between each save. But this can be
+replaced by a JavaScript dropdown menu to select the save interval.
+
+.. code:: python
+
+ ## you can inspect the autosave code to see what it does.
+ %autosave??
+
+custom.js
+^^^^^^^^^
+
+To inject Javascript we provide an entry point: ``custom.js`` that
+allows the user to execute and load other resources into the notebook.
+Javascript code in ``custom.js`` will be executed when the notebook app
+starts and can then be used to customize almost anything in the UI and
+in the behavior of the notebook.
+
+``custom.js`` can be found in the Jupyter dir. You can share your
+custom.js with others.
+
+Back to theory
+''''''''''''''
+
+.. code:: python
+
+ from jupyter_core.paths import jupyter_config_dir
+ jupyter_dir = jupyter_config_dir()
+ jupyter_dir
+
+and custom js is in
+
+.. code:: python
+
+ import os.path
+ custom_js_path = os.path.join(jupyter_dir, 'custom', 'custom.js')
+
+.. code:: python
+
+ # my custom js
+ if os.path.isfile(custom_js_path):
+ with open(custom_js_path) as f:
+ print(f.read())
+ else:
+ print("You don't have a custom.js file")
+
+Note that ``custom.js`` is meant to be modified by user. When writing a
+script, you can define it in a separate file and add a line of
+configuration into ``custom.js`` that will fetch and execute the file.
+
+**Warning** : even if modification of ``custom.js`` takes effect
+immediately after browser refresh (except if browser cache is
+aggressive), *creating* a file in ``static/`` directory needs a **server
+restart**.
+
+Exercise :
+----------
+
+- Create a ``custom.js`` in the right location with the following
+ content:
+
+ .. code:: javascript
+
+ alert("hello world from custom.js")
+
+- Restart your server and open any notebook.
+- Be greeted by custom.js
+
+Have a look at `default
+custom.js <https://github.com/jupyter/notebook/blob/4.0.x/notebook/static/custom/custom.js>`__,
+to see it's content and for more explanation.
+
+For the quick ones :
+~~~~~~~~~~~~~~~~~~~~
+
+We've seen above that you can change the autosave rate by using a magic.
+This is typically something I don't want to type every time, and that I
+don't like to embed into my workflow and documents. (readers don't care
+what my autosave time is). Let's build an extension that allows us to do
+it.
+
+Create a dropdown element in the toolbar (DOM
+``Jupyter.toolbar.element``), you will need
+
+- ``Jupyter.notebook.set_autosave_interval(miliseconds)``
+- know that 1 min = 60 sec, and 1 sec = 1000 ms
+
+.. code:: javascript
+
+
+ var label = jQuery('<label/>').text('AutoScroll Limit:');
+ var select = jQuery('<select/>')
+ //.append(jQuery('<option/>').attr('value', '2').text('2min (default)'))
+ .append(jQuery('<option/>').attr('value', undefined).text('disabled'))
+
+ // TODO:
+ //the_toolbar_element.append(label)
+ //the_toolbar_element.append(select);
+
+ select.change(function() {
+ var val = jQuery(this).val() // val will be the value in [2]
+ // TODO
+ // this will be called when dropdown changes
+
+ });
+
+ var time_m = [1,5,10,15,30];
+ for (var i=0; i < time_m.length; i++) {
+ var ts = time_m[i];
+ //[2] ____ this will be `val` on [1]
+ // |
+ // v
+ select.append($('<option/>').attr('value', ts).text(thr+'min'));
+ // this will fill up the dropdown `select` with
+ // 1 min
+ // 5 min
+ // 10 min
+ // 10 min
+ // ...
+ }
+
+A non-interactive example first
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+I like my cython to be nicely highlighted
+
+.. code:: javascript
+
+ Jupyter.config.cell_magic_highlight['magic_text/x-cython'] = {}
+ Jupyter.config.cell_magic_highlight['magic_text/x-cython'].reg = [/^%%cython/]
+
+``text/x-cython`` is the name of CodeMirror mode name, ``magic_`` prefix
+will just patch the mode so that the first line that contains a magic
+does not screw up the highlighting. ``reg``\ is a list or regular
+expression that will trigger the change of mode.
+
+Get more documentation
+^^^^^^^^^^^^^^^^^^^^^^
+
+Sadly, you will have to read the js source file (but there are lots of
+comments) and/or build the JavaScript documentation using yuidoc. If you
+have ``node`` and ``yui-doc`` installed:
+
+.. code:: bash
+
+ $ cd ~/jupyter/notebook/notebook/static/notebook/js/
+ $ yuidoc . --server
+ warn: (yuidoc): Failed to extract port, setting to the default :3000
+ info: (yuidoc): Starting YUIDoc@0.3.45 using YUI@3.9.1 with NodeJS@0.10.15
+ info: (yuidoc): Scanning for yuidoc.json file.
+ info: (yuidoc): Starting YUIDoc with the following options:
+ info: (yuidoc):
+ { port: 3000,
+ nocode: false,
+ paths: [ '.' ],
+ server: true,
+ outdir: './out' }
+ info: (yuidoc): Scanning for yuidoc.json file.
+ info: (server): Starting server: http://127.0.0.1:3000
+
+and browse http://127.0.0.1:3000 to get documentation
+
+Some convenience methods
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+By browsing the documentation you will see that we have some convenience
+methods that allows us to avoid re-inventing the UI every time :
+
+.. code:: javascript
+
+ Jupyter.toolbar.add_buttons_group([
+ {
+ 'label' : 'run qtconsole',
+ 'icon' : 'icon-terminal', // select your icon from
+ // http://fortawesome.github.io/Font-Awesome/icons/
+ 'callback': function(){Jupyter.notebook.kernel.execute('%qtconsole')}
+ }
+ // add more button here if needed.
+ ]);
+
+with a `lot of
+icons <http://fortawesome.github.io/Font-Awesome/icons/>`__ you can
+select from.
+
+Cell Metadata
+-------------
+
+The most requested feature is generally to be able to distinguish an
+individual cell in the notebook, or run a specific action with them. To
+do so, you can either use ``Jupyter.notebook.get_selected_cell()``, or
+rely on ``CellToolbar``. This allows you to register a set of actions
+and graphical elements that will be attached to individual cells.
+
+Cell Toolbar
+~~~~~~~~~~~~
+
+You can see some example of what can be done by toggling the
+``Cell Toolbar`` selector in the toolbar on top of the notebook. It
+provides two default ``presets`` that are ``Default`` and ``slideshow``.
+Default allows the user to edit the metadata attached to each cell
+manually.
+
+First we define a function that takes at first parameter an element on
+the DOM in which to inject UI element. The second element is the cell
+this element wis registered with. Then we will need to register that
+function and give it a name.
+
+Register a callback
+^^^^^^^^^^^^^^^^^^^
+
+.. code:: python
+
+ %%javascript
+ var CellToolbar = Jupyter.CellToolbar
+ var toggle = function(div, cell) {
+ var button_container = $(div)
+
+ // let's create a button that shows the current value of the metadata
+ var button = $('<button/>').addClass('btn btn-mini').text(String(cell.metadata.foo));
+
+ // On click, change the metadata value and update the button label
+ button.click(function(){
+ var v = cell.metadata.foo;
+ cell.metadata.foo = !v;
+ button.text(String(!v));
+ })
+
+ // add the button to the DOM div.
+ button_container.append(button);
+ }
+
+ // now we register the callback under the name foo to give the
+ // user the ability to use it later
+ CellToolbar.register_callback('tuto.foo', toggle);
+
+Registering a preset
+^^^^^^^^^^^^^^^^^^^^
+
+This function can now be part of many ``preset`` of the CellToolBar.
+
+.. code:: python
+
+ %%javascript
+ Jupyter.CellToolbar.register_preset('Tutorial 1',['tuto.foo','default.rawedit'])
+ Jupyter.CellToolbar.register_preset('Tutorial 2',['slideshow.select','tuto.foo'])
+
+You should now have access to two presets :
+
+- Tutorial 1
+- Tutorial 2
+
+And check that the buttons you defined share state when you toggle
+preset. Also check that the metadata of the cell is modified when you
+click the button, and that when saved on reloaded the metadata is still
+available.
+
+Exercise:
+^^^^^^^^^
+
+Try to wrap the all code in a file, put this file in
+``{jupyter_dir}/custom/<a-name>.js``, and add
+
+::
+
+ require(['custom/<a-name>']);
+
+in ``custom.js`` to have this script automatically loaded in all your
+notebooks.
+
+``require`` is provided by a `javascript
+library <http://requirejs.org/>`__ that allow you to express dependency.
+For simple extension like the previous one we directly mute the global
+namespace, but for more complex extension you could pass a callback to
+``require([...], <callback>)`` call, to allow the user to pass
+configuration information to your plugin.
+
+In Python lang,
+
+.. code:: javascript
+
+ require(['a/b', 'c/d'], function( e, f){
+ e.something()
+ f.something()
+ })
+
+could be read as
+
+.. code:: python
+
+ import a.b as e
+ import c.d as f
+ e.something()
+ f.something()
+
+See for example @damianavila `"ZenMode"
+plugin <https://github.com/ipython-contrib/IPython-notebook-extensions/blob/master/custom.example.js#L34>`__
+:
+
+.. code:: javascript
+
+
+ // read that as
+ // import custom.zenmode.main as zenmode
+ require(['custom/zenmode/main'],function(zenmode){
+ zenmode.background('images/back12.jpg');
+ })
+
+For the quickest
+^^^^^^^^^^^^^^^^
+
+Try to use `the
+following <https://github.com/ipython/ipython/blob/1.x/IPython/html/static/notebook/js/celltoolbar.js#L367>`__
+to bind a dropdown list to ``cell.metadata.difficulty.select``.
+
+It should be able to take the 4 following values :
+
+- ``<None>``
+- ``Easy``
+- ``Medium``
+- ``Hard``
+
+We will use it to customiZe the output of the converted notebook
+depending on the tag on each cell
+
+.. code:: python
+
+ %load soln/celldiff.js
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/JavaScript%20Notebook%20Extensions.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/Notebook Basics.rst b/docs/source/examples/Notebook/rstversions/Notebook Basics.rst
new file mode 100644
index 0000000..4f245db
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Notebook Basics.rst
@@ -0,0 +1,212 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Notebook%20Basics.ipynb>`__
+
+Notebook Basics
+===============
+
+The Notebook dashboard
+----------------------
+
+When you first start the notebook server, your browser will open to the
+notebook dashboard. The dashboard serves as a home page for the
+notebook. Its main purpose is to display the notebooks and files in the
+current directory. For example, here is a screenshot of the dashboard
+page for the ``examples`` directory in the Jupyter repository:
+
+The top of the notebook list displays clickable breadcrumbs of the
+current directory. By clicking on these breadcrumbs or on
+sub-directories in the notebook list, you can navigate your file system.
+
+To create a new notebook, click on the "New" button at the top of the
+list and select a kernel from the dropdown (as seen below). Which
+kernels are listed depend on what's installed on the server. Some of the
+kernels in the screenshot below may not exist as an option to you.
+
+Notebooks and files can be uploaded to the current directory by dragging
+a notebook file onto the notebook list or by the "click here" text above
+the list.
+
+The notebook list shows green "Running" text and a green notebook icon
+next to running notebooks (as seen below). Notebooks remain running
+until you explicitly shut them down; closing the notebook's page is not
+sufficient.
+
+To shutdown, delete, duplicate, or rename a notebook check the checkbox
+next to it and an array of controls will appear at the top of the
+notebook list (as seen below). You can also use the same operations on
+directories and files when applicable.
+
+To see all of your running notebooks along with their directories, click
+on the "Running" tab:
+
+This view provides a convenient way to track notebooks that you start as
+you navigate the file system in a long running notebook server.
+
+Overview of the Notebook UI
+---------------------------
+
+If you create a new notebook or open an existing one, you will be taken
+to the notebook user interface (UI). This UI allows you to run code and
+author notebook documents interactively. The notebook UI has the
+following main areas:
+
+- Menu
+- Toolbar
+- Notebook area and cells
+
+The notebook has an interactive tour of these elements that can be
+started in the "Help:User Interface Tour" menu item.
+
+Modal editor
+------------
+
+Starting with IPython 2.0, the Jupyter Notebook has a modal user
+interface. This means that the keyboard does different things depending
+on which mode the Notebook is in. There are two modes: edit mode and
+command mode.
+
+Edit mode
+~~~~~~~~~
+
+Edit mode is indicated by a green cell border and a prompt showing in
+the editor area:
+
+When a cell is in edit mode, you can type into the cell, like a normal
+text editor.
+
+.. raw:: html
+
+ <div class="alert alert-success">
+
+Enter edit mode by pressing ``Enter`` or using the mouse to click on a
+cell's editor area.
+
+.. raw:: html
+
+ </div>
+
+Command mode
+~~~~~~~~~~~~
+
+Command mode is indicated by a grey cell border with a blue left margin:
+
+When you are in command mode, you are able to edit the notebook as a
+whole, but not type into individual cells. Most importantly, in command
+mode, the keyboard is mapped to a set of shortcuts that let you perform
+notebook and cell actions efficiently. For example, if you are in
+command mode and you press ``c``, you will copy the current cell - no
+modifier is needed.
+
+.. raw:: html
+
+ <div class="alert alert-error">
+
+Don't try to type into a cell in command mode; unexpected things will
+happen!
+
+.. raw:: html
+
+ </div>
+
+.. raw:: html
+
+ <div class="alert alert-success">
+
+Enter command mode by pressing ``Esc`` or using the mouse to click
+*outside* a cell's editor area.
+
+.. raw:: html
+
+ </div>
+
+Mouse navigation
+----------------
+
+All navigation and actions in the Notebook are available using the mouse
+through the menubar and toolbar, which are both above the main Notebook
+area:
+
+The first idea of mouse based navigation is that **cells can be selected
+by clicking on them.** The currently selected cell gets a grey or green
+border depending on whether the notebook is in edit or command mode. If
+you click inside a cell's editor area, you will enter edit mode. If you
+click on the prompt or output area of a cell you will enter command
+mode.
+
+If you are running this notebook in a live session (not on
+http://nbviewer.jupyter.org) try selecting different cells and going
+between edit and command mode. Try typing into a cell.
+
+The second idea of mouse based navigation is that **cell actions usually
+apply to the currently selected cell**. Thus if you want to run the code
+in a cell, you would select it and click the
+
+.. raw:: html
+
+ <button class="btn btn-default btn-xs">
+
+.. raw:: html
+
+ </button>
+
+button in the toolbar or the "Cell:Run" menu item. Similarly, to copy a
+cell you would select it and click the
+
+.. raw:: html
+
+ <button class="btn btn-default btn-xs">
+
+.. raw:: html
+
+ </button>
+
+button in the toolbar or the "Edit:Copy" menu item. With this simple
+pattern, you should be able to do most everything you need with the
+mouse.
+
+Markdown and heading cells have one other state that can be modified
+with the mouse. These cells can either be rendered or unrendered. When
+they are rendered, you will see a nice formatted representation of the
+cell's contents. When they are unrendered, you will see the raw text
+source of the cell. To render the selected cell with the mouse, click
+the
+
+.. raw:: html
+
+ <button class="btn btn-default btn-xs">
+
+.. raw:: html
+
+ </button>
+
+button in the toolbar or the "Cell:Run" menu item. To unrender the
+selected cell, double click on the cell.
+
+Keyboard Navigation
+-------------------
+
+The modal user interface of the Jupyter Notebook has been optimized for
+efficient keyboard usage. This is made possible by having two different
+sets of keyboard shortcuts: one set that is active in edit mode and
+another in command mode.
+
+The most important keyboard shortcuts are ``Enter``, which enters edit
+mode, and ``Esc``, which enters command mode.
+
+In edit mode, most of the keyboard is dedicated to typing into the
+cell's editor. Thus, in edit mode there are relatively few shortcuts. In
+command mode, the entire keyboard is available for shortcuts, so there
+are many more. The ``Help``->``Keyboard Shortcuts`` dialog lists the
+available shortcuts.
+
+We recommend learning the command mode shortcuts in the following rough
+order:
+
+1. Basic navigation: ``enter``, ``shift-enter``, ``up/k``, ``down/j``
+2. Saving the notebook: ``s``
+3. Change Cell types: ``y``, ``m``, ``1-6``, ``t``
+4. Cell creation: ``a``, ``b``
+5. Cell editing: ``x``, ``c``, ``v``, ``d``, ``z``
+6. Kernel operations: ``i``, ``0`` (press twice)
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Notebook%20Basics.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/Running Code.rst b/docs/source/examples/Notebook/rstversions/Running Code.rst
new file mode 100644
index 0000000..c76ecc6
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Running Code.rst
@@ -0,0 +1,154 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Running%20Code.ipynb>`__
+
+Running Code
+============
+
+First and foremost, the Jupyter Notebook is an interactive environment
+for writing and running code. The notebook is capable of running code in
+a wide range of languages. However, each notebook is associated with a
+single kernel. This notebook is associated with the IPython kernel,
+therefor runs Python code.
+
+Code cells allow you to enter and run code
+------------------------------------------
+
+Run a code cell using ``Shift-Enter`` or pressing the
+
+.. raw:: html
+
+ <button class="btn btn-default btn-xs">
+
+.. raw:: html
+
+ </button>
+
+button in the toolbar above:
+
+.. code:: python
+
+ a = 10
+
+.. code:: python
+
+ print(a)
+
+There are two other keyboard shortcuts for running code:
+
+- ``Alt-Enter`` runs the current cell and inserts a new one below.
+- ``Ctrl-Enter`` run the current cell and enters command mode.
+
+Managing the Kernel
+-------------------
+
+Code is run in a separate process called the Kernel. The Kernel can be
+interrupted or restarted. Try running the following cell and then hit
+the
+
+.. raw:: html
+
+ <button class="btn btn-default btn-xs">
+
+.. raw:: html
+
+ </button>
+
+button in the toolbar above.
+
+.. code:: python
+
+ import time
+ time.sleep(10)
+
+If the Kernel dies you will be prompted to restart it. Here we call the
+low-level system libc.time routine with the wrong argument via ctypes to
+segfault the Python interpreter:
+
+.. code:: python
+
+ import sys
+ from ctypes import CDLL
+ # This will crash a Linux or Mac system
+ # equivalent calls can be made on Windows
+ dll = 'dylib' if sys.platform == 'darwin' else 'so.6'
+ libc = CDLL("libc.%s" % dll)
+ libc.time(-1) # BOOM!!
+
+Cell menu
+---------
+
+The "Cell" menu has a number of menu items for running code in different
+ways. These includes:
+
+- Run and Select Below
+- Run and Insert Below
+- Run All
+- Run All Above
+- Run All Below
+
+Restarting the kernels
+----------------------
+
+The kernel maintains the state of a notebook's computations. You can
+reset this state by restarting the kernel. This is done by clicking on
+the
+
+.. raw:: html
+
+ <button class="btn btn-default btn-xs">
+
+.. raw:: html
+
+ </button>
+
+in the toolbar above.
+
+sys.stdout and sys.stderr
+-------------------------
+
+The stdout and stderr streams are displayed as text in the output area.
+
+.. code:: python
+
+ print("hi, stdout")
+
+.. code:: python
+
+ from __future__ import print_function
+ print('hi, stderr', file=sys.stderr)
+
+Output is asynchronous
+----------------------
+
+All output is displayed asynchronously as it is generated in the Kernel.
+If you execute the next cell, you will see the output one piece at a
+time, not all at the end.
+
+.. code:: python
+
+ import time, sys
+ for i in range(8):
+ print(i)
+ time.sleep(0.5)
+
+Large outputs
+-------------
+
+To better handle large outputs, the output area can be collapsed. Run
+the following cell and then single- or double- click on the active area
+to the left of the output:
+
+.. code:: python
+
+ for i in range(50):
+ print(i)
+
+Beyond a certain point, output will scroll automatically:
+
+.. code:: python
+
+ for i in range(500):
+ print(2**i - 1)
+
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Running%20Code.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/Typesetting Equations.rst b/docs/source/examples/Notebook/rstversions/Typesetting Equations.rst
new file mode 100644
index 0000000..adf889d
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Typesetting Equations.rst
@@ -0,0 +1,290 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Typesetting%20Equations.ipynb>`__
+
+The Markdown parser included in the Jupyter Notebook is MathJax-aware.
+This means that you can freely mix in mathematical expressions using the
+`MathJax subset of Tex and
+LaTeX <http://docs.mathjax.org/en/latest/tex.html#tex-support>`__. `Some
+examples from the MathJax
+site <http://www.mathjax.org/demos/tex-samples/>`__ are reproduced
+below, as well as the Markdown+TeX source.
+
+Motivating Examples
+===================
+
+The Lorenz Equations
+--------------------
+
+Source
+~~~~~~
+
+::
+
+ \begin{align}
+ \dot{x} & = \sigma(y-x) \\
+ \dot{y} & = \rho x - y - xz \\
+ \dot{z} & = -\beta z + xy
+ \end{align}
+
+Display
+~~~~~~~
+
+.. raw:: latex
+
+ \begin{align}
+ \dot{x} & = \sigma(y-x) \\
+ \dot{y} & = \rho x - y - xz \\
+ \dot{z} & = -\beta z + xy
+ \end{align}
+
+The Cauchy-Schwarz Inequality
+-----------------------------
+
+Source
+~~~~~~
+
+::
+
+ \begin{equation*}
+ \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+ \end{equation*}
+
+Display
+~~~~~~~
+
+.. raw:: latex
+
+ \begin{equation*}
+ \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+ \end{equation*}
+
+A Cross Product Formula
+-----------------------
+
+Source
+~~~~~~
+
+::
+
+ \begin{equation*}
+ \mathbf{V}_1 \times \mathbf{V}_2 = \begin{vmatrix}
+ \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+ \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+ \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0
+ \end{vmatrix}
+ \end{equation*}
+
+Display
+~~~~~~~
+
+.. raw:: latex
+
+ \begin{equation*}
+ \mathbf{V}_1 \times \mathbf{V}_2 = \begin{vmatrix}
+ \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+ \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+ \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0
+ \end{vmatrix}
+ \end{equation*}
+
+The probability of getting (k) heads when flipping (n) coins is
+---------------------------------------------------------------
+
+Source
+~~~~~~
+
+::
+
+ \begin{equation*}
+ P(E) = {n \choose k} p^k (1-p)^{ n-k}
+ \end{equation*}
+
+Display
+~~~~~~~
+
+.. raw:: latex
+
+ \begin{equation*}
+ P(E) = {n \choose k} p^k (1-p)^{ n-k}
+ \end{equation*}
+
+An Identity of Ramanujan
+------------------------
+
+Source
+~~~~~~
+
+::
+
+ \begin{equation*}
+ \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} =
+ 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+ {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+ \end{equation*}
+
+Display
+~~~~~~~
+
+.. raw:: latex
+
+ \begin{equation*}
+ \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} =
+ 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+ {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+ \end{equation*}
+
+A Rogers-Ramanujan Identity
+---------------------------
+
+Source
+~~~~~~
+
+::
+
+ \begin{equation*}
+ 1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+ \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+ \quad\quad \text{for $|q|<1$}.
+ \end{equation*}
+
+Display
+~~~~~~~
+
+.. raw:: latex
+
+ \begin{equation*}
+ 1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+ \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+ \quad\quad \text{for $|q|<1$}.
+ \end{equation*}
+
+Maxwell's Equations
+-------------------
+
+Source
+~~~~~~
+
+::
+
+ \begin{align}
+ \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\ \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+ \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+ \nabla \cdot \vec{\mathbf{B}} & = 0
+ \end{align}
+
+Display
+~~~~~~~
+
+.. raw:: latex
+
+ \begin{align}
+ \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\ \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+ \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+ \nabla \cdot \vec{\mathbf{B}} & = 0
+ \end{align}
+
+Equation Numbering and References
+=================================
+
+Equation numbering and referencing will be available in a future version
+of the Jupyter notebook.
+
+Inline Typesetting (Mixing Markdown and TeX)
+--------------------------------------------
+
+While display equations look good for a page of samples, the ability to
+mix math and *formatted* **text** in a paragraph is also important.
+
+Source
+~~~~~~
+
+::
+
+ This expression $\sqrt{3x-1}+(1+x)^2$ is an example of a TeX inline equation in a [Markdown-formatted](http://daringfireball.net/projects/markdown/) sentence.
+
+Display
+~~~~~~~
+
+This expression :math:`\sqrt{3x-1}+(1+x)^2` is an example of a TeX
+inline equation in a
+`Markdown-formatted <http://daringfireball.net/projects/markdown/>`__
+sentence.
+
+Other Syntax
+============
+
+You will notice in other places on the web that ``$$`` are needed
+explicitly to begin and end MathJax typesetting. This is **not**
+required if you will be using TeX environments, but the Jupyter notebook
+will accept this syntax on legacy notebooks.
+
+Source
+------
+
+::
+
+ $$
+ \begin{array}{c}
+ y_1 \\\
+ y_2 \mathtt{t}_i \\\
+ z_{3,4}
+ \end{array}
+ $$
+
+::
+
+ $$
+ \begin{array}{c}
+ y_1 \cr
+ y_2 \mathtt{t}_i \cr
+ y_{3}
+ \end{array}
+ $$
+
+::
+
+ $$\begin{eqnarray}
+ x' &=& &x \sin\phi &+& z \cos\phi \\
+ z' &=& - &x \cos\phi &+& z \sin\phi \\
+ \end{eqnarray}$$
+
+::
+
+ $$
+ x=4
+ $$
+
+Display
+-------
+
+.. math::
+
+
+ \begin{array}{c}
+ y_1 \\\
+ y_2 \mathtt{t}_i \\\
+ z_{3,4}
+ \end{array}
+
+.. math::
+
+
+ \begin{array}{c}
+ y_1 \cr
+ y_2 \mathtt{t}_i \cr
+ y_{3}
+ \end{array}
+
+.. math::
+
+ \begin{eqnarray}
+ x' &=& &x \sin\phi &+& z \cos\phi \\
+ z' &=& - &x \cos\phi &+& z \sin\phi \\
+ \end{eqnarray}
+
+.. math::
+
+
+ x=4
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Typesetting%20Equations.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/What is the Jupyter Notebook.rst b/docs/source/examples/Notebook/rstversions/What is the Jupyter Notebook.rst
new file mode 100644
index 0000000..b014e03
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/What is the Jupyter Notebook.rst
@@ -0,0 +1,136 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/What%20is%20the%20Jupyter%20Notebook.ipynb>`__
+
+What is the Jupyter Notebook?
+=============================
+
+Introduction
+------------
+
+The Jupyter Notebook is an **interactive computing environment** that
+enables users to author notebook documents that include: - Live code -
+Interactive widgets - Plots - Narrative text - Equations - Images -
+Video
+
+These documents provide a **complete and self-contained record of a
+computation** that can be converted to various formats and shared with
+others using email, `Dropbox <http://dropbox.com>`__, version control
+systems (like git/\ `GitHub <http://github.com>`__) or
+`nbviewer.jupyter.org <http://nbviewer.jupyter.org>`__.
+
+Components
+~~~~~~~~~~
+
+The Jupyter Notebook combines three components:
+
+- **The notebook web application**: An interactive web application for
+ writing and running code interactively and authoring notebook
+ documents.
+- **Kernels**: Separate processes started by the notebook web
+ application that runs users' code in a given language and returns
+ output back to the notebook web application. The kernel also handles
+ things like computations for interactive widgets, tab completion and
+ introspection.
+- **Notebook documents**: Self-contained documents that contain a
+ representation of all content visible in the notebook web
+ application, including inputs and outputs of the computations,
+ narrative text, equations, images, and rich media representations of
+ objects. Each notebook document has its own kernel.
+
+Notebook web application
+------------------------
+
+The notebook web application enables users to:
+
+- **Edit code in the browser**, with automatic syntax highlighting,
+ indentation, and tab completion/introspection.
+- **Run code from the browser**, with the results of computations
+ attached to the code which generated them.
+- See the results of computations with **rich media representations**,
+ such as HTML, LaTeX, PNG, SVG, PDF, etc.
+- Create and use **interactive JavaScript widgets**, which bind
+ interactive user interface controls and visualizations to reactive
+ kernel side computations.
+- Author **narrative text** using the
+ `Markdown <https://daringfireball.net/projects/markdown/>`__ markup
+ language.
+- Build **hierarchical documents** that are organized into sections
+ with different levels of headings.
+- Include mathematical equations using **LaTeX syntax in Markdown**,
+ which are rendered in-browser by
+ `MathJax <http://www.mathjax.org/>`__.
+
+Kernels
+-------
+
+Through Jupyter's kernel and messaging architecture, the Notebook allows
+code to be run in a range of different programming languages. For each
+notebook document that a user opens, the web application starts a kernel
+that runs the code for that notebook. Each kernel is capable of running
+code in a single programming language and there are kernels available in
+the following languages:
+
+- Python(https://github.com/ipython/ipython)
+- Julia (https://github.com/JuliaLang/IJulia.jl)
+- R (https://github.com/takluyver/IRkernel)
+- Ruby (https://github.com/minrk/iruby)
+- Haskell (https://github.com/gibiansky/IHaskell)
+- Scala (https://github.com/Bridgewater/scala-notebook)
+- node.js (https://gist.github.com/Carreau/4279371)
+- Go (https://github.com/takluyver/igo)
+
+The default kernel runs Python code. The notebook provides a simple way
+for users to pick which of these kernels is used for a given notebook.
+
+Each of these kernels communicate with the notebook web application and
+web browser using a JSON over ZeroMQ/WebSockets message protocol that is
+described
+`here <http://ipython.org/ipython-doc/dev/development/messaging.html>`__.
+Most users don't need to know about these details, but it helps to
+understand that "kernels run code."
+
+Notebook documents
+------------------
+
+Notebook documents contain the **inputs and outputs** of an interactive
+session as well as **narrative text** that accompanies the code but is
+not meant for execution. **Rich output** generated by running code,
+including HTML, images, video, and plots, is embeddeed in the notebook,
+which makes it a complete and self-contained record of a computation.
+
+When you run the notebook web application on your computer, notebook
+documents are just **files on your local filesystem with a ``.ipynb``
+extension**. This allows you to use familiar workflows for organizing
+your notebooks into folders and sharing them with others.
+
+Notebooks consist of a **linear sequence of cells**. There are four
+basic cell types:
+
+- **Code cells:** Input and output of live code that is run in the
+ kernel
+- **Markdown cells:** Narrative text with embedded LaTeX equations
+- **Heading cells:** 6 levels of hierarchical organization and
+ formatting
+- **Raw cells:** Unformatted text that is included, without
+ modification, when notebooks are converted to different formats using
+ nbconvert
+
+Internally, notebook documents are
+**`JSON <http://en.wikipedia.org/wiki/JSON>`__ data** with **binary
+values `base64 <http://en.wikipedia.org/wiki/Base64>`__** encoded. This
+allows them to be **read and manipulated programmatically** by any
+programming language. Because JSON is a text format, notebook documents
+are version control friendly.
+
+**Notebooks can be exported** to different static formats including
+HTML, reStructeredText, LaTeX, PDF, and slide shows
+(`reveal.js <http://lab.hakim.se/reveal-js/#/>`__) using Jupyter's
+``nbconvert`` utility.
+
+Furthermore, any notebook document available from a **public URL on or
+GitHub can be shared** via `nbviewer <http://nbviewer.ipython.org>`__.
+This service loads the notebook document from the URL and renders it as
+a static web page. The resulting web page may thus be shared with others
+**without their needing to install the Jupyter Notebook**.
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/What%20is%20the%20Jupyter%20Notebook.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/Working With Markdown Cells.rst b/docs/source/examples/Notebook/rstversions/Working With Markdown Cells.rst
new file mode 100644
index 0000000..b87404a
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/Working With Markdown Cells.rst
@@ -0,0 +1,313 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Working%20With%20Markdown%20Cells.ipynb>`__
+
+Markdown Cells
+==============
+
+Text can be added to Jupyter Notebooks using Markdown cells. Markdown is
+a popular markup language that is a superset of HTML. Its specification
+can be found here:
+
+http://daringfireball.net/projects/markdown/
+
+Markdown basics
+---------------
+
+You can make text *italic* or **bold**.
+
+You can build nested itemized or enumerated lists:
+
+- One
+
+ - Sublist
+
+ - This
+
+- Sublist - That - The other thing
+- Two
+- Sublist
+- Three
+- Sublist
+
+Now another list:
+
+1. Here we go
+
+ 1. Sublist
+ 2. Sublist
+
+2. There we go
+3. Now this
+
+You can add horizontal rules:
+
+--------------
+
+Here is a blockquote:
+
+ Beautiful is better than ugly. Explicit is better than implicit.
+ Simple is better than complex. Complex is better than complicated.
+ Flat is better than nested. Sparse is better than dense. Readability
+ counts. Special cases aren't special enough to break the rules.
+ Although practicality beats purity. Errors should never pass
+ silently. Unless explicitly silenced. In the face of ambiguity,
+ refuse the temptation to guess. There should be one-- and preferably
+ only one --obvious way to do it. Although that way may not be
+ obvious at first unless you're Dutch. Now is better than never.
+ Although never is often better than *right* now. If the
+ implementation is hard to explain, it's a bad idea. If the
+ implementation is easy to explain, it may be a good idea. Namespaces
+ are one honking great idea -- let's do more of those!
+
+And shorthand for links:
+
+`Jupyter's website <http://jupyter.org>`__
+
+Headings
+--------
+
+You can add headings by starting a line with one (or multiple) ``#``
+followed by a space, as in the following example:
+
+Heading 1
+=========
+
+Heading 2
+=========
+
+Heading 2.1
+-----------
+
+Heading 2.2
+-----------
+
+Embedded code
+-------------
+
+You can embed code meant for illustration instead of execution in
+Python:
+
+::
+
+ def f(x):
+ """a docstring"""
+ return x**2
+
+or other languages:
+
+::
+
+ if (i=0; i<n; i++) {
+ printf("hello %d\n", i);
+ x += 4;
+ }
+
+LaTeX equations
+---------------
+
+Courtesy of MathJax, you can include mathematical expressions both
+inline: :math:`e^{i\pi} + 1 = 0` and displayed:
+
+.. math:: e^x=\sum_{i=0}^\infty \frac{1}{i!}x^i
+
+Inline expressions can be added by surrounding the latex code with
+``$``:
+
+::
+
+ $e^{i\pi} + 1 = 0$
+
+Expressions on their own line are surrounded by ``$$``:
+
+.. code:: latex
+
+ $$e^x=\sum_{i=0}^\infty \frac{1}{i!}x^i$$
+
+Github flavored markdown (GFM)
+------------------------------
+
+The Notebook webapp support Github flavored markdown meaning that you
+can use triple backticks for code blocks
+
+.. raw:: html
+
+ <pre>
+ ```python
+ print "Hello World"
+ ```
+
+ ```javascript
+ console.log("Hello World")
+ ```
+ </pre>
+
+Gives
+
+.. code:: python
+
+ print "Hello World"
+
+.. code:: javascript
+
+ console.log("Hello World")
+
+And a table like this :
+
+.. raw:: html
+
+ <pre>
+ | This | is |
+ |------|------|
+ | a | table|
+ </pre>
+
+A nice Html Table
+
++--------+---------+
+| This | is |
++========+=========+
+| a | table |
++--------+---------+
+
+General HTML
+------------
+
+Because Markdown is a superset of HTML you can even add things like HTML
+tables:
+
+.. raw:: html
+
+ <table>
+
+.. raw:: html
+
+ <tr>
+
+.. raw:: html
+
+ <th>
+
+Header 1
+
+.. raw:: html
+
+ </th>
+
+.. raw:: html
+
+ <th>
+
+Header 2
+
+.. raw:: html
+
+ </th>
+
+.. raw:: html
+
+ </tr>
+
+.. raw:: html
+
+ <tr>
+
+.. raw:: html
+
+ <td>
+
+row 1, cell 1
+
+.. raw:: html
+
+ </td>
+
+.. raw:: html
+
+ <td>
+
+row 1, cell 2
+
+.. raw:: html
+
+ </td>
+
+.. raw:: html
+
+ </tr>
+
+.. raw:: html
+
+ <tr>
+
+.. raw:: html
+
+ <td>
+
+row 2, cell 1
+
+.. raw:: html
+
+ </td>
+
+.. raw:: html
+
+ <td>
+
+row 2, cell 2
+
+.. raw:: html
+
+ </td>
+
+.. raw:: html
+
+ </tr>
+
+.. raw:: html
+
+ </table>
+
+Local files
+-----------
+
+If you have local files in your Notebook directory, you can refer to
+these files in Markdown cells directly:
+
+::
+
+ [subdirectory/]<filename>
+
+For example, in the images folder, we have the Python logo:
+
+::
+
+ <img src="../images/python_logo.svg" />
+
+and a video with the HTML5 video tag:
+
+::
+
+ <video controls src="images/animation.m4v" />
+
+.. raw:: html
+
+ <video controls src="images/animation.m4v" />
+
+These do not embed the data into the notebook file, and require that the
+files exist when you are viewing the notebook.
+
+Security of local files
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Note that this means that the Jupyter notebook server also acts as a
+generic file server for files inside the same tree as your notebooks.
+Access is not granted outside the notebook folder so you have strict
+control over what files are visible, but for this reason it is highly
+recommended that you do not run the notebook server with a notebook
+directory at a high level in your filesystem (e.g. your home directory).
+
+When you run the notebook in a password-protected manner, local file
+access is restricted to authenticated users unless read-only views are
+active.
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Working%20With%20Markdown%20Cells.ipynb>`__
diff --git a/docs/source/examples/Notebook/rstversions/index.rst b/docs/source/examples/Notebook/rstversions/index.rst
new file mode 100644
index 0000000..512437d
--- /dev/null
+++ b/docs/source/examples/Notebook/rstversions/index.rst
@@ -0,0 +1,37 @@
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Examples%20and%20Tutorials%20Index.ipynb>`__
+
+
+
+Examples and Tutorials
+======================
+
+This portion of the documentation was generated from notebook files. You
+can download the original interactive notebook files using the links at
+the tops and bottoms of the pages.
+
+Tutorials
+---------
+
+.. toctree::
+ :maxdepth: 1
+
+ What is the Jupyter Notebook
+ Notebook Basics
+ Running Code
+ Working With Markdown Cells
+ Custom Keyboard Shortcuts
+ JavaScript Notebook Extensions
+
+Examples
+--------
+
+.. toctree::
+ :maxdepth: 1
+
+ Importing Notebooks
+ Connecting with the Qt Console
+ Typesetting Equations
+
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Examples%20and%20Tutorials%20Index.ipynb>`__
diff --git a/docs/source/examples/images/FrontendKernel.graffle/data.plist b/docs/source/examples/images/FrontendKernel.graffle/data.plist
new file mode 100644
index 0000000..6179209
--- /dev/null
+++ b/docs/source/examples/images/FrontendKernel.graffle/data.plist
@@ -0,0 +1,461 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>ActiveLayerIndex</key>
+ <integer>0</integer>
+ <key>ApplicationVersion</key>
+ <array>
+ <string>com.omnigroup.OmniGraffle</string>
+ <string>139.18.0.187838</string>
+ </array>
+ <key>AutoAdjust</key>
+ <true/>
+ <key>BackgroundGraphic</key>
+ <dict>
+ <key>Bounds</key>
+ <string>{{0, 0}, {576, 733}}</string>
+ <key>Class</key>
+ <string>SolidGraphic</string>
+ <key>ID</key>
+ <integer>2</integer>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>BaseZoom</key>
+ <integer>0</integer>
+ <key>CanvasOrigin</key>
+ <string>{0, 0}</string>
+ <key>ColumnAlign</key>
+ <integer>1</integer>
+ <key>ColumnSpacing</key>
+ <real>36</real>
+ <key>CreationDate</key>
+ <string>2014-05-27 21:39:30 +0000</string>
+ <key>Creator</key>
+ <string>bgranger</string>
+ <key>DisplayScale</key>
+ <string>1 0/72 in = 1.0000 in</string>
+ <key>GraphDocumentVersion</key>
+ <integer>8</integer>
+ <key>GraphicsList</key>
+ <array>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 0}</string>
+ <string>{-7, 8}</string>
+ <string>{6.9999849080788863, -8.0000033519149838}</string>
+ <string>{0, 0}</string>
+ </array>
+ <key>ID</key>
+ <integer>29</integer>
+ <key>Points</key>
+ <array>
+ <string>{164, 341.5}</string>
+ <string>{186.5, 338}</string>
+ <string>{196, 327.5}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{107.64779663085938, 305.5}, {69.088050842285156, 84.499992370605469}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>9</integer>
+ <key>ImageID</key>
+ <integer>1</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>6</integer>
+ <key>Position</key>
+ <real>0.53676468133926392</real>
+ </dict>
+ <key>ID</key>
+ <integer>8</integer>
+ <key>Points</key>
+ <array>
+ <string>{288.09285678056523, 276}</string>
+ <string>{288.49999833106995, 304.50001973116196}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>FilledArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>7</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{207, 263}, {162, 13}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>7</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Pad</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf190
+\cocoascreenfonts1{\fonttbl\f0\fnil\fcharset0 xkcd-Regular;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs20 \cf0 Interactive Computing Protocol}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>1</integer>
+ </dict>
+ <key>ID</key>
+ <integer>6</integer>
+ <key>Points</key>
+ <array>
+ <string>{252, 304.50001973116196}</string>
+ <string>{320, 304.50001973116196}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>FilledArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>FilledArrow</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>5</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{186.5, 286.5}, {65, 36}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>5</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf190
+\cocoascreenfonts1{\fonttbl\f0\fnil\fcharset0 xkcd-Regular;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs20 \cf0 Frontend}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{320.5, 286.5}, {65, 36}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>1</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf190
+\cocoascreenfonts1{\fonttbl\f0\fnil\fcharset0 xkcd-Regular;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs20 \cf0 Kernel}</string>
+ </dict>
+ </dict>
+ </array>
+ <key>GridInfo</key>
+ <dict/>
+ <key>GuidesLocked</key>
+ <string>NO</string>
+ <key>GuidesVisible</key>
+ <string>YES</string>
+ <key>HPages</key>
+ <integer>1</integer>
+ <key>ImageCounter</key>
+ <integer>2</integer>
+ <key>ImageLinkBack</key>
+ <array>
+ <dict/>
+ </array>
+ <key>ImageList</key>
+ <array>
+ <string>image1.png</string>
+ </array>
+ <key>KeepToScale</key>
+ <false/>
+ <key>Layers</key>
+ <array>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>Layer 1</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>YES</string>
+ </dict>
+ </array>
+ <key>LayoutInfo</key>
+ <dict>
+ <key>Animate</key>
+ <string>NO</string>
+ <key>circoMinDist</key>
+ <real>18</real>
+ <key>circoSeparation</key>
+ <real>0.0</real>
+ <key>layoutEngine</key>
+ <string>dot</string>
+ <key>neatoSeparation</key>
+ <real>0.0</real>
+ <key>twopiSeparation</key>
+ <real>0.0</real>
+ </dict>
+ <key>LinksVisible</key>
+ <string>NO</string>
+ <key>MagnetsVisible</key>
+ <string>NO</string>
+ <key>MasterSheets</key>
+ <array/>
+ <key>ModificationDate</key>
+ <string>2014-05-27 22:28:18 +0000</string>
+ <key>Modifier</key>
+ <string>bgranger</string>
+ <key>NotesVisible</key>
+ <string>NO</string>
+ <key>Orientation</key>
+ <integer>2</integer>
+ <key>OriginVisible</key>
+ <string>NO</string>
+ <key>PageBreaks</key>
+ <string>YES</string>
+ <key>PrintInfo</key>
+ <dict>
+ <key>NSBottomMargin</key>
+ <array>
+ <string>float</string>
+ <string>41</string>
+ </array>
+ <key>NSHorizonalPagination</key>
+ <array>
+ <string>coded</string>
+ <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
+ </array>
+ <key>NSLeftMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSPaperSize</key>
+ <array>
+ <string>size</string>
+ <string>{612, 792}</string>
+ </array>
+ <key>NSPrintReverseOrientation</key>
+ <array>
+ <string>int</string>
+ <string>0</string>
+ </array>
+ <key>NSRightMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSTopMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ </dict>
+ <key>PrintOnePage</key>
+ <false/>
+ <key>ReadOnly</key>
+ <string>NO</string>
+ <key>RowAlign</key>
+ <integer>1</integer>
+ <key>RowSpacing</key>
+ <real>36</real>
+ <key>SheetTitle</key>
+ <string>Canvas 1</string>
+ <key>SmartAlignmentGuidesActive</key>
+ <string>YES</string>
+ <key>SmartDistanceGuidesActive</key>
+ <string>YES</string>
+ <key>UniqueID</key>
+ <integer>1</integer>
+ <key>UseEntirePage</key>
+ <false/>
+ <key>VPages</key>
+ <integer>1</integer>
+ <key>WindowInfo</key>
+ <dict>
+ <key>CurrentSheet</key>
+ <integer>0</integer>
+ <key>ExpandedCanvases</key>
+ <array>
+ <dict>
+ <key>name</key>
+ <string>Canvas 1</string>
+ </dict>
+ </array>
+ <key>Frame</key>
+ <string>{{277, 7}, {832, 871}}</string>
+ <key>ListView</key>
+ <true/>
+ <key>OutlineWidth</key>
+ <integer>142</integer>
+ <key>RightSidebar</key>
+ <false/>
+ <key>ShowRuler</key>
+ <true/>
+ <key>Sidebar</key>
+ <true/>
+ <key>SidebarWidth</key>
+ <integer>120</integer>
+ <key>VisibleRegion</key>
+ <string>{{96.5, 197.5}, {348.5, 366}}</string>
+ <key>Zoom</key>
+ <real>2</real>
+ <key>ZoomValues</key>
+ <array>
+ <array>
+ <string>Canvas 1</string>
+ <real>2</real>
+ <real>1</real>
+ </array>
+ </array>
+ </dict>
+</dict>
+</plist>
diff --git a/docs/source/examples/images/FrontendKernel.graffle/image1.png b/docs/source/examples/images/FrontendKernel.graffle/image1.png
new file mode 100644
index 0000000..c01e349
--- /dev/null
+++ b/docs/source/examples/images/FrontendKernel.graffle/image1.png
Binary files differ
diff --git a/docs/source/examples/images/FrontendKernel.png b/docs/source/examples/images/FrontendKernel.png
new file mode 100644
index 0000000..62aa890
--- /dev/null
+++ b/docs/source/examples/images/FrontendKernel.png
Binary files differ
diff --git a/docs/source/examples/images/animation.m4v b/docs/source/examples/images/animation.m4v
new file mode 100644
index 0000000..13ecf88
--- /dev/null
+++ b/docs/source/examples/images/animation.m4v
Binary files differ
diff --git a/docs/source/examples/images/ipython_logo.png b/docs/source/examples/images/ipython_logo.png
new file mode 100644
index 0000000..e9bdce3
--- /dev/null
+++ b/docs/source/examples/images/ipython_logo.png
Binary files differ
diff --git a/docs/source/examples/images/jupyter_logo.png b/docs/source/examples/images/jupyter_logo.png
new file mode 100644
index 0000000..54cc416
--- /dev/null
+++ b/docs/source/examples/images/jupyter_logo.png
Binary files differ
diff --git a/docs/source/examples/images/python_logo.svg b/docs/source/examples/images/python_logo.svg
new file mode 100644
index 0000000..116eaac
--- /dev/null
+++ b/docs/source/examples/images/python_logo.svg
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ width="388.84pt"
+ height="115.02pt"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.43"
+ sodipodi:docname="logo-python-generic.svg"
+ sodipodi:docbase="/home/sdeibel">
+ <metadata
+ id="metadata2193">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ inkscape:window-height="543"
+ inkscape:window-width="791"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ inkscape:zoom="1.4340089"
+ inkscape:cx="243.02499"
+ inkscape:cy="71.887497"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:current-layer="svg2" />
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient2795">
+ <stop
+ style="stop-color:#b8b8b8;stop-opacity:0.49803922"
+ offset="0"
+ id="stop2797" />
+ <stop
+ style="stop-color:#7f7f7f;stop-opacity:0"
+ offset="1"
+ id="stop2799" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient2787">
+ <stop
+ style="stop-color:#7f7f7f;stop-opacity:0.5"
+ offset="0"
+ id="stop2789" />
+ <stop
+ style="stop-color:#7f7f7f;stop-opacity:0"
+ offset="1"
+ id="stop2791" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3676">
+ <stop
+ style="stop-color:#b2b2b2;stop-opacity:0.5"
+ offset="0"
+ id="stop3678" />
+ <stop
+ style="stop-color:#b3b3b3;stop-opacity:0"
+ offset="1"
+ id="stop3680" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3236">
+ <stop
+ style="stop-color:#f4f4f4;stop-opacity:1"
+ offset="0"
+ id="stop3244" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="1"
+ id="stop3240" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4671">
+ <stop
+ style="stop-color:#ffd43b;stop-opacity:1"
+ offset="0"
+ id="stop4673" />
+ <stop
+ style="stop-color:#ffe873;stop-opacity:1"
+ offset="1"
+ id="stop4675" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4689">
+ <stop
+ style="stop-color:#5a9fd4;stop-opacity:1"
+ offset="0"
+ id="stop4691" />
+ <stop
+ style="stop-color:#306998;stop-opacity:1"
+ offset="1"
+ id="stop4693" />
+ </linearGradient>
+ <linearGradient
+ x1="224.23996"
+ y1="144.75717"
+ x2="-65.308502"
+ y2="144.75717"
+ id="linearGradient2987"
+ xlink:href="#linearGradient4671"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(100.2702,99.61116)" />
+ <linearGradient
+ x1="172.94208"
+ y1="77.475983"
+ x2="26.670298"
+ y2="76.313133"
+ id="linearGradient2990"
+ xlink:href="#linearGradient4689"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(100.2702,99.61116)" />
+ <linearGradient
+ x1="172.94208"
+ y1="77.475983"
+ x2="26.670298"
+ y2="76.313133"
+ id="linearGradient2587"
+ xlink:href="#linearGradient4689"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(100.2702,99.61116)" />
+ <linearGradient
+ x1="224.23996"
+ y1="144.75717"
+ x2="-65.308502"
+ y2="144.75717"
+ id="linearGradient2589"
+ xlink:href="#linearGradient4671"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(100.2702,99.61116)" />
+ <linearGradient
+ x1="172.94208"
+ y1="77.475983"
+ x2="26.670298"
+ y2="76.313133"
+ id="linearGradient2248"
+ xlink:href="#linearGradient4689"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(100.2702,99.61116)" />
+ <linearGradient
+ x1="224.23996"
+ y1="144.75717"
+ x2="-65.308502"
+ y2="144.75717"
+ id="linearGradient2250"
+ xlink:href="#linearGradient4671"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(100.2702,99.61116)" />
+ <linearGradient
+ x1="224.23996"
+ y1="144.75717"
+ x2="-65.308502"
+ y2="144.75717"
+ id="linearGradient2255"
+ xlink:href="#linearGradient4671"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.562541,0,0,0.567972,-11.5974,-7.60954)" />
+ <linearGradient
+ x1="172.94208"
+ y1="76.176224"
+ x2="26.670298"
+ y2="76.313133"
+ id="linearGradient2258"
+ xlink:href="#linearGradient4689"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.562541,0,0,0.567972,-11.5974,-7.60954)" />
+ <radialGradient
+ cx="61.518883"
+ cy="132.28575"
+ r="29.036913"
+ fx="61.518883"
+ fy="132.28575"
+ id="radialGradient2801"
+ xlink:href="#linearGradient2795"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1,0,0,0.177966,0,108.7434)" />
+ <linearGradient
+ x1="150.96111"
+ y1="192.35176"
+ x2="112.03144"
+ y2="137.27299"
+ id="linearGradient1475"
+ xlink:href="#linearGradient4671"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)" />
+ <linearGradient
+ x1="26.648937"
+ y1="20.603781"
+ x2="135.66525"
+ y2="114.39767"
+ id="linearGradient1478"
+ xlink:href="#linearGradient4689"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)" />
+ <radialGradient
+ cx="61.518883"
+ cy="132.28575"
+ r="29.036913"
+ fx="61.518883"
+ fy="132.28575"
+ id="radialGradient1480"
+ xlink:href="#linearGradient2795"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.382716e-8,-0.296405,1.43676,4.683673e-7,-128.544,150.5202)" />
+ </defs>
+ <g
+ id="g2303">
+ <path
+ id="path46"
+ style="fill:#646464;fill-opacity:1"
+ d="M 184.61344,61.929363 C 184.61344,47.367213 180.46118,39.891193 172.15666,39.481813 C 168.85239,39.325863 165.62611,39.852203 162.48754,41.070593 C 159.98254,41.967323 158.2963,42.854313 157.40931,43.751043 L 157.40931,78.509163 C 162.72147,81.842673 167.43907,83.392453 171.55234,83.148783 C 180.25649,82.573703 184.61344,75.507063 184.61344,61.929363 z M 194.85763,62.533683 C 194.85763,69.931723 193.12265,76.072393 189.63319,80.955683 C 185.7441,86.482283 180.35396,89.328433 173.46277,89.484393 C 168.26757,89.650093 162.91642,88.022323 157.40931,84.610843 L 157.40931,116.20116 L 148.50047,113.02361 L 148.50047,42.903043 C 149.96253,41.109583 151.84372,39.569543 154.12454,38.263433 C 159.42696,35.173603 165.86978,33.584823 173.45302,33.506853 L 173.57973,33.633563 C 180.50991,33.545833 185.85132,36.391993 189.60395,42.162263 C 193.10315,47.454933 194.85763,54.238913 194.85763,62.533683 z " />
+ <path
+ id="path48"
+ style="fill:#646464;fill-opacity:1"
+ d="M 249.30487,83.265743 C 249.30487,93.188283 248.31067,100.05998 246.32227,103.88084 C 244.32411,107.7017 240.52275,110.75254 234.90842,113.02361 C 230.35653,114.81707 225.43425,115.79178 220.15133,115.95748 L 218.67952,110.34316 C 224.05016,109.61213 227.83204,108.88109 230.02513,108.15006 C 234.34309,106.688 237.30621,104.44617 238.93397,101.44406 C 240.24008,98.997543 240.88339,94.328693 240.88339,87.418003 L 240.88339,85.098203 C 234.79146,87.866373 228.40711,89.240713 221.73036,89.240713 C 217.34417,89.240713 213.47457,87.866373 210.14107,85.098203 C 206.39818,82.086343 204.52674,78.265483 204.52674,73.635623 L 204.52674,36.557693 L 213.43558,33.506853 L 213.43558,70.828453 C 213.43558,74.815013 214.7222,77.885353 217.29543,80.039463 C 219.86866,82.193563 223.20217,83.226753 227.2862,83.148783 C 231.37023,83.061053 235.74667,81.482023 240.39603,78.392203 L 240.39603,34.851953 L 249.30487,34.851953 L 249.30487,83.265743 z " />
+ <path
+ id="path50"
+ style="fill:#646464;fill-opacity:1"
+ d="M 284.08249,88.997033 C 283.02006,89.084753 282.04535,89.123743 281.14862,89.123743 C 276.10937,89.123743 272.18129,87.924853 269.37413,85.517323 C 266.57671,83.109793 265.17314,79.786033 265.17314,75.546053 L 265.17314,40.456523 L 259.07146,40.456523 L 259.07146,34.851953 L 265.17314,34.851953 L 265.17314,19.968143 L 274.07223,16.800333 L 274.07223,34.851953 L 284.08249,34.851953 L 284.08249,40.456523 L 274.07223,40.456523 L 274.07223,75.302373 C 274.07223,78.645623 274.96896,81.014163 276.76243,82.398253 C 278.30247,83.538663 280.74899,84.191723 284.08249,84.357423 L 284.08249,88.997033 z " />
+ <path
+ id="path52"
+ style="fill:#646464;fill-opacity:1"
+ d="M 338.02288,88.266003 L 329.11404,88.266003 L 329.11404,53.878273 C 329.11404,50.379063 328.29528,47.367213 326.66753,44.852463 C 324.78634,42.006313 322.17411,40.583233 318.82112,40.583233 C 314.73708,40.583233 309.6296,42.737343 303.4987,47.045563 L 303.4987,88.266003 L 294.58985,88.266003 L 294.58985,6.0687929 L 303.4987,3.2616329 L 303.4987,40.700203 C 309.191,36.557693 315.40963,34.481563 322.16436,34.481563 C 326.88196,34.481563 330.70282,36.070333 333.62694,39.238143 C 336.56082,42.405943 338.02288,46.353513 338.02288,51.071103 L 338.02288,88.266003 L 338.02288,88.266003 z " />
+ <path
+ id="path54"
+ style="fill:#646464;fill-opacity:1"
+ d="M 385.37424,60.525783 C 385.37424,54.930953 384.31182,50.310833 382.19669,46.655673 C 379.68195,42.201253 375.77337,39.852203 370.49044,39.608523 C 360.72386,40.173863 355.85032,47.172273 355.85032,60.584263 C 355.85032,66.734683 356.86401,71.871393 358.91089,75.994413 C 361.52312,81.248093 365.44145,83.840823 370.66589,83.753103 C 380.47146,83.675123 385.37424,75.935933 385.37424,60.525783 z M 395.13109,60.584263 C 395.13109,68.547643 393.09395,75.175663 389.02941,80.468333 C 384.5555,86.394563 378.37584,89.367423 370.49044,89.367423 C 362.67328,89.367423 356.58135,86.394563 352.18541,80.468333 C 348.19885,75.175663 346.21044,68.547643 346.21044,60.584263 C 346.21044,53.098503 348.36455,46.801883 352.67276,41.674913 C 357.22466,36.236033 363.20937,33.506853 370.6074,33.506853 C 378.00545,33.506853 384.02914,36.236033 388.66877,41.674913 C 392.97697,46.801883 395.13109,53.098503 395.13109,60.584263 z " />
+ <path
+ id="path56"
+ style="fill:#646464;fill-opacity:1"
+ d="M 446.20583,88.266003 L 437.29699,88.266003 L 437.29699,51.928853 C 437.29699,47.942293 436.0981,44.832973 433.70032,42.591133 C 431.30253,40.359053 428.10549,39.277123 424.11893,39.364853 C 419.8887,39.442833 415.86314,40.826913 412.04229,43.507363 L 412.04229,88.266003 L 403.13345,88.266003 L 403.13345,42.405943 C 408.26042,38.672813 412.97801,36.236033 417.28621,35.095623 C 421.35076,34.033193 424.93769,33.506853 428.02752,33.506853 C 430.14264,33.506853 432.13104,33.711543 434.00248,34.120913 C 437.50169,34.929923 440.34783,36.430973 442.54093,38.633823 C 444.98744,41.070593 446.20583,43.994723 446.20583,47.415943 L 446.20583,88.266003 z " />
+ <path
+ id="path1948"
+ style="fill:url(#linearGradient1478);fill-opacity:1"
+ d="M 60.510156,6.3979729 C 55.926503,6.4192712 51.549217,6.8101906 47.697656,7.4917229 C 36.35144,9.4962267 34.291407,13.691825 34.291406,21.429223 L 34.291406,31.647973 L 61.103906,31.647973 L 61.103906,35.054223 L 34.291406,35.054223 L 24.228906,35.054223 C 16.436447,35.054223 9.6131468,39.73794 7.4789058,48.647973 C 5.0170858,58.860939 4.9078907,65.233996 7.4789058,75.897973 C 9.3848341,83.835825 13.936449,89.491721 21.728906,89.491723 L 30.947656,89.491723 L 30.947656,77.241723 C 30.947656,68.391821 38.6048,60.585475 47.697656,60.585473 L 74.478906,60.585473 C 81.933857,60.585473 87.885159,54.447309 87.885156,46.960473 L 87.885156,21.429223 C 87.885156,14.162884 81.755176,8.7044455 74.478906,7.4917229 C 69.872919,6.7249976 65.093809,6.3766746 60.510156,6.3979729 z M 46.010156,14.616723 C 48.779703,14.616723 51.041406,16.915369 51.041406,19.741723 C 51.041404,22.558059 48.779703,24.835473 46.010156,24.835473 C 43.23068,24.835472 40.978906,22.558058 40.978906,19.741723 C 40.978905,16.91537 43.23068,14.616723 46.010156,14.616723 z " />
+ <path
+ id="path1950"
+ style="fill:url(#linearGradient1475);fill-opacity:1"
+ d="M 91.228906,35.054223 L 91.228906,46.960473 C 91.228906,56.191228 83.403011,63.960472 74.478906,63.960473 L 47.697656,63.960473 C 40.361823,63.960473 34.291407,70.238956 34.291406,77.585473 L 34.291406,103.11672 C 34.291406,110.38306 40.609994,114.65704 47.697656,116.74172 C 56.184987,119.23733 64.323893,119.68835 74.478906,116.74172 C 81.229061,114.78733 87.885159,110.85411 87.885156,103.11672 L 87.885156,92.897973 L 61.103906,92.897973 L 61.103906,89.491723 L 87.885156,89.491723 L 101.29141,89.491723 C 109.08387,89.491723 111.98766,84.056315 114.69765,75.897973 C 117.49698,67.499087 117.37787,59.422197 114.69765,48.647973 C 112.77187,40.890532 109.09378,35.054223 101.29141,35.054223 L 91.228906,35.054223 z M 76.166406,99.710473 C 78.945884,99.710476 81.197656,101.98789 81.197656,104.80422 C 81.197654,107.63057 78.945881,109.92922 76.166406,109.92922 C 73.396856,109.92922 71.135156,107.63057 71.135156,104.80422 C 71.135158,101.98789 73.396853,99.710473 76.166406,99.710473 z " />
+ <path
+ id="text3004"
+ style="font-size:15.16445827px;font-style:normal;font-weight:normal;line-height:125%;fill:#646464;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+ d="M 463.5544,26.909383 L 465.11635,26.909383 L 465.11635,17.113143 L 468.81648,17.113143 L 468.81648,15.945483 L 459.85427,15.945483 L 459.85427,17.113143 L 463.5544,17.113143 L 463.5544,26.909383 M 470.20142,26.909383 L 471.53589,26.909383 L 471.53589,17.962353 L 474.4323,26.908259 L 475.91799,26.908259 L 478.93615,17.992683 L 478.93615,26.909383 L 480.39194,26.909383 L 480.39194,15.945483 L 478.46605,15.945483 L 475.16774,25.33834 L 472.35477,15.945483 L 470.20142,15.945483 L 470.20142,26.909383" />
+ <path
+ id="path1894"
+ style="opacity:0.44382019;fill:url(#radialGradient1480);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(0.73406,0,0,0.809524,16.24958,27.00935)"
+ d="M 110.46717 132.28575 A 48.948284 8.6066771 0 1 1 12.570599,132.28575 A 48.948284 8.6066771 0 1 1 110.46717 132.28575 z" />
+ </g>
+</svg>
diff --git a/docs/source/examples/utils/list_pyfiles.ipy b/docs/source/examples/utils/list_pyfiles.ipy
new file mode 100644
index 0000000..71a9e5d
--- /dev/null
+++ b/docs/source/examples/utils/list_pyfiles.ipy
@@ -0,0 +1,6 @@
+# A simple IPython script that provides Notebook links to .py files in the cwd
+
+from IPython.display import FileLink, display
+files =!ls *.py
+for f in files:
+ display(FileLink(f)) \ No newline at end of file
diff --git a/docs/source/examples/utils/list_subdirs.ipy b/docs/source/examples/utils/list_subdirs.ipy
new file mode 100644
index 0000000..a1bbb74
--- /dev/null
+++ b/docs/source/examples/utils/list_subdirs.ipy
@@ -0,0 +1,7 @@
+# A simple IPython script that lists files in all subdirs
+
+from IPython.display import FileLinks, display
+dirs =!ls -d */
+for d in dirs:
+ if d != '__pycache__/':
+ display(FileLinks(d)) \ No newline at end of file
diff --git a/docs/source/extending/contents.rst b/docs/source/extending/contents.rst
new file mode 100644
index 0000000..9271d8e
--- /dev/null
+++ b/docs/source/extending/contents.rst
@@ -0,0 +1,219 @@
+.. _contents_api:
+
+Contents API
+============
+
+.. currentmodule:: notebook.services.contents
+
+The Jupyter Notebook web application provides a graphical interface for
+creating, opening, renaming, and deleting files in a virtual filesystem.
+
+The :class:`~manager.ContentsManager` class defines an abstract
+API for translating these interactions into operations on a particular storage
+medium. The default implementation,
+:class:`~filemanager.FileContentsManager`, uses the local
+filesystem of the server for storage and straightforwardly serializes notebooks
+into JSON. Users can override these behaviors by supplying custom subclasses
+of ContentsManager.
+
+This section describes the interface implemented by ContentsManager subclasses.
+We refer to this interface as the **Contents API**.
+
+Data Model
+----------
+
+.. currentmodule:: notebook.services.contents.manager
+
+Filesystem Entities
+~~~~~~~~~~~~~~~~~~~
+.. _notebook models:
+
+ContentsManager methods represent virtual filesystem entities as dictionaries,
+which we refer to as **models**.
+
+Models may contain the following entries:
+
++--------------------+-----------+------------------------------+
+| Key | Type |Info |
++====================+===========+==============================+
+|**name** |unicode |Basename of the entity. |
++--------------------+-----------+------------------------------+
+|**path** |unicode |Full |
+| | |(:ref:`API-style<apipaths>`) |
+| | |path to the entity. |
++--------------------+-----------+------------------------------+
+|**type** |unicode |The entity type. One of |
+| | |``"notebook"``, ``"file"`` or |
+| | |``"directory"``. |
++--------------------+-----------+------------------------------+
+|**created** |datetime |Creation date of the entity. |
++--------------------+-----------+------------------------------+
+|**last_modified** |datetime |Last modified date of the |
+| | |entity. |
++--------------------+-----------+------------------------------+
+|**content** |variable |The "content" of the entity. |
+| | |(:ref:`See |
+| | |Below<modelcontent>`) |
++--------------------+-----------+------------------------------+
+|**mimetype** |unicode or |The mimetype of ``content``, |
+| |``None`` |if any. (:ref:`See |
+| | |Below<modelcontent>`) |
++--------------------+-----------+------------------------------+
+|**format** |unicode or |The format of ``content``, |
+| |``None`` |if any. (:ref:`See |
+| | |Below<modelcontent>`) |
++--------------------+-----------+------------------------------+
+
+.. _modelcontent:
+
+Certain model fields vary in structure depending on the ``type`` field of the
+model. There are three model types: **notebook**, **file**, and **directory** .
+
+- ``notebook`` models
+ - The ``format`` field is always ``"json"``.
+ - The ``mimetype`` field is always ``None``.
+ - The ``content`` field contains a
+ :class:`nbformat.notebooknode.NotebookNode` representing the .ipynb file
+ represented by the model. See the `NBFormat`_ documentation for a full
+ description.
+
+- ``file`` models
+ - The ``format`` field is either ``"text"`` or ``"base64"``.
+ - The ``mimetype`` field is ``text/plain`` for text-format models and
+ ``application/octet-stream`` for base64-format models.
+ - The ``content`` field is always of type ``unicode``. For text-format
+ file models, ``content`` simply contains the file's bytes after decoding
+ as UTF-8. Non-text (``base64``) files are read as bytes, base64 encoded,
+ and then decoded as UTF-8.
+
+- ``directory`` models
+ - The ``format`` field is always ``"json"``.
+ - The ``mimetype`` field is always ``None``.
+ - The ``content`` field contains a list of :ref:`content-free<contentfree>`
+ models representing the entities in the directory.
+
+.. note::
+
+ .. _contentfree:
+
+ In certain circumstances, we don't need the full content of an entity to
+ complete a Contents API request. In such cases, we omit the ``mimetype``,
+ ``content``, and ``format`` keys from the model. This most commonly occurs
+ when listing a directory, in which circumstance we represent files within
+ the directory as content-less models to avoid having to recursively traverse
+ and serialize the entire filesystem.
+
+**Sample Models**
+
+.. sourcecode:: python
+
+ # Notebook Model with Content
+ {
+ 'content': {
+ 'metadata': {},
+ 'nbformat': 4,
+ 'nbformat_minor': 0,
+ 'cells': [
+ {
+ 'cell_type': 'markdown',
+ 'metadata': {},
+ 'source': 'Some **Markdown**',
+ },
+ ],
+ },
+ 'created': datetime(2015, 7, 25, 19, 50, 19, 19865),
+ 'format': 'json',
+ 'last_modified': datetime(2015, 7, 25, 19, 50, 19, 19865),
+ 'mimetype': None,
+ 'name': 'a.ipynb',
+ 'path': 'foo/a.ipynb',
+ 'type': 'notebook',
+ 'writable': True,
+ }
+
+ # Notebook Model without Content
+ {
+ 'content': None,
+ 'created': datetime.datetime(2015, 7, 25, 20, 17, 33, 271931),
+ 'format': None,
+ 'last_modified': datetime.datetime(2015, 7, 25, 20, 17, 33, 271931),
+ 'mimetype': None,
+ 'name': 'a.ipynb',
+ 'path': 'foo/a.ipynb',
+ 'type': 'notebook',
+ 'writable': True
+ }
+
+
+API Paths
+~~~~~~~~~
+.. _apipaths:
+
+ContentsManager methods represent the locations of filesystem resources as
+**API-style paths**. Such paths are interpreted as relative to the root
+directory of the notebook server. For compatibility across systems, the
+following guarantees are made:
+
+* Paths are always ``unicode``, not ``bytes``.
+* Paths are not URL-escaped.
+* Paths are always forward-slash (/) delimited, even on Windows.
+* Leading and trailing slashes are stripped. For example, ``/foo/bar/buzz/``
+ becomes ``foo/bar/buzz``.
+* The empty string (``""``) represents the root directory.
+
+
+Writing a Custom ContentsManager
+--------------------------------
+
+The default ContentsManager is designed for users running the notebook as an
+application on a personal computer. It stores notebooks as .ipynb files on the
+local filesystem, and it maps files and directories in the Notebook UI to files
+and directories on disk. It is possible to override how notebooks are stored
+by implementing your own custom subclass of ``ContentsManager``. For example,
+if you deploy the notebook in a context where you don't trust or don't have
+access to the filesystem of the notebook server, it's possible to write your
+own ContentsManager that stores notebooks and files in a database.
+
+
+Required Methods
+~~~~~~~~~~~~~~~~
+
+A minimal complete implementation of a custom
+:class:`~manager.ContentsManager` must implement the following
+methods:
+
+.. autosummary::
+ ContentsManager.get
+ ContentsManager.save
+ ContentsManager.delete_file
+ ContentsManager.rename_file
+ ContentsManager.file_exists
+ ContentsManager.dir_exists
+ ContentsManager.is_hidden
+
+
+Customizing Checkpoints
+-----------------------
+
+TODO:
+
+
+Testing
+-------
+.. currentmodule:: notebook.services.contents.tests
+
+:mod:`notebook.services.contents.tests` includes several test suites written
+against the abstract Contents API. This means that an excellent way to test a
+new ContentsManager subclass is to subclass our tests to make them use your
+ContentsManager.
+
+.. note::
+
+ PGContents_ is an example of a complete implementation of a custom
+ ``ContentsManager``. It stores notebooks and files in PostgreSQL_ and encodes
+ directories as SQL relations. PGContents also provides an example of how to
+ re-use the notebook's tests.
+
+.. _NBFormat: http://nbformat.readthedocs.org/en/latest/index.html
+.. _PGContents: https://github.com/quantopian/pgcontents
+.. _PostgreSQL: http://www.postgresql.org/
diff --git a/docs/source/extending/frontend_extensions.rst b/docs/source/extending/frontend_extensions.rst
new file mode 100644
index 0000000..405b362
--- /dev/null
+++ b/docs/source/extending/frontend_extensions.rst
@@ -0,0 +1,231 @@
+Custom front-end extensions
+===========================
+
+This describes the basic steps to write a JavaScript extension for the Jupyter
+notebook front-end. This allows you to customize the behaviour of the various
+pages like the dashboard, the notebook, or the text editor.
+
+The structure of a front-end extension
+--------------------------------------
+
+.. note::
+
+ The notebook front-end and Javascript API are not stable, and are subject
+ to a lot of changes. Any extension written for the current notebook is
+ almost guaranteed to break in the next release.
+
+.. _AMD module: https://en.wikipedia.org/wiki/Asynchronous_module_definition
+
+A front-end extension is a JavaScript file that defines an `AMD module`_
+which exposes at least a function called ``load_ipython_extension``, which
+takes no arguments. We will not get into the details of what each of these
+terms consists of yet, but here is the minimal code needed for a working
+extension:
+
+.. code:: javascript
+
+ // file my_extension/main.js
+
+ define(function(){
+
+ function load_ipython_extension(){
+ console.info('this is my first extension');
+ }
+
+ return {
+ load_ipython_extension: load_ipython_extension
+ };
+ });
+
+.. note::
+
+ Although for historical reasons the function is called
+ ``load_ipython_extension``, it does apply to the Jupyter notebook in
+ general, and will work regardless of the kernel in use.
+
+If you are familiar with JavaScript, you can use this template to require any
+Jupyter module and modify its configuration, or do anything else in client-side
+Javascript. Your extension will be loaded at the right time during the notebook
+page initialisation for you to set up a listener for the various events that
+the page can trigger.
+
+You might want access to the current instances of the various Jupyter notebook
+components on the page, as opposed to the classes defined in the modules. The
+current instances are exposed by a module named ``base/js/namespace``. If you
+plan on accessing instances on the page, you should ``require`` this module
+rather than accessing the global variable ``Jupyter``, which will be removed in
+future. The following example demonstrates how to access the current notebook
+instance:
+
+.. code:: javascript
+
+ // file my_extension/main.js
+
+ define([
+ 'base/js/namespace'
+ ], function(
+ Jupyter
+ ) {
+ function load_ipython_extension() {
+ console.log(
+ 'This is the current notebook application instance:',
+ Jupyter.notebook
+ );
+ }
+
+ return {
+ load_ipython_extension: load_ipython_extension
+ };
+ });
+
+
+Modifying key bindings
+----------------------
+
+One of the abilities of extensions is to modify key bindings, although once
+again this is an API which is not guaranteed to be stable. However, custom key
+bindings are frequently requested, and are helpful to increase accessibility,
+so in the following we show how to access them.
+
+Here is an example of an extension that will unbind the shortcut ``0,0`` in
+command mode, which normally restarts the kernel, and bind ``0,0,0`` in its
+place:
+
+.. code:: javascript
+
+ // file my_extension/main.js
+
+ define([
+ 'base/js/namespace'
+ ], function(
+ Jupyter
+ ) {
+
+ function load_ipython_extension() {
+ Jupyter.keyboard_manager.command_shortcuts.remove_shortcut('0,0');
+ Jupyter.keyboard_manager.command_shortcuts.add_shortcut('0,0,0', 'jupyter-notebook:restart-kernel');
+ }
+
+ return {
+ load_ipython_extension: load_ipython_extension
+ };
+ });
+
+.. note::
+
+ The standard keybindings might not work correctly on non-US keyboards.
+ Unfortunately, this is a limitation of browser implementations and the
+ status of keyboard event handling on the web in general. We appreciate your
+ feedback if you have issues binding keys, or have any ideas to help improve
+ the situation.
+
+You can see that I have used the **action name**
+``jupyter-notebook:restart-kernel`` to bind the new shortcut. There is no API
+yet to access the list of all available *actions*, though the following in the
+JavaScript console of your browser on a notebook page should give you an idea
+of what is available:
+
+.. code:: javascript
+
+ Object.keys(require('base/js/namespace').actions._actions);
+
+In this example, we changed a keyboard shortcut in **command mode**; you
+can also customize keyboard shortcuts in **edit mode**.
+However, most of the keyboard shortcuts in edit mode are handled by CodeMirror,
+which supports custom key bindings via a completely different API.
+
+
+Defining and registering your own actions
+-----------------------------------------
+
+As part of your front-end extension, you may wish to define actions, which can
+be attached to toolbar buttons, or called from the command palette. Here is an
+example of an extension that defines a (not very useful!) action to show an
+alert, and adds a toolabr button using the full action name:
+
+.. code:: javascript
+
+ // file my_extension/main.js
+
+ define([
+ 'base/js/namespace'
+ ], function(
+ Jupyter
+ ) {
+ function load_ipython_extension() {
+
+ var handler = function () {
+ alert('this is an alert from my_extension!');
+ };
+
+ var action = {
+ icon: 'fa-comment-o', // a font-awesome class used on buttons, etc
+ help : 'Show an alert',
+ help_index : 'zz',
+ handler : handler
+ };
+ var prefix = 'my_extension';
+ var action_name = 'show-alert';
+
+ var full_action_name = Jupyter.actions.register(action, name, prefix); // returns 'my_extension:show-alert'
+ Jupyter.toolbar.add_buttons_group([full_action_name]);
+ }
+
+ return {
+ load_ipython_extension: load_ipython_extension
+ };
+ });
+
+Every action needs a name, which, when joined with its prefix to make the full
+action name, should be unique. Built-in actions, like the
+``jupyter-notebook:restart-kernel`` we bound in the earlier
+`Modifying key bindings`_ example, use the prefix ``jupyter-notebook``. For
+actions defined in an extension, it makes sense to use the extension name as
+the prefix. For the action name, the following guidelines should be considered:
+
+.. adapted from notebook/static/notebook/js/actions.js
+
+* First pick a noun and a verb for the action. For example, if the action is
+ "restart kernel," the verb is "restart" and the noun is "kernel".
+* Omit terms like "selected" and "active" by default, so "delete-cell", rather
+ than "delete-selected-cell". Only provide a scope like "-all-" if it is other
+ than the default "selected" or "active" scope.
+* If an action has a secondary action, separate the secondary action with
+ "-and-", so "restart-kernel-and-clear-output".
+* Use above/below or previous/next to indicate spatial and sequential
+ relationships.
+* Don't ever use before/after as they have a temporal connotation that is
+ confusing when used in a spatial context.
+* For dialogs, use a verb that indicates what the dialog will accomplish, such
+ as "confirm-restart-kernel".
+
+
+Installing and enabling extensions
+----------------------------------
+
+You can install your nbextension with the command::
+
+ jupyter nbextension install path/to/my_extension/ [--user|--sys-prefix]
+
+The default installation is system-wide. You can use ``--user`` to do a per-user installation,
+or ``--sys-prefix`` to install to Python's prefix (e.g. in a virtual or conda environment).
+Where my_extension is the directory containing the Javascript files.
+This will copy it to a Jupyter data directory (the exact location is platform
+dependent - see :ref:`jupyter_path`).
+
+For development, you can use the ``--symlink`` flag to symlink your extension
+rather than copying it, so there's no need to reinstall after changes.
+
+To use your extension, you'll also need to **enable** it, which tells the
+notebook interface to load it. You can do that with another command::
+
+ jupyter nbextension enable my_extension/main [--sys-prefix]
+
+The argument refers to the Javascript module containing your
+``load_ipython_extension`` function, which is ``my_extension/main.js`` in this
+example. There is a corresponding ``disable`` command to stop using an
+extension without uninstalling it.
+
+.. versionchanged:: 4.2
+
+ Added ``--sys-prefix`` argument
diff --git a/docs/source/extending/handlers.rst b/docs/source/extending/handlers.rst
new file mode 100644
index 0000000..cb29247
--- /dev/null
+++ b/docs/source/extending/handlers.rst
@@ -0,0 +1,127 @@
+Custom request handlers
+=======================
+
+The notebook webserver can be interacted with using a well `defined
+RESTful
+API <http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyter-js-services/master/rest_api.yaml>`__.
+You can define custom RESTful API handlers in addition to the ones
+provided by the notebook. As described below, to define a custom handler
+you need to first write a notebook server extension. Then, in the
+extension, you can register the custom handler.
+
+Writing a notebook server extension
+-----------------------------------
+
+The notebook webserver is written in Python, hence your server extension
+should be written in Python too. Server extensions, like IPython
+extensions, are Python modules that define a specially named load
+function, ``load_jupyter_server_extension``. This function is called
+when the extension is loaded.
+
+.. code:: python
+
+ def load_jupyter_server_extension(nb_server_app):
+ """
+ Called when the extension is loaded.
+
+ Args:
+ nb_server_app (NotebookWebApplication): handle to the Notebook webserver instance.
+ """
+ pass
+
+To get the notebook server to load your custom extension, you'll need to
+add it to the list of extensions to be loaded. You can do this using the
+config system. ``NotebookApp.server_extensions`` is a config variable
+which is an array of strings, each a Python module to be imported.
+Because this variable is notebook config, you can set it two different
+ways, using config files or via the command line.
+
+For example, to get your extension to load via the command line add a
+double dash before the variable name, and put the Python array in
+double quotes. If your package is "mypackage" and module is
+"mymodule", this would look like
+``jupyter notebook --NotebookApp.server_extensions="['mypackage.mymodule']"``
+.
+Basically the string should be Python importable.
+
+Alternatively, you can have your extension loaded regardless of the
+command line args by setting the variable in the Jupyter config file.
+The default location of the Jupyter config file is
+``~/.jupyter/profile_default/jupyter_notebook_config.py``. Then, inside
+the config file, you can use Python to set the variable. For example,
+the following config does the same as the previous command line example
+[1].
+
+.. code:: python
+
+ c = get_config()
+ c.NotebookApp.server_extensions = [
+ 'mypackage.mymodule'
+ ]
+
+Before continuing, it's a good idea to verify that your extension is
+being loaded. Use a print statement to print something unique. Launch
+the notebook server and you should see your statement printed to the
+console.
+
+Registering custom handlers
+---------------------------
+
+Once you've defined a server extension, you can register custom handlers
+because you have a handle to the Notebook server app instance
+(``nb_server_app`` above). However, you first need to define your custom
+handler. To declare a custom handler, inherit from
+``notebook.base.handlers.IPythonHandler``. The example below[1] is a
+Hello World handler:
+
+.. code:: python
+
+ from notebook.base.handlers import IPythonHandler
+
+ class HelloWorldHandler(IPythonHandler):
+ def get(self):
+ self.finish('Hello, world!')
+
+The Jupyter Notebook server use
+`Tornado <http://www.tornadoweb.org/en/stable/>`__ as its web framework.
+For more information on how to implement request handlers, refer to the
+`Tornado documentation on the
+matter <http://www.tornadoweb.org/en/stable/web.html#request-handlers>`__.
+
+After defining the handler, you need to register the handler with the
+Notebook server. See the following example:
+
+.. code:: python
+
+ web_app = nb_server_app.web_app
+ host_pattern = '.*$'
+ route_pattern = url_path_join(web_app.settings['base_url'], '/hello')
+ web_app.add_handlers(host_pattern, [(route_pattern, HelloWorldHandler)])
+
+Putting this together with the extension code, the example looks like the
+following:
+
+.. code:: python
+
+ from notebook.utils import url_path_join
+ from notebook.base.handlers import IPythonHandler
+
+ class HelloWorldHandler(IPythonHandler):
+ def get(self):
+ self.finish('Hello, world!')
+
+ def load_jupyter_server_extension(nb_server_app):
+ """
+ Called when the extension is loaded.
+
+ Args:
+ nb_server_app (NotebookWebApplication): handle to the Notebook webserver instance.
+ """
+ web_app = nb_server_app.web_app
+ host_pattern = '.*$'
+ route_pattern = url_path_join(web_app.settings['base_url'], '/hello')
+ web_app.add_handlers(host_pattern, [(route_pattern, HelloWorldHandler)])
+
+References:
+1. `Peter Parente's
+Mindtrove <http://mindtrove.info/#nb-server-exts>`__
diff --git a/docs/source/extending/index.rst b/docs/source/extending/index.rst
new file mode 100644
index 0000000..d9805c4
--- /dev/null
+++ b/docs/source/extending/index.rst
@@ -0,0 +1,15 @@
+======================
+Extending the Notebook
+======================
+
+Certain subsystems of the notebook server are designed to be extended or
+overridden by users. These documents explain these systems, and show how to
+override the notebook's defaults with your own custom behavior.
+
+.. toctree::
+ :maxdepth: 2
+
+ contents
+ savehooks
+ handlers
+ frontend_extensions
diff --git a/docs/source/extending/savehooks.rst b/docs/source/extending/savehooks.rst
new file mode 100644
index 0000000..c07cdaf
--- /dev/null
+++ b/docs/source/extending/savehooks.rst
@@ -0,0 +1,77 @@
+File save hooks
+===============
+
+You can configure functions that are run whenever a file is saved. There are
+two hooks available:
+
+* ``ContentsManager.pre_save_hook`` runs on the API path and model with content.
+ This can be used for things like stripping output that people don't like
+ adding to VCS noise.
+* ``FileContentsManager.post_save_hook`` runs on the filesystem path and model
+ without content. This could be used to commit changes after every save, for
+ instance.
+
+They are both called with keyword arguments::
+
+ pre_save_hook(model=model, path=path, contents_manager=cm)
+ post_save_hook(model=model, os_path=os_path, contents_manager=cm)
+
+Examples
+--------
+
+These can both be added to :file:`jupyter_notebook_config.py`.
+
+A pre-save hook for stripping output::
+
+ def scrub_output_pre_save(model, **kwargs):
+ """scrub output before saving notebooks"""
+ # only run on notebooks
+ if model['type'] != 'notebook':
+ return
+ # only run on nbformat v4
+ if model['content']['nbformat'] != 4:
+ return
+
+ for cell in model['content']['cells']:
+ if cell['cell_type'] != 'code':
+ continue
+ cell['outputs'] = []
+ cell['execution_count'] = None
+
+ c.FileContentsManager.pre_save_hook = scrub_output_pre_save
+
+A post-save hook to make a script equivalent whenever the notebook is saved
+(replacing the ``--script`` option in older versions of the notebook)::
+
+ import io
+ import os
+ from notebook.utils import to_api_path
+
+ _script_exporter = None
+
+ def script_post_save(model, os_path, contents_manager, **kwargs):
+ """convert notebooks to Python script after save with nbconvert
+
+ replaces `ipython notebook --script`
+ """
+ from nbconvert.exporters.script import ScriptExporter
+
+ if model['type'] != 'notebook':
+ return
+
+ global _script_exporter
+ if _script_exporter is None:
+ _script_exporter = ScriptExporter(parent=contents_manager)
+ log = contents_manager.log
+
+ base, ext = os.path.splitext(os_path)
+ py_fname = base + '.py'
+ script, resources = _script_exporter.from_filename(os_path)
+ script_fname = base + resources.get('output_extension', '.txt')
+ log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
+ with io.open(script_fname, 'w', encoding='utf-8') as f:
+ f.write(script)
+ c.FileContentsManager.post_save_hook = script_post_save
+
+This could be a simple call to ``jupyter nbconvert --to script``, but spawning
+the subprocess every time is quite slow.
diff --git a/docs/source/frontend_config.rst b/docs/source/frontend_config.rst
new file mode 100644
index 0000000..6c4280a
--- /dev/null
+++ b/docs/source/frontend_config.rst
@@ -0,0 +1,79 @@
+.. _frontend_config:
+
+Configuring the notebook frontend
+=================================
+
+.. note::
+
+ The ability to configure the notebook frontend UI and preferences is
+ still a work in progress.
+
+This document is a rough explanation on how you can persist some configuration
+options for the notebook JavaScript.
+
+There is no exhaustive list of all the configuration options as most options
+are passed down to other libraries, which means that non valid
+configuration can be ignored without any error messages.
+
+
+How front end configuration works
+---------------------------------
+The frontend configuration system works as follows:
+
+ - get a handle of a configurable JavaScript object.
+ - access its configuration attribute.
+ - update its configuration attribute with a JSON patch.
+
+
+Example - Changing the notebook's default indentation
+-----------------------------------------------------
+This example explains how to change the default setting ``indentUnit``
+for CodeMirror Code Cells::
+
+ var cell = Jupyter.notebook.get_selected_cell();
+ var config = cell.config;
+ var patch = {
+ CodeCell:{
+ cm_config:{indentUnit:2}
+ }
+ }
+ config.update(patch)
+
+You can enter the previous snippet in your browser's JavaScript console once.
+Then reload the notebook page in your browser. Now, the preferred indent unit
+should be equal to two spaces. The custom setting persists and you do not need
+to reissue the patch on new notebooks.
+
+``indentUnit``, used in this example, is one of the many `CodeMirror options
+<https://codemirror.net/doc/manual.html#option_indentUnit>`_ which are available
+for configuration.
+
+
+Example - Restoring the notebook's default indentation
+------------------------------------------------------
+If you want to restore a notebook frontend preference to its default value,
+you will enter a JSON patch with a ``null`` value for the preference setting.
+
+For example, let's restore the indent setting ``indentUnit`` to its default of
+four spaces. Enter the following code snippet in your JavaScript console::
+
+ var cell = Jupyter.notebook.get_selected_cell();
+ var config = cell.config;
+ var patch = {
+ CodeCell:{
+ cm_config:{indentUnit: null} # only change here.
+ }
+ }
+ config.update(patch)
+
+Reload the notebook in your browser and the default indent should again be two
+spaces.
+
+Persisting configuration settings
+---------------------------------
+Under the hood, Jupyter will persist the preferred configuration settings in
+``~/.jupyter/nbconfig/<section>.json``, with ``<section>``
+taking various value depending on the page where the configuration is issued.
+``<section>`` can take various values like ``notebook``, ``tree``, and
+``editor``. A ``common`` section contains configuration settings shared by all
+pages.
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 0000000..0de6411
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,52 @@
+====================
+The Jupyter notebook
+====================
+
+.. toctree::
+ :maxdepth: 1
+ :caption: User Documentation
+
+ notebook
+ Installation <https://jupyter.readthedocs.org/en/latest/install.html>
+ Running the Notebook <https://jupyter.readthedocs.org/en/latest/running.html>
+ Migrating from IPython <https://jupyter.readthedocs.org/en/latest/migrating.html>
+ ui_components
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Configuration
+
+ config
+ public_server
+ security
+ frontend_config
+ extending/index
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Developer Documentation
+
+ development_js
+ development_faq
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Community documentation
+
+ examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages
+ examples/Notebook/rstversions/Examples and Tutorials Index
+
+.. toctree::
+ :maxdepth: 2
+ :caption: About Jupyter Notebook
+
+ changelog
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Questions? Suggestions?
+
+ Jupyter mailing list <https://groups.google.com/forum/#!forum/jupyter>
+ Jupyter website <https://jupyter.org>
+ Stack Overflow - Jupyter <https://stackoverflow.com/questions/tagged/jupyter>
+ Stack Overflow - Jupyter-notebook <https://stackoverflow.com/questions/tagged/jupyter-notebook>
diff --git a/docs/source/ipython_security.asc b/docs/source/ipython_security.asc
new file mode 100644
index 0000000..9543681
--- /dev/null
+++ b/docs/source/ipython_security.asc
@@ -0,0 +1,52 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v2.0.22 (GNU/Linux)
+
+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
+5vmLc+Ibz+eT+rM=
+=XVDe
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/docs/source/links.txt b/docs/source/links.txt
new file mode 100644
index 0000000..34192d7
--- /dev/null
+++ b/docs/source/links.txt
@@ -0,0 +1,39 @@
+.. This (-*- rst -*-) format file contains commonly used link targets
+ and name substitutions. It may be included in many files,
+ therefore it should only contain link targets and name
+ substitutions. Try grepping for "^\.\. _" to find plausible
+ candidates for this list.
+
+ NOTE: this file must have an extension *opposite* to that of the main reST
+ files in the manuals, so that we can include it with ".. include::"
+ directives, but without triggering warnings from Sphinx for not being listed
+ in any toctree. Since IPython uses .txt for the main files, this one will
+ use .rst.
+
+ NOTE: reST targets are
+ __not_case_sensitive__, so only one target definition is needed for
+ ipython, IPython, etc.
+
+ NOTE: Some of these were taken from the nipy links compendium.
+
+.. Main Jupyter notebook links
+
+.. _Notebook Basics: notebook_p2_
+.. _notebook_p2: https://nbviewer.jupyter.org/urls/raw.github.com/ipython/ipython/3.x/examples/Notebook/Notebook%20Basics.ipynb
+
+.. _Running Code in the Jupyter Notebook: notebook_p1_
+.. _notebook_p1: https://nbviewer.jupyter.org/urls/raw.github.com/ipython/ipython/3.x/examples/Notebook/Running%20Code.ipynb
+
+.. Other python projects
+.. _matplotlib: http://matplotlib.org
+.. _nbviewer: http://nbviewer.jupyter.org
+.. _nbconvert: http://nbconvert.readthedocs.org/en/latest/
+
+.. Other tools and projects
+.. _Markdown: http://daringfireball.net/projects/markdown/syntax
+
+.. _Rich Output: notebook_p5_
+.. _notebook_p5: https://nbviewer.jupyter.org/urls/raw.github.com/ipython/ipython/3.x/examples/IPython%20Kernel/Rich%20Output.ipynb
+
+.. _Plotting with Matplotlib: notebook_p3_
+.. _notebook_p3: https://nbviewer.jupyter.org/urls/raw.github.com/ipython/ipython/3.x/examples/IPython%20Kernel/Plotting%20in%20the%20Notebook.ipynb
diff --git a/docs/source/notebook.rst b/docs/source/notebook.rst
new file mode 100644
index 0000000..7159907
--- /dev/null
+++ b/docs/source/notebook.rst
@@ -0,0 +1,435 @@
+.. _htmlnotebook:
+
+The Jupyter Notebook
+====================
+
+Introduction
+------------
+
+The notebook extends the console-based approach to interactive computing in
+a qualitatively new direction, providing a web-based application suitable for
+capturing the whole computation process: developing, documenting, and
+executing code, as well as communicating the results. The Jupyter notebook
+combines two components:
+
+**A web application**: a browser-based tool for interactive authoring of
+documents which combine explanatory text, mathematics, computations and their
+rich media output.
+
+**Notebook documents**: a representation of all content visible in the web
+application, including inputs and outputs of the computations, explanatory
+text, mathematics, images, and rich media representations of objects.
+
+.. seealso::
+
+ See the :ref:`installation guide <jupyter:install>` on how to install the notebook and its dependencies.
+
+
+Main features of the web application
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* In-browser editing for code, with automatic syntax highlighting,
+ indentation, and tab completion/introspection.
+
+* The ability to execute code from the browser, with the results of
+ computations attached to the code which generated them.
+
+* Displaying the result of computation using rich media representations, such
+ as HTML, LaTeX, PNG, SVG, etc. For example, publication-quality figures
+ rendered by the matplotlib_ library, can be included inline.
+
+* In-browser editing for rich text using the Markdown_ markup language, which
+ can provide commentary for the code, is not limited to plain text.
+
+* The ability to easily include mathematical notation within markdown cells
+ using LaTeX, and rendered natively by MathJax_.
+
+
+
+.. _MathJax: http://www.mathjax.org/
+
+
+Notebook documents
+~~~~~~~~~~~~~~~~~~
+Notebook documents contains the inputs and outputs of a interactive session as
+well as additional text that accompanies the code but is not meant for
+execution. In this way, notebook files can serve as a complete computational
+record of a session, interleaving executable code with explanatory text,
+mathematics, and rich representations of resulting objects. These documents
+are internally JSON_ files and are saved with the ``.ipynb`` extension. Since
+JSON is a plain text format, they can be version-controlled and shared with
+colleagues.
+
+.. _JSON: http://en.wikipedia.org/wiki/JSON
+
+Notebooks may be exported to a range of static formats, including HTML (for
+example, for blog posts), reStructuredText, LaTeX, PDF, and slide shows, via
+the nbconvert_ command.
+
+Furthermore, any ``.ipynb`` notebook document available from a public
+URL can be shared via the `Jupyter Notebook Viewer <nbviewer>`_ (nbviewer_).
+This service loads the notebook document from the URL and renders it as a
+static web page. The results may thus be shared with a colleague, or as a
+public blog post, without other users needing to install the Jupyter notebook
+themselves. In effect, nbviewer_ is simply nbconvert_ as
+a web service, so you can do your own static conversions with nbconvert,
+without relying on nbviewer.
+
+
+
+.. seealso::
+
+ :ref:`Details on the notebook JSON file format <nbformat:notebook_file_format>`
+
+
+Starting the notebook server
+----------------------------
+
+You can start running a notebook server from the command line using the
+following command::
+
+ jupyter notebook
+
+This will print some information about the notebook server in your console,
+and open a web browser to the URL of the web application (by default,
+``http://127.0.0.1:8888``).
+
+The landing page of the Jupyter notebook web application, the **dashboard**,
+shows the notebooks currently available in the notebook directory (by default,
+the directory from which the notebook server was started).
+
+You can create new notebooks from the dashboard with the ``New Notebook``
+button, or open existing ones by clicking on their name. You can also drag
+and drop ``.ipynb`` notebooks and standard ``.py`` Python source code files
+into the notebook list area.
+
+When starting a notebook server from the command line, you can also open a
+particular notebook directly, bypassing the dashboard, with ``jupyter notebook
+my_notebook.ipynb``. The ``.ipynb`` extension is assumed if no extension is
+given.
+
+When you are inside an open notebook, the `File | Open...` menu option will
+open the dashboard in a new browser tab, to allow you to open another notebook
+from the notebook directory or to create a new notebook.
+
+
+.. note::
+
+ You can start more than one notebook server at the same time, if you want
+ to work on notebooks in different directories. By default the first
+ notebook server starts on port 8888, and later notebook servers search for
+ ports near that one. You can also manually specify the port with the
+ ``--port`` option.
+
+Creating a new notebook document
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A new notebook may be created at any time, either from the dashboard, or using
+the `File | New` menu option from within an active notebook. The new notebook
+is created within the same directory and will open in a new browser tab. It
+will also be reflected as a new entry in the notebook list on the dashboard.
+
+
+Opening notebooks
+~~~~~~~~~~~~~~~~~
+An open notebook has **exactly one** interactive session connected to an
+:ref:`IPython kernel <ipythonzmq>`, which will execute code sent by the user
+and communicate back results. This kernel remains active if the web browser
+window is closed, and reopening the same notebook from the dashboard will
+reconnect the web application to the same kernel. In the dashboard, notebooks
+with an active kernel have a ``Shutdown`` button next to them, whereas
+notebooks without an active kernel have a ``Delete`` button in its place.
+
+Other clients may connect to the same underlying IPython kernel.
+The notebook server always prints to the terminal the full details of
+how to connect to each kernel, with messages such as the following::
+
+ [NotebookApp] Kernel started: 87f7d2c0-13e3-43df-8bb8-1bd37aaf3373
+
+This long string is the kernel's ID which is sufficient for getting the
+information necessary to connect to the kernel. You can also request this
+connection data by running the ``%connect_info`` :ref:`magic
+<magics_explained>`. This will print the same ID information as well as the
+content of the JSON data structure it contains.
+
+You can then, for example, manually start a Qt console connected to the *same*
+kernel from the command line, by passing a portion of the ID::
+
+ $ ipython qtconsole --existing 87f7d2c0
+
+Without an ID, ``--existing`` will connect to the most recently
+started kernel. This can also be done by running the ``%qtconsole``
+:ref:`magic <magics_explained>` in the notebook.
+
+.. seealso::
+
+ :ref:`ipythonzmq`
+
+Notebook user interface
+-----------------------
+
+When you create a new notebook document, you will be presented with the
+**notebook name**, a **menu bar**, a **toolbar** and an empty **code
+cell**.
+
+**notebook name**: The name of the notebook document is displayed at the top
+of the page, next to the ``IP[y]: Notebook`` logo. This name reflects the name
+of the ``.ipynb`` notebook document file. Clicking on the notebook name
+brings up a dialog which allows you to rename it. Thus, renaming a notebook
+from "Untitled0" to "My first notebook" in the browser, renames the
+``Untitled0.ipynb`` file to ``My first notebook.ipynb``.
+
+**menu bar**: The menu bar presents different options that may be used to
+manipulate the way the notebook functions.
+
+**toolbar**: The tool bar gives a quick way of performing the most-used
+operations within the notebook, by clicking on an icon.
+
+**code cell**: the default type of cell, read on for an explanation of cells
+
+.. note::
+
+ As of notebook version 4.1, the user interface allows for multiple cells to
+ be selected. The ``quick celltype selector``, found in the menubar, will
+ display a dash ``-`` when multiple cells are selected to indicate that the
+ type of the cells in the selection might not be unique. The quick selector
+ can still be used to change the type of the selection and will change the
+ type of all the currently selected cells.
+
+
+Structure of a notebook document
+--------------------------------
+
+The notebook consists of a sequence of cells. A cell is a multi-line
+text input field, and its contents can be executed by using
+:kbd:`Shift-Enter`, or by clicking either the "Play" button the toolbar, or
+`Cell | Run` in the menu bar. The execution behavior of a cell is determined
+the cell's type. There are four types of cells: **code cells**, **markdown
+cells**, **raw cells** and **heading cells**. Every cell starts off
+being a **code cell**, but its type can be changed by using a dropdown on the
+toolbar (which will be "Code", initially), or via :ref:`keyboard shortcuts
+<keyboard-shortcuts>`.
+
+For more information on the different things you can do in a notebook,
+see the `collection of examples
+<http://nbviewer.jupyter.org/github/jupyter/notebook/tree/master/docs/source/examples/Notebook/>`_.
+
+Code cells
+~~~~~~~~~~
+A *code cell* allows you to edit and write new code, with full syntax
+highlighting and tab completion. By default, the language associated to a code
+cell is Python, but other languages, such as ``Julia`` and ``R``, can be
+handled using :ref:`cell magic commands <magics_explained>`.
+
+When a code cell is executed, code that it contains is sent to the kernel
+associated with the notebook. The results that are returned from this
+computation are then displayed in the notebook as the cell's *output*. The
+output is not limited to text, with many other possible forms of output are
+also possible, including ``matplotlib`` figures and HTML tables (as used, for
+example, in the ``pandas`` data analysis package). This is known as IPython's
+*rich display* capability.
+
+.. seealso::
+
+ `Rich Output`_ example notebook
+
+Markdown cells
+~~~~~~~~~~~~~~
+You can document the computational process in a literate way, alternating
+descriptive text with code, using *rich text*. In IPython this is accomplished
+by marking up text with the Markdown language. The corresponding cells are
+called *Markdown cells*. The Markdown language provides a simple way to
+perform this text markup, that is, to specify which parts of the text should
+be emphasized (italics), bold, form lists, etc.
+
+
+When a Markdown cell is executed, the Markdown code is converted into
+the corresponding formatted rich text. Markdown allows arbitrary HTML code for
+formatting.
+
+Within Markdown cells, you can also include *mathematics* in a straightforward
+way, using standard LaTeX notation: ``$...$`` for inline mathematics and
+``$$...$$`` for displayed mathematics. When the Markdown cell is executed,
+the LaTeX portions are automatically rendered in the HTML output as equations
+with high quality typography. This is made possible by MathJax_, which
+supports a `large subset <mathjax_tex>`_ of LaTeX functionality
+
+.. _mathjax_tex: http://docs.mathjax.org/en/latest/tex.html
+
+Standard mathematics environments defined by LaTeX and AMS-LaTeX (the
+`amsmath` package) also work, such as
+``\begin{equation}...\end{equation}``, and ``\begin{align}...\end{align}``.
+New LaTeX macros may be defined using standard methods,
+such as ``\newcommand``, by placing them anywhere *between math delimiters* in
+a Markdown cell. These definitions are then available throughout the rest of
+the IPython session.
+
+.. seealso::
+
+ `Markdown Cells`_ example notebook
+
+Raw cells
+~~~~~~~~~
+
+*Raw* cells provide a place in which you can write *output* directly.
+Raw cells are not evaluated by the notebook.
+When passed through nbconvert_, raw cells arrive in the
+destination format unmodified. For example, this allows you to type full LaTeX
+into a raw cell, which will only be rendered by LaTeX after conversion by
+nbconvert.
+
+Heading cells
+~~~~~~~~~~~~~
+
+If you want to provide structure for your document, you can use markdown
+headings. Markdown headings consist of 1 to 6 hash # signs ``#`` followed by a
+space and the title of your section. The markdown heading will be converted
+to a clickable link for a section of the notebook. It is also used as a hint
+when exporting to other document formats, like PDF.
+We recommend using only one markdown header in a cell and limit the cell's
+content to the header text. For flexibility of text format conversion, we
+suggest placing additional text in the next notebook cell.
+
+Basic workflow
+--------------
+
+The normal workflow in a notebook is, then, quite similar to a standard
+IPython session, with the difference that you can edit cells in-place multiple
+times until you obtain the desired results, rather than having to
+rerun separate scripts with the ``%run`` magic command.
+
+
+Typically, you will work on a computational problem in pieces, organizing
+related ideas into cells and moving forward once previous parts work
+correctly. This is much more convenient for interactive exploration than
+breaking up a computation into scripts that must be executed together, as was
+previously necessary, especially if parts of them take a long time to run.
+
+At certain moments, it may be necessary to interrupt a calculation which is
+taking too long to complete. This may be done with the `Kernel | Interrupt`
+menu option, or the :kbd:`Ctrl-m i` keyboard shortcut.
+Similarly, it may be necessary or desirable to restart the whole computational
+process, with the `Kernel | Restart` menu option or :kbd:`Ctrl-m .`
+shortcut.
+
+A notebook may be downloaded in either a ``.ipynb`` or ``.py`` file from the
+menu option `File | Download as`. Choosing the ``.py`` option downloads a
+Python ``.py`` script, in which all rich output has been removed and the
+content of markdown cells have been inserted as comments.
+
+.. seealso::
+
+ `Running Code in the Jupyter Notebook`_ example notebook
+
+ `Notebook Basics`_ example notebook
+
+ :ref:`a warning about doing "roundtrip" conversions <note_about_roundtrip>`.
+
+.. _keyboard-shortcuts:
+
+Keyboard shortcuts
+~~~~~~~~~~~~~~~~~~
+All actions in the notebook can be performed with the mouse, but keyboard
+shortcuts are also available for the most common ones. The essential shortcuts
+to remember are the following:
+
+* :kbd:`Shift-Enter`: run cell
+ Execute the current cell, show output (if any), and jump to the next cell
+ below. If :kbd:`Shift-Enter` is invoked on the last cell, a new code
+ cell will also be created. Note that in the notebook, typing :kbd:`Enter`
+ on its own *never* forces execution, but rather just inserts a new line in
+ the current cell. :kbd:`Shift-Enter` is equivalent to clicking the
+ ``Cell | Run`` menu item.
+
+* :kbd:`Ctrl-Enter`: run cell in-place
+ Execute the current cell as if it were in "terminal mode", where any
+ output is shown, but the cursor *remains* in the current cell. The cell's
+ entire contents are selected after execution, so you can just start typing
+ and only the new input will be in the cell. This is convenient for doing
+ quick experiments in place, or for querying things like filesystem
+ content, without needing to create additional cells that you may not want
+ to be saved in the notebook.
+
+* :kbd:`Alt-Enter`: run cell, insert below
+ Executes the current cell, shows the output, and inserts a *new*
+ cell between the current cell and the cell below (if one exists). This
+ is thus a shortcut for the sequence :kbd:`Shift-Enter`, :kbd:`Ctrl-m a`.
+ (:kbd:`Ctrl-m a` adds a new cell above the current one.)
+
+* :kbd:`Esc` and :kbd:`Enter`: Command mode and edit mode
+ In command mode, you can easily navigate around the notebook using keyboard
+ shortcuts. In edit mode, you can edit text in cells.
+
+For the full list of available shortcuts, click :guilabel:`Help`,
+:guilabel:`Keyboard Shortcuts` in the notebook menus.
+
+Plotting
+--------
+One major feature of the Jupyter notebook is the ability to display plots that
+are the output of running code cells. The IPython kernel is designed to work
+seamlessly with the matplotlib_ plotting library to provide this functionality.
+Specific plotting library integration is a feature of the kernel.
+
+Installing kernels
+------------------
+
+For information on how to install a Python kernel, refer to the `IPython install
+page <http://ipython.org/install.html>`__.
+
+Kernels for other languages can be found in the `IPython wiki
+<https://github.com/ipython/ipython/wiki/IPython%20kernels%20for%20other%20languages>`_.
+They usually come with instruction what to run to make the kernel available in the notebook.
+
+
+.. _signing_notebooks:
+
+Signing Notebooks
+-----------------
+
+To prevent untrusted code from executing on users' behalf when notebooks open,
+we have added a signature to the notebook, stored in metadata.
+The notebook server verifies this signature when a notebook is opened.
+If the signature stored in the notebook metadata does not match,
+javascript and HTML output will not be displayed on load,
+and must be regenerated by re-executing the cells.
+
+Any notebook that you have executed yourself *in its entirety* will be considered trusted,
+and its HTML and javascript output will be displayed on load.
+
+If you need to see HTML or Javascript output without re-executing,
+you can explicitly trust notebooks, such as those shared with you,
+or those that you have written yourself prior to IPython 2.0,
+at the command-line with::
+
+ $ jupyter trust mynotebook.ipynb [other notebooks.ipynb]
+
+This just generates a new signature stored in each notebook.
+
+You can generate a new notebook signing key with::
+
+ $ jupyter trust --reset
+
+.. include:: links.txt
+
+Browser Compatibility
+---------------------
+
+The Jupyter Notebook is officially supported the latest stable version the following browsers:
+
+* Chrome
+* Safari
+* Firefox
+
+The is mainly due to the notebook's usage of WebSockets and the flexible box model.
+
+The following browsers are unsupported:
+
+* Safari < 5
+* Firefox < 6
+* Chrome < 13
+* Opera (any): CSS issues, but execution might work
+* Internet Explorer < 10
+* Internet Explorer ≥ 10 (same as Opera)
+
+Using Safari with HTTPS and an untrusted certificate is known to not work
+(websockets will fail).
diff --git a/docs/source/public_server.rst b/docs/source/public_server.rst
new file mode 100644
index 0000000..d65ba27
--- /dev/null
+++ b/docs/source/public_server.rst
@@ -0,0 +1,231 @@
+.. _working_remotely:
+
+Running a notebook server
+=========================
+
+
+The :doc:`Jupyter notebook <notebook>` web application is based on a
+server-client structure. The notebook server uses a :ref:`two-process kernel
+architecture <ipython:ipythonzmq>` based on ZeroMQ_, as well as Tornado_ for
+serving HTTP requests.
+
+.. note::
+ By default, a notebook server runs locally at 127.0.0.1:8888
+ and is accessible only from `localhost`. You may access the
+ notebook server from the browser using `http://127.0.0.1:8888`.
+
+This document describes how you can
+:ref:`secure a notebook server <notebook_server_security>` and how to
+:ref:`run it on a public interface <notebook_public_server>`.
+
+.. _ZeroMQ: http://zeromq.org
+
+.. _Tornado: http://www.tornadoweb.org
+
+
+.. _notebook_server_security:
+
+Securing a notebook server
+--------------------------
+
+You can protect your notebook server with a simple single password by
+configuring the :attr:`NotebookApp.password` setting in
+:file:`jupyter_notebook_config.py`.
+
+Prerequisite: A notebook configuration file
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Check to see if you have a notebook configuration file,
+:file:`jupyter_notebook_config.py`. The default location for this file
+is your Jupyter folder in your home directory, ``~/.jupyter``.
+
+If you don't already have one, create a config file for the notebook
+using the following command::
+
+ $ jupyter notebook --generate-config
+
+
+Preparing a hashed password
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+You can prepare a hashed password using the function
+:func:`notebook.auth.security.passwd`:
+
+.. sourcecode:: ipython
+
+ In [1]: from notebook.auth import passwd
+ In [2]: passwd()
+ Enter password:
+ Verify password:
+ Out[2]: 'sha1:67c9e60bb8b6:9ffede0825894254b2e042ea597d771089e11aed'
+
+.. caution::
+
+ :func:`~notebook.auth.security.passwd` when called with no arguments
+ will prompt you to enter and verify your password such as
+ in the above code snippet. Although the function can also
+ be passed a string as an argument such as ``passwd('mypassword')``, please
+ **do not** pass a string as an argument inside an IPython session, as it
+ will be saved in your input history.
+
+Adding hashed password to your notebook configuration file
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+You can then add the hashed password to your :file:`jupyter_notebook_config.py`.
+The default location for this file :file:`jupyter_notebook_config.py` is in
+your Jupyter folder in your home directory, ``~/.jupyter``, e.g.::
+
+ c.NotebookApp.password = u'sha1:67c9e60bb8b6:9ffede0825894254b2e042ea597d771089e11aed'
+
+Using SSL for encrypted communication
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+When using a password, it is a good idea to also use SSL with a web certificate,
+so that your hashed password is not sent unencrypted by your browser.
+
+.. important::
+ Web security is rapidly changing and evolving. We provide this document
+ as a convenience to the user, and recommend that the user keep current on
+ changes that may impact security, such as new releases of OpenSSL.
+ The Open Web Application Security Project (`OWASP`_) website is a good resource
+ on general security issues and web practices.
+
+You can start the notebook to communicate via a secure protocol mode by setting
+the ``certfile`` option to your self-signed certificate, i.e. ``mycert.pem``,
+with the command::
+
+ $ jupyter notebook --certfile=mycert.pem --keyfile mykey.key
+
+.. tip::
+
+ A self-signed certificate can be generated with ``openssl``. For example,
+ the following command will create a certificate valid for 365 days with
+ both the key and certificate data written to the same file::
+
+ $ openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mykey.key -out mycert.pem
+
+When starting the notebook server, your browser may warn that your self-signed
+certificate is insecure or unrecognized. If you wish to have a fully
+compliant self-signed certificate that will not raise warnings, it is possible
+(but rather involved) to create one, as explained in detail in `this tutorial`__.
+
+.. __: http://arstechnica.com/security/news/2009/12/how-to-get-set-with-a-secure-sertificate-for-free.ars
+
+.. TODO: Find an additional resource that walks the user through this two-process step by step.
+
+.. _OWASP: https://www.owasp.org
+
+
+.. _notebook_public_server:
+
+Running a public notebook server
+--------------------------------
+
+If you want to access your notebook server remotely via a web browser,
+you can do so by running a public notebook server. For optimal security
+when running a public notebook server, you should first secure the
+server with a password and SSL/HTTPS as described in
+:ref:`notebook_server_security`.
+
+Start by creating a certificate file and a hashed password, as explained in
+:ref:`notebook_server_security`.
+
+If you don't already have one, create a
+config file for the notebook using the following command line::
+
+ $ jupyter notebook --generate-config
+
+In the ``~/.jupyter`` directory, edit the notebook config file,
+``jupyter_notebook_config.py``. By default, the notebook config file has
+all fields commented out. The minimum set of configuration options that
+you should to uncomment and edit in :file:``jupyter_notebook_config.py`` is the
+following::
+
+ # Set options for certfile, ip, password, and toggle off browser auto-opening
+ c.NotebookApp.certfile = u'/absolute/path/to/your/certificate/mycert.pem'
+ c.NotebookApp.keyfile = u'/absolute/path/to/your/certificate/mykey.key'
+ # Set ip to '*' to bind on all interfaces (ips) for the public server
+ c.NotebookApp.ip = '*'
+ c.NotebookApp.password = u'sha1:bcd259ccf...<your hashed password here>'
+ c.NotebookApp.open_browser = False
+
+ # It is a good idea to set a known, fixed port for server access
+ c.NotebookApp.port = 9999
+
+You can then start the notebook using the ``jupyter notebook`` command.
+
+.. important::
+
+ **Use 'https'.**
+ Keep in mind that when you enable SSL support, you must access the
+ notebook server over ``https://``, not over plain ``http://``. The startup
+ message from the server prints a reminder in the console, but *it is easy
+ to overlook this detail and think the server is for some reason
+ non-responsive*.
+
+ **When using SSL, always access the notebook server with 'https://'.**
+
+You may now access the public server by pointing your browser to
+``https://your.host.com:9999`` where ``your.host.com`` is your public server's
+domain.
+
+Firewall Setup
+~~~~~~~~~~~~~~
+
+To function correctly, the firewall on the computer running the jupyter
+notebook server must be configured to allow connections from client
+machines on the access port ``c.NotebookApp.port`` set in
+:file:``jupyter_notebook_config.py`` port to allow connections to the
+web interface. The firewall must also allow connections from
+127.0.0.1 (localhost) on ports from 49152 to 65535.
+These ports are used by the server to communicate with the notebook kernels.
+The kernel communication ports are chosen randomly by ZeroMQ, and may require
+multiple connections per kernel, so a large range of ports must be accessible.
+
+Running the notebook with a customized URL prefix
+-------------------------------------------------
+
+The notebook dashboard, which is the landing page with an overview
+of the notebooks in your working directory, is typically found and accessed
+at the default URL ``http://localhost:8888/``.
+
+If you prefer to customize the URL prefix for the notebook dashboard, you can
+do so through modifying ``jupyter_notebook_config.py``. For example, if you
+prefer that the notebook dashboard be located with a sub-directory that
+contains other ipython files, e.g. ``http://localhost:8888/ipython/``,
+you can do so with configuration options like the following (see above for
+instructions about modifying ``jupyter_notebook_config.py``)::
+
+ c.NotebookApp.base_url = '/ipython/'
+ c.NotebookApp.webapp_settings = {'static_url_prefix':'/ipython/static/'}
+
+Known issues
+------------
+
+Proxies
+~~~~~~~
+
+When behind a proxy, especially if your system or browser is set to autodetect
+the proxy, the notebook web application might fail to connect to the server's
+websockets, and present you with a warning at startup. In this case, you need
+to configure your system not to use the proxy for the server's address.
+
+For example, in Firefox, go to the Preferences panel, Advanced section,
+Network tab, click 'Settings...', and add the address of the notebook server
+to the 'No proxy for' field.
+
+Docker CMD
+~~~~~~~~~~
+
+Using ``jupyter notebook`` as a
+`Docker CMD <https://docs.docker.com/reference/builder/#cmd>`_ results in
+kernels repeatedly crashing, likely due to a lack of `PID reaping
+<https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/>`_.
+To avoid this, use the `tini <https://github.com/krallin/tini>`_ ``init`` as your
+Dockerfile `ENTRYPOINT`::
+
+ # Add Tini. Tini operates as a process subreaper for jupyter. This prevents
+ # kernel crashes.
+ ENV TINI_VERSION v0.6.0
+ ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/bin/tini
+ RUN chmod +x /usr/bin/tini
+ ENTRYPOINT ["/usr/bin/tini", "--"]
+
+ EXPOSE 8888
+ CMD ["jupyter", "notebook", "--port=8888", "--no-browser", "--ip=0.0.0.0"]
diff --git a/docs/source/security.rst b/docs/source/security.rst
new file mode 100644
index 0000000..c23bfb8
--- /dev/null
+++ b/docs/source/security.rst
@@ -0,0 +1,154 @@
+.. _notebook_security:
+
+Security in Jupyter notebooks
+=============================
+
+As Jupyter notebooks become more popular for sharing and collaboration,
+the potential for malicious people to attempt to exploit the notebook
+for their nefarious purposes increases. IPython 2.0 introduces a
+security model to prevent execution of untrusted code without explicit
+user input.
+
+The problem
+-----------
+
+The whole point of Jupyter is arbitrary code execution. We have no
+desire to limit what can be done with a notebook, which would negatively
+impact its utility.
+
+Unlike other programs, a Jupyter notebook document includes output.
+Unlike other documents, that output exists in a context that can execute
+code (via Javascript).
+
+The security problem we need to solve is that no code should execute
+just because a user has **opened** a notebook that **they did not
+write**. Like any other program, once a user decides to execute code in
+a notebook, it is considered trusted, and should be allowed to do
+anything.
+
+Our security model
+------------------
+
+- Untrusted HTML is always sanitized
+- Untrusted Javascript is never executed
+- HTML and Javascript in Markdown cells are never trusted
+- **Outputs** generated by the user are trusted
+- Any other HTML or Javascript (in Markdown cells, output generated by
+ others) is never trusted
+- The central question of trust is "Did the current user do this?"
+
+The details of trust
+--------------------
+
+Jupyter notebooks store a signature in metadata, which is used to answer
+the question "Did the current user do this?"
+
+This signature is a digest of the notebooks contents plus a secret key,
+known only to the user. The secret key is a user-only readable file in
+the Jupyter profile's security directory. By default, this is::
+
+ ~/.jupyter/profile_default/security/notebook_secret
+
+.. note::
+
+ The notebook secret being stored in the profile means that
+ loading a notebook in another profile results in it being untrusted,
+ unless you copy or symlink the notebook secret to share it across profiles.
+
+When a notebook is opened by a user, the server computes a signature
+with the user's key, and compares it with the signature stored in the
+notebook's metadata. If the signature matches, HTML and Javascript
+output in the notebook will be trusted at load, otherwise it will be
+untrusted.
+
+Any output generated during an interactive session is trusted.
+
+Updating trust
+**************
+
+A notebook's trust is updated when the notebook is saved. If there are
+any untrusted outputs still in the notebook, the notebook will not be
+trusted, and no signature will be stored. If all untrusted outputs have
+been removed (either via ``Clear Output`` or re-execution), then the
+notebook will become trusted.
+
+While trust is updated per output, this is only for the duration of a
+single session. A notebook file on disk is either trusted or not in its
+entirety.
+
+Explicit trust
+**************
+
+Sometimes re-executing a notebook to generate trusted output is not an
+option, either because dependencies are unavailable, or it would take a
+long time. Users can explicitly trust a notebook in two ways:
+
+- At the command-line, with::
+
+ jupyter trust /path/to/notebook.ipynb
+
+- After loading the untrusted notebook, with ``File / Trust Notebook``
+
+These two methods simply load the notebook, compute a new signature with
+the user's key, and then store the newly signed notebook.
+
+Reporting security issues
+-------------------------
+
+If you find a security vulnerability in Jupyter, either a failure of the
+code to properly implement the model described here, or a failure of the
+model itself, please report it to security@ipython.org.
+
+If you prefer to encrypt your security reports,
+you can use :download:`this PGP public key <ipython_security.asc>`.
+
+Affected use cases
+------------------
+
+Some use cases that work in Jupyter 1.0 will become less convenient in
+2.0 as a result of the security changes. We do our best to minimize
+these annoyance, but security is always at odds with convenience.
+
+Javascript and CSS in Markdown cells
+************************************
+
+While never officially supported, it had become common practice to put
+hidden Javascript or CSS styling in Markdown cells, so that they would
+not be visible on the page. Since Markdown cells are now sanitized (by
+`Google Caja <https://developers.google.com/caja>`__), all Javascript
+(including click event handlers, etc.) and CSS will be stripped.
+
+We plan to provide a mechanism for notebook themes, but in the meantime
+styling the notebook can only be done via either ``custom.css`` or CSS
+in HTML output. The latter only have an effect if the notebook is
+trusted, because otherwise the output will be sanitized just like
+Markdown.
+
+Collaboration
+*************
+
+When collaborating on a notebook, people probably want to see the
+outputs produced by their colleagues' most recent executions. Since each
+collaborator's key will differ, this will result in each share starting
+in an untrusted state. There are three basic approaches to this:
+
+- re-run notebooks when you get them (not always viable)
+- explicitly trust notebooks via ``jupyter trust`` or the notebook menu
+ (annoying, but easy)
+- share a notebook secret, and use a Jupyter profile dedicated to the
+ collaboration while working on the project.
+
+Multiple profiles or machines
+*****************************
+
+Since the notebook secret is stored in a profile directory by default,
+opening a notebook with a different profile or on a different machine
+will result in a different key, and thus be untrusted. The only current
+way to address this is by sharing the notebook secret. This can be
+facilitated by setting the configurable:
+
+.. sourcecode:: python
+
+ c.NotebookApp.secret_file = "/path/to/notebook_secret"
+
+in each profile, and only sharing the secret once per machine.
diff --git a/docs/source/template.tpl b/docs/source/template.tpl
new file mode 100644
index 0000000..af2561b
--- /dev/null
+++ b/docs/source/template.tpl
@@ -0,0 +1,20 @@
+{%- extends 'rst.tpl' -%}
+
+{% macro notebooklink() -%}
+
+`View the original notebook on nbviewer <http://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/{{ resources['metadata']['path'] }}/{{ resources['metadata']['name'] | replace(' ', '%20') }}.ipynb>`__
+
+
+{%- endmacro %}
+
+{%- block header %}
+{{ notebooklink() }}
+{% endblock header -%}
+
+{%- block footer %}
+{{ notebooklink() }}
+{% endblock footer -%}
+
+{% block markdowncell scoped %}
+{{ cell.source | markdown2rst | replace(".ipynb>", ".html>") }}
+{% endblock markdowncell %}
diff --git a/docs/source/ui_components.rst b/docs/source/ui_components.rst
new file mode 100644
index 0000000..1275977
--- /dev/null
+++ b/docs/source/ui_components.rst
@@ -0,0 +1,49 @@
+UI Components
+=============
+When opening bug reports or sending emails to the Jupyter mailing list, it is
+useful to know the names of different UI components so that other developers
+and users have an easier time helping you diagnose your problems. This section
+will familiarize you with the names of UI elements within the Notebook and the
+different Notebook modes.
+
+Notebook Dashboard
+-------------------
+
+When you launch ``jupyter notebook`` the first page that you encounter is the
+Notebook Dashboard.
+
+.. image:: /_static/images/jupyter-notebook-dashboard.png
+
+Notebook Editor
+---------------
+
+Once you've selected a Notebook to edit, the Notebook will open in the Notebook
+Editor.
+
+.. image:: /_static/images/jupyter-notebook-default.png
+
+Interactive User Interface Tour of the Notebook
+-----------------------------------------------
+
+If you would like to learn more about the specific elements within the Notebook
+Editor, you can go through the User Interface Tour by selecting Help in the
+menubar then selecting User Interface Tour.
+
+Edit Mode and Notebook Editor
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When a cell is in edit mode, the Cell Mode Indicator will change to reflect
+the cell's state. This state is indicated by a small pencil icon on the
+top right of the interface. When the cell is in command mode, there is no
+icon in that location.
+
+.. image:: /_static/images/jupyter-notebook-edit.png
+
+File Editor
+-----------
+
+Now let's say that you've chosen to open a Markdown file instead of a Notebook
+file whilst in the Notebook Dashboard. If so, the file will be opened in the
+File Editor.
+
+.. image:: /_static/images/jupyter-file-editor.png
diff --git a/git-hooks/README.md b/git-hooks/README.md
new file mode 100644
index 0000000..959b752
--- /dev/null
+++ b/git-hooks/README.md
@@ -0,0 +1,9 @@
+git hooks for Jupyter
+
+add these to your `.git/hooks`
+
+For now, we just have `post-checkout` and `post-merge`,
+both of which attempt to rebuild javascript and css sourcemaps,
+so make sure that you have a fully synced repo whenever you checkout or pull.
+
+To use these hooks, run `./install-hooks.sh`.
diff --git a/git-hooks/install-hooks.sh b/git-hooks/install-hooks.sh
new file mode 100755
index 0000000..60ea641
--- /dev/null
+++ b/git-hooks/install-hooks.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+DOTGIT=`git rev-parse --git-dir`
+TOPLEVEL=`git rev-parse --show-toplevel`
+TO=${DOTGIT}/hooks
+FROM=${TOPLEVEL}/git-hooks
+
+ln -s ${FROM}/post-checkout ${TO}/post-checkout
+ln -s ${FROM}/post-merge ${TO}/post-merge
diff --git a/git-hooks/post-checkout b/git-hooks/post-checkout
new file mode 100755
index 0000000..f329619
--- /dev/null
+++ b/git-hooks/post-checkout
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+if [[ "$(basename $0)" == "post-merge" ]]; then
+ PREVIOUS_HEAD=ORIG_HEAD
+else
+ PREVIOUS_HEAD=$1
+fi
+
+# if style changed (and less available), rebuild sourcemaps
+if [[
+ ! -z "$(git diff $PREVIOUS_HEAD notebook/static/*/js/**.js)"
+]]; then
+ echo "rebuilding javascript"
+ python setup.py js || echo "fail to rebuild javascript"
+fi
+
+if [[
+ ! -z "$(git diff $PREVIOUS_HEAD notebook/static/*/less/**.less)"
+]]; then
+ echo "rebuilding css sourcemaps"
+ python setup.py css || echo "fail to recompile css"
+fi
diff --git a/git-hooks/post-merge b/git-hooks/post-merge
new file mode 120000
index 0000000..5513d1d
--- /dev/null
+++ b/git-hooks/post-merge
@@ -0,0 +1 @@
+post-checkout \ No newline at end of file
diff --git a/notebook/__init__.py b/notebook/__init__.py
new file mode 100644
index 0000000..f406f3c
--- /dev/null
+++ b/notebook/__init__.py
@@ -0,0 +1,26 @@
+"""The Jupyter HTML Notebook"""
+
+import os
+# Packagers: modify this line if you store the notebook static files elsewhere
+DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
+
+# Packagers: modify the next line if you store the notebook template files
+# elsewhere
+
+# Include both notebook/ and notebook/templates/. This makes it
+# possible for users to override a template with a file that inherits from that
+# template.
+#
+# For example, if you want to override a specific block of notebook.html, you
+# can create a file called notebook.html that inherits from
+# templates/notebook.html, and the latter will resolve correctly to the base
+# implementation.
+DEFAULT_TEMPLATE_PATH_LIST = [
+ os.path.dirname(__file__),
+ os.path.join(os.path.dirname(__file__), "templates"),
+]
+
+del os
+
+from .nbextensions import install_nbextension
+from ._version import version_info, __version__
diff --git a/notebook/__main__.py b/notebook/__main__.py
new file mode 100644
index 0000000..9fb28e6
--- /dev/null
+++ b/notebook/__main__.py
@@ -0,0 +1,5 @@
+from __future__ import absolute_import
+
+if __name__ == '__main__':
+ from notebook import notebookapp as app
+ app.launch_new_instance()
diff --git a/notebook/_sysinfo.py b/notebook/_sysinfo.py
new file mode 100644
index 0000000..ea237bb
--- /dev/null
+++ b/notebook/_sysinfo.py
@@ -0,0 +1,95 @@
+# encoding: utf-8
+"""
+Utilities for getting information about Jupyter and the system it's running in.
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from __future__ import absolute_import
+
+import os
+import platform
+import pprint
+import sys
+import subprocess
+
+from ipython_genutils import py3compat, encoding
+
+import notebook
+
+def pkg_commit_hash(pkg_path):
+ """Get short form of commit hash given directory `pkg_path`
+
+ We get the commit hash from git if it's a repo.
+
+ If this fail, we return a not-found placeholder tuple
+
+ Parameters
+ ----------
+ pkg_path : str
+ directory containing package
+ only used for getting commit from active repo
+
+ Returns
+ -------
+ hash_from : str
+ Where we got the hash from - description
+ hash_str : str
+ short form of hash
+ """
+
+ # maybe we are in a repository, check for a .git folder
+ p = os.path
+ cur_path = None
+ par_path = pkg_path
+ while cur_path != par_path:
+ cur_path = par_path
+ if p.exists(p.join(cur_path, '.git')):
+ proc = subprocess.Popen('git rev-parse --short HEAD',
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ cwd=pkg_path, shell=True)
+ repo_commit, _ = proc.communicate()
+ if repo_commit:
+ return 'repository', repo_commit.strip().decode('ascii')
+ else:
+ return u'', u''
+ par_path = p.dirname(par_path)
+
+ return u'', u''
+
+
+def pkg_info(pkg_path):
+ """Return dict describing the context of this package
+
+ Parameters
+ ----------
+ pkg_path : str
+ path containing __init__.py for package
+
+ Returns
+ -------
+ context : dict
+ with named parameters of interest
+ """
+ src, hsh = pkg_commit_hash(pkg_path)
+ return dict(
+ notebook_version=notebook.__version__,
+ notebook_path=pkg_path,
+ commit_source=src,
+ commit_hash=hsh,
+ sys_version=sys.version,
+ sys_executable=sys.executable,
+ sys_platform=sys.platform,
+ platform=platform.platform(),
+ os_name=os.name,
+ default_encoding=encoding.DEFAULT_ENCODING,
+ )
+
+def get_sys_info():
+ """Return useful information about the system as a dict."""
+ p = os.path
+ path = p.realpath(p.dirname(p.abspath(p.join(notebook.__file__))))
+ return pkg_info(path)
+
diff --git a/notebook/_version.py b/notebook/_version.py
new file mode 100644
index 0000000..5f7ee50
--- /dev/null
+++ b/notebook/_version.py
@@ -0,0 +1,13 @@
+"""
+store the current version info of the notebook.
+
+"""
+
+# Downstream maintainer, when running `python.setup.py jsversion`,
+# the version string is propagated to the JavaScript files, do not forget to
+# patch the JavaScript files in `.postN` release done by distributions.
+
+# Next beta/alpha/rc release: The version number for beta is X.Y.ZbN **without dots**.
+
+version_info = (4, 2, 3)
+__version__ = '.'.join(map(str, version_info[:3])) + ''.join(version_info[3:])
diff --git a/notebook/allow76.py b/notebook/allow76.py
new file mode 100644
index 0000000..67e87e8
--- /dev/null
+++ b/notebook/allow76.py
@@ -0,0 +1,311 @@
+"""WebsocketProtocol76 from tornado 3.2.2 for tornado >= 4.0
+
+The contents of this file are Copyright (c) Tornado
+Used under the Apache 2.0 license
+"""
+
+
+from __future__ import absolute_import, division, print_function, with_statement
+# Author: Jacob Kristhammar, 2010
+
+import functools
+import hashlib
+import struct
+import time
+import tornado.escape
+import tornado.web
+
+from tornado.log import gen_log, app_log
+from tornado.util import bytes_type, unicode_type
+
+from tornado.websocket import WebSocketHandler, WebSocketProtocol13
+
+class AllowDraftWebSocketHandler(WebSocketHandler):
+ """Restore Draft76 support for tornado 4
+
+ Remove when we can run tests without phantomjs + qt4
+ """
+
+ # get is unmodified except between the BEGIN/END PATCH lines
+ @tornado.web.asynchronous
+ def get(self, *args, **kwargs):
+ self.open_args = args
+ self.open_kwargs = kwargs
+
+ # Upgrade header should be present and should be equal to WebSocket
+ if self.request.headers.get("Upgrade", "").lower() != 'websocket':
+ self.set_status(400)
+ self.finish("Can \"Upgrade\" only to \"WebSocket\".")
+ return
+
+ # Connection header should be upgrade. Some proxy servers/load balancers
+ # might mess with it.
+ headers = self.request.headers
+ connection = map(lambda s: s.strip().lower(), headers.get("Connection", "").split(","))
+ if 'upgrade' not in connection:
+ self.set_status(400)
+ self.finish("\"Connection\" must be \"Upgrade\".")
+ return
+
+ # Handle WebSocket Origin naming convention differences
+ # The difference between version 8 and 13 is that in 8 the
+ # client sends a "Sec-Websocket-Origin" header and in 13 it's
+ # simply "Origin".
+ if "Origin" in self.request.headers:
+ origin = self.request.headers.get("Origin")
+ else:
+ origin = self.request.headers.get("Sec-Websocket-Origin", None)
+
+
+ # If there was an origin header, check to make sure it matches
+ # according to check_origin. When the origin is None, we assume it
+ # did not come from a browser and that it can be passed on.
+ if origin is not None and not self.check_origin(origin):
+ self.set_status(403)
+ self.finish("Cross origin websockets not allowed")
+ return
+
+ self.stream = self.request.connection.detach()
+ self.stream.set_close_callback(self.on_connection_close)
+
+ if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
+ self.ws_connection = WebSocketProtocol13(self)
+ self.ws_connection.accept_connection()
+ #--------------- BEGIN PATCH ----------------
+ elif (self.allow_draft76() and
+ "Sec-WebSocket-Version" not in self.request.headers):
+ self.ws_connection = WebSocketProtocol76(self)
+ self.ws_connection.accept_connection()
+ #--------------- END PATCH ----------------
+ else:
+ if not self.stream.closed():
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 426 Upgrade Required\r\n"
+ "Sec-WebSocket-Version: 8\r\n\r\n"))
+ self.stream.close()
+
+ # 3.2 methods removed in 4.0:
+ def allow_draft76(self):
+ """Using this class allows draft76 connections by default"""
+ return True
+
+ def get_websocket_scheme(self):
+ """Return the url scheme used for this request, either "ws" or "wss".
+ This is normally decided by HTTPServer, but applications
+ may wish to override this if they are using an SSL proxy
+ that does not provide the X-Scheme header as understood
+ by HTTPServer.
+ Note that this is only used by the draft76 protocol.
+ """
+ return "wss" if self.request.protocol == "https" else "ws"
+
+
+
+# No modifications from tornado-3.2.2 below this line
+
+class WebSocketProtocol(object):
+ """Base class for WebSocket protocol versions.
+ """
+ def __init__(self, handler):
+ self.handler = handler
+ self.request = handler.request
+ self.stream = handler.stream
+ self.client_terminated = False
+ self.server_terminated = False
+
+ def async_callback(self, callback, *args, **kwargs):
+ """Wrap callbacks with this if they are used on asynchronous requests.
+
+ Catches exceptions properly and closes this WebSocket if an exception
+ is uncaught.
+ """
+ if args or kwargs:
+ callback = functools.partial(callback, *args, **kwargs)
+
+ def wrapper(*args, **kwargs):
+ try:
+ return callback(*args, **kwargs)
+ except Exception:
+ app_log.error("Uncaught exception in %s",
+ self.request.path, exc_info=True)
+ self._abort()
+ return wrapper
+
+ def on_connection_close(self):
+ self._abort()
+
+ def _abort(self):
+ """Instantly aborts the WebSocket connection by closing the socket"""
+ self.client_terminated = True
+ self.server_terminated = True
+ self.stream.close() # forcibly tear down the connection
+ self.close() # let the subclass cleanup
+
+
+class WebSocketProtocol76(WebSocketProtocol):
+ """Implementation of the WebSockets protocol, version hixie-76.
+
+ This class provides basic functionality to process WebSockets requests as
+ specified in
+ http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+ """
+ def __init__(self, handler):
+ WebSocketProtocol.__init__(self, handler)
+ self.challenge = None
+ self._waiting = None
+
+ def accept_connection(self):
+ try:
+ self._handle_websocket_headers()
+ except ValueError:
+ gen_log.debug("Malformed WebSocket request received")
+ self._abort()
+ return
+
+ scheme = self.handler.get_websocket_scheme()
+
+ # draft76 only allows a single subprotocol
+ subprotocol_header = ''
+ subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
+ if subprotocol:
+ selected = self.handler.select_subprotocol([subprotocol])
+ if selected:
+ assert selected == subprotocol
+ subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
+
+ # Write the initial headers before attempting to read the challenge.
+ # This is necessary when using proxies (such as HAProxy), which
+ # need to see the Upgrade headers before passing through the
+ # non-HTTP traffic that follows.
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
+ "Upgrade: WebSocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Server: TornadoServer/%(version)s\r\n"
+ "Sec-WebSocket-Origin: %(origin)s\r\n"
+ "Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
+ "%(subprotocol)s"
+ "\r\n" % (dict(
+ version=tornado.version,
+ origin=self.request.headers["Origin"],
+ scheme=scheme,
+ host=self.request.host,
+ uri=self.request.uri,
+ subprotocol=subprotocol_header))))
+ self.stream.read_bytes(8, self._handle_challenge)
+
+ def challenge_response(self, challenge):
+ """Generates the challenge response that's needed in the handshake
+
+ The challenge parameter should be the raw bytes as sent from the
+ client.
+ """
+ key_1 = self.request.headers.get("Sec-Websocket-Key1")
+ key_2 = self.request.headers.get("Sec-Websocket-Key2")
+ try:
+ part_1 = self._calculate_part(key_1)
+ part_2 = self._calculate_part(key_2)
+ except ValueError:
+ raise ValueError("Invalid Keys/Challenge")
+ return self._generate_challenge_response(part_1, part_2, challenge)
+
+ def _handle_challenge(self, challenge):
+ try:
+ challenge_response = self.challenge_response(challenge)
+ except ValueError:
+ gen_log.debug("Malformed key data in WebSocket request")
+ self._abort()
+ return
+ self._write_response(challenge_response)
+
+ def _write_response(self, challenge):
+ self.stream.write(challenge)
+ self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
+ self._receive_message()
+
+ def _handle_websocket_headers(self):
+ """Verifies all invariant- and required headers
+
+ If a header is missing or have an incorrect value ValueError will be
+ raised
+ """
+ fields = ("Origin", "Host", "Sec-Websocket-Key1",
+ "Sec-Websocket-Key2")
+ if not all(map(lambda f: self.request.headers.get(f), fields)):
+ raise ValueError("Missing/Invalid WebSocket headers")
+
+ def _calculate_part(self, key):
+ """Processes the key headers and calculates their key value.
+
+ Raises ValueError when feed invalid key."""
+ # pyflakes complains about variable reuse if both of these lines use 'c'
+ number = int(''.join(c for c in key if c.isdigit()))
+ spaces = len([c2 for c2 in key if c2.isspace()])
+ try:
+ key_number = number // spaces
+ except (ValueError, ZeroDivisionError):
+ raise ValueError
+ return struct.pack(">I", key_number)
+
+ def _generate_challenge_response(self, part_1, part_2, part_3):
+ m = hashlib.md5()
+ m.update(part_1)
+ m.update(part_2)
+ m.update(part_3)
+ return m.digest()
+
+ def _receive_message(self):
+ self.stream.read_bytes(1, self._on_frame_type)
+
+ def _on_frame_type(self, byte):
+ frame_type = ord(byte)
+ if frame_type == 0x00:
+ self.stream.read_until(b"\xff", self._on_end_delimiter)
+ elif frame_type == 0xff:
+ self.stream.read_bytes(1, self._on_length_indicator)
+ else:
+ self._abort()
+
+ def _on_end_delimiter(self, frame):
+ if not self.client_terminated:
+ self.async_callback(self.handler.on_message)(
+ frame[:-1].decode("utf-8", "replace"))
+ if not self.client_terminated:
+ self._receive_message()
+
+ def _on_length_indicator(self, byte):
+ if ord(byte) != 0x00:
+ self._abort()
+ return
+ self.client_terminated = True
+ self.close()
+
+ def write_message(self, message, binary=False):
+ """Sends the given message to the client of this Web Socket."""
+ if binary:
+ raise ValueError(
+ "Binary messages not supported by this version of websockets")
+ if isinstance(message, unicode_type):
+ message = message.encode("utf-8")
+ assert isinstance(message, bytes_type)
+ self.stream.write(b"\x00" + message + b"\xff")
+
+ def write_ping(self, data):
+ """Send ping frame."""
+ raise ValueError("Ping messages not supported by this version of websockets")
+
+ def close(self):
+ """Closes the WebSocket connection."""
+ if not self.server_terminated:
+ if not self.stream.closed():
+ self.stream.write("\xff\x00")
+ self.server_terminated = True
+ if self.client_terminated:
+ if self._waiting is not None:
+ self.stream.io_loop.remove_timeout(self._waiting)
+ self._waiting = None
+ self.stream.close()
+ elif self._waiting is None:
+ self._waiting = self.stream.io_loop.add_timeout(
+ time.time() + 5, self._abort)
+
diff --git a/notebook/auth/__init__.py b/notebook/auth/__init__.py
new file mode 100644
index 0000000..9f84fa2
--- /dev/null
+++ b/notebook/auth/__init__.py
@@ -0,0 +1 @@
+from .security import passwd
diff --git a/notebook/auth/login.py b/notebook/auth/login.py
new file mode 100644
index 0000000..24ac954
--- /dev/null
+++ b/notebook/auth/login.py
@@ -0,0 +1,109 @@
+"""Tornado handlers for logging into the notebook."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import uuid
+
+from tornado.escape import url_escape
+
+from ..auth.security import passwd_check
+
+from ..base.handlers import IPythonHandler
+
+
+class LoginHandler(IPythonHandler):
+ """The basic tornado login handler
+
+ authenticates with a hashed password from the configuration.
+ """
+ def _render(self, message=None):
+ self.write(self.render_template('login.html',
+ next=url_escape(self.get_argument('next', default=self.base_url)),
+ message=message,
+ ))
+
+ def get(self):
+ if self.current_user:
+ next_url = self.get_argument('next', default=self.base_url)
+ if not next_url.startswith(self.base_url):
+ # require that next_url be absolute path within our path
+ next_url = self.base_url
+ self.redirect(next_url)
+ else:
+ self._render()
+
+ @property
+ def hashed_password(self):
+ return self.password_from_settings(self.settings)
+
+ def post(self):
+ typed_password = self.get_argument('password', default=u'')
+ cookie_options = self.settings.get('cookie_options', {})
+ cookie_options.setdefault('httponly', True)
+ if self.login_available(self.settings):
+ if passwd_check(self.hashed_password, typed_password):
+ # tornado <4.2 has a bug that considers secure==True as soon as
+ # 'secure' kwarg is passed to set_secure_cookie
+ if self.settings.get('secure_cookie', self.request.protocol == 'https'):
+ cookie_options.setdefault('secure', True)
+ self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()), **cookie_options)
+ else:
+ self.set_status(401)
+ self._render(message={'error': 'Invalid password'})
+ return
+
+ next_url = self.get_argument('next', default=self.base_url)
+ if not next_url.startswith(self.base_url):
+ # require that next_url be absolute path within our path
+ next_url = self.base_url
+ self.redirect(next_url)
+
+ @classmethod
+ def get_user(cls, handler):
+ """Called by handlers.get_current_user for identifying the current user.
+
+ See tornado.web.RequestHandler.get_current_user for details.
+ """
+ # Can't call this get_current_user because it will collide when
+ # called on LoginHandler itself.
+
+ user_id = handler.get_secure_cookie(handler.cookie_name)
+ # For now the user_id should not return empty, but it could, eventually.
+ if user_id == '':
+ user_id = 'anonymous'
+ if user_id is None:
+ # prevent extra Invalid cookie sig warnings:
+ handler.clear_login_cookie()
+ if not handler.login_available:
+ user_id = 'anonymous'
+ return user_id
+
+
+ @classmethod
+ def validate_security(cls, app, ssl_options=None):
+ """Check the notebook application's security.
+
+ Show messages, or abort if necessary, based on the security configuration.
+ """
+ if not app.ip:
+ warning = "WARNING: The notebook server is listening on all IP addresses"
+ if ssl_options is None:
+ app.log.warning(warning + " and not using encryption. This "
+ "is not recommended.")
+ if not app.password:
+ app.log.warning(warning + " and not using authentication. "
+ "This is highly insecure and not recommended.")
+
+ @classmethod
+ def password_from_settings(cls, settings):
+ """Return the hashed password from the tornado settings.
+
+ If there is no configured password, an empty string will be returned.
+ """
+ return settings.get('password', u'')
+
+ @classmethod
+ def login_available(cls, settings):
+ """Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
+ return bool(cls.password_from_settings(settings))
diff --git a/notebook/auth/logout.py b/notebook/auth/logout.py
new file mode 100644
index 0000000..9eaee1f
--- /dev/null
+++ b/notebook/auth/logout.py
@@ -0,0 +1,23 @@
+"""Tornado handlers for logging out of the notebook.
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from ..base.handlers import IPythonHandler
+
+
+class LogoutHandler(IPythonHandler):
+
+ def get(self):
+ self.clear_login_cookie()
+ if self.login_available:
+ message = {'info': 'Successfully logged out.'}
+ else:
+ message = {'warning': 'Cannot log out. Notebook authentication '
+ 'is disabled.'}
+ self.write(self.render_template('logout.html',
+ message=message))
+
+
+default_handlers = [(r"/logout", LogoutHandler)] \ No newline at end of file
diff --git a/notebook/auth/security.py b/notebook/auth/security.py
new file mode 100644
index 0000000..b9865a3
--- /dev/null
+++ b/notebook/auth/security.py
@@ -0,0 +1,101 @@
+"""
+Password generation for the Notebook.
+"""
+import getpass
+import hashlib
+import random
+
+from ipython_genutils.py3compat import cast_bytes, str_to_bytes
+
+# Length of the salt in nr of hex chars, which implies salt_len * 4
+# bits of randomness.
+salt_len = 12
+
+
+def passwd(passphrase=None, algorithm='sha1'):
+ """Generate hashed password and salt for use in notebook configuration.
+
+ In the notebook configuration, set `c.NotebookApp.password` to
+ the generated string.
+
+ Parameters
+ ----------
+ passphrase : str
+ Password to hash. If unspecified, the user is asked to input
+ and verify a password.
+ algorithm : str
+ Hashing algorithm to use (e.g, 'sha1' or any argument supported
+ by :func:`hashlib.new`).
+
+ Returns
+ -------
+ hashed_passphrase : str
+ Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'.
+
+ Examples
+ --------
+ >>> passwd('mypassword')
+ 'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12'
+
+ """
+ if passphrase is None:
+ for i in range(3):
+ p0 = getpass.getpass('Enter password: ')
+ p1 = getpass.getpass('Verify password: ')
+ if p0 == p1:
+ passphrase = p0
+ break
+ else:
+ print('Passwords do not match.')
+ else:
+ raise ValueError('No matching passwords found. Giving up.')
+
+ h = hashlib.new(algorithm)
+ salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len)
+ h.update(cast_bytes(passphrase, 'utf-8') + str_to_bytes(salt, 'ascii'))
+
+ return ':'.join((algorithm, salt, h.hexdigest()))
+
+
+def passwd_check(hashed_passphrase, passphrase):
+ """Verify that a given passphrase matches its hashed version.
+
+ Parameters
+ ----------
+ hashed_passphrase : str
+ Hashed password, in the format returned by `passwd`.
+ passphrase : str
+ Passphrase to validate.
+
+ Returns
+ -------
+ valid : bool
+ True if the passphrase matches the hash.
+
+ Examples
+ --------
+ >>> from notebook.auth.security import passwd_check
+ >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
+ ... 'mypassword')
+ True
+
+ >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
+ ... 'anotherpassword')
+ False
+ """
+ try:
+ algorithm, salt, pw_digest = hashed_passphrase.split(':', 2)
+ except (ValueError, TypeError):
+ return False
+
+ try:
+ h = hashlib.new(algorithm)
+ except ValueError:
+ return False
+
+ if len(pw_digest) == 0:
+ return False
+
+ h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii'))
+
+ return h.hexdigest() == pw_digest
diff --git a/notebook/auth/tests/__init__.py b/notebook/auth/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/auth/tests/__init__.py
diff --git a/notebook/auth/tests/test_security.py b/notebook/auth/tests/test_security.py
new file mode 100644
index 0000000..a17e800
--- /dev/null
+++ b/notebook/auth/tests/test_security.py
@@ -0,0 +1,25 @@
+# coding: utf-8
+from ..security import passwd, passwd_check, salt_len
+import nose.tools as nt
+
+def test_passwd_structure():
+ p = passwd('passphrase')
+ algorithm, salt, hashed = p.split(':')
+ nt.assert_equal(algorithm, 'sha1')
+ nt.assert_equal(len(salt), salt_len)
+ nt.assert_equal(len(hashed), 40)
+
+def test_roundtrip():
+ p = passwd('passphrase')
+ nt.assert_equal(passwd_check(p, 'passphrase'), True)
+
+def test_bad():
+ p = passwd('passphrase')
+ nt.assert_equal(passwd_check(p, p), False)
+ nt.assert_equal(passwd_check(p, 'a:b:c:d'), False)
+ nt.assert_equal(passwd_check(p, 'a:b'), False)
+
+def test_passwd_check_unicode():
+ # GH issue #4524
+ phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f'
+ assert passwd_check(phash, u"łe¶ŧ←↓→") \ No newline at end of file
diff --git a/notebook/base/__init__.py b/notebook/base/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/base/__init__.py
diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py
new file mode 100644
index 0000000..502cbf7
--- /dev/null
+++ b/notebook/base/handlers.py
@@ -0,0 +1,615 @@
+"""Base Tornado handlers for the notebook server."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import functools
+import json
+import os
+import re
+import sys
+import traceback
+try:
+ # py3
+ from http.client import responses
+except ImportError:
+ from httplib import responses
+try:
+ from urllib.parse import urlparse # Py 3
+except ImportError:
+ from urlparse import urlparse # Py 2
+
+from jinja2 import TemplateNotFound
+from tornado import web
+
+from tornado import gen, escape
+from tornado.log import app_log
+
+from notebook._sysinfo import get_sys_info
+
+from traitlets.config import Application
+from ipython_genutils.path import filefind
+from ipython_genutils.py3compat import string_types
+
+import notebook
+from notebook.utils import is_hidden, url_path_join, url_is_absolute, url_escape
+from notebook.services.security import csp_report_uri
+
+#-----------------------------------------------------------------------------
+# Top-level handlers
+#-----------------------------------------------------------------------------
+non_alphanum = re.compile(r'[^A-Za-z0-9]')
+
+sys_info = json.dumps(get_sys_info())
+
+class AuthenticatedHandler(web.RequestHandler):
+ """A RequestHandler with an authenticated user."""
+
+ @property
+ def content_security_policy(self):
+ """The default Content-Security-Policy header
+
+ Can be overridden by defining Content-Security-Policy in settings['headers']
+ """
+ return '; '.join([
+ "frame-ancestors 'self'",
+ # Make sure the report-uri is relative to the base_url
+ "report-uri " + url_path_join(self.base_url, csp_report_uri),
+ ])
+
+ def set_default_headers(self):
+ headers = self.settings.get('headers', {})
+
+ if "Content-Security-Policy" not in headers:
+ headers["Content-Security-Policy"] = self.content_security_policy
+
+ # Allow for overriding headers
+ for header_name,value in headers.items() :
+ try:
+ self.set_header(header_name, value)
+ except Exception as e:
+ # tornado raise Exception (not a subclass)
+ # if method is unsupported (websocket and Access-Control-Allow-Origin
+ # for example, so just ignore)
+ self.log.debug(e)
+
+ def clear_login_cookie(self):
+ self.clear_cookie(self.cookie_name)
+
+ def get_current_user(self):
+ if self.login_handler is None:
+ return 'anonymous'
+ return self.login_handler.get_user(self)
+
+ @property
+ def cookie_name(self):
+ default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
+ self.request.host
+ ))
+ return self.settings.get('cookie_name', default_cookie_name)
+
+ @property
+ def logged_in(self):
+ """Is a user currently logged in?"""
+ user = self.get_current_user()
+ return (user and not user == 'anonymous')
+
+ @property
+ def login_handler(self):
+ """Return the login handler for this application, if any."""
+ return self.settings.get('login_handler_class', None)
+
+ @property
+ def login_available(self):
+ """May a user proceed to log in?
+
+ This returns True if login capability is available, irrespective of
+ whether the user is already logged in or not.
+
+ """
+ if self.login_handler is None:
+ return False
+ return bool(self.login_handler.login_available(self.settings))
+
+
+class IPythonHandler(AuthenticatedHandler):
+ """IPython-specific extensions to authenticated handling
+
+ Mostly property shortcuts to IPython-specific settings.
+ """
+
+
+ @property
+ def ignore_minified_js(self):
+ """Wether to user bundle in template. (*.min files)
+
+ Mainly use for development and avoid file recompilation
+ """
+ return self.settings.get('ignore_minified_js', False)
+
+ @property
+ def config(self):
+ return self.settings.get('config', None)
+
+ @property
+ def log(self):
+ """use the IPython log by default, falling back on tornado's logger"""
+ if Application.initialized():
+ return Application.instance().log
+ else:
+ return app_log
+
+ @property
+ def jinja_template_vars(self):
+ """User-supplied values to supply to jinja templates."""
+ return self.settings.get('jinja_template_vars', {})
+
+ #---------------------------------------------------------------
+ # URLs
+ #---------------------------------------------------------------
+
+ @property
+ def version_hash(self):
+ """The version hash to use for cache hints for static files"""
+ return self.settings.get('version_hash', '')
+
+ @property
+ def mathjax_url(self):
+ url = self.settings.get('mathjax_url', '')
+ if not url or url_is_absolute(url):
+ return url
+ return url_path_join(self.base_url, url)
+
+ @property
+ def base_url(self):
+ return self.settings.get('base_url', '/')
+
+ @property
+ def default_url(self):
+ return self.settings.get('default_url', '')
+
+ @property
+ def ws_url(self):
+ return self.settings.get('websocket_url', '')
+
+ @property
+ def contents_js_source(self):
+ self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
+ 'services/contents'))
+ return self.settings.get('contents_js_source', 'services/contents')
+
+ #---------------------------------------------------------------
+ # Manager objects
+ #---------------------------------------------------------------
+
+ @property
+ def kernel_manager(self):
+ return self.settings['kernel_manager']
+
+ @property
+ def contents_manager(self):
+ return self.settings['contents_manager']
+
+ @property
+ def session_manager(self):
+ return self.settings['session_manager']
+
+ @property
+ def terminal_manager(self):
+ return self.settings['terminal_manager']
+
+ @property
+ def kernel_spec_manager(self):
+ return self.settings['kernel_spec_manager']
+
+ @property
+ def config_manager(self):
+ return self.settings['config_manager']
+
+ #---------------------------------------------------------------
+ # CORS
+ #---------------------------------------------------------------
+
+ @property
+ def allow_origin(self):
+ """Normal Access-Control-Allow-Origin"""
+ return self.settings.get('allow_origin', '')
+
+ @property
+ def allow_origin_pat(self):
+ """Regular expression version of allow_origin"""
+ return self.settings.get('allow_origin_pat', None)
+
+ @property
+ def allow_credentials(self):
+ """Whether to set Access-Control-Allow-Credentials"""
+ return self.settings.get('allow_credentials', False)
+
+ def set_default_headers(self):
+ """Add CORS headers, if defined"""
+ super(IPythonHandler, self).set_default_headers()
+ if self.allow_origin:
+ self.set_header("Access-Control-Allow-Origin", self.allow_origin)
+ elif self.allow_origin_pat:
+ origin = self.get_origin()
+ if origin and self.allow_origin_pat.match(origin):
+ self.set_header("Access-Control-Allow-Origin", origin)
+ if self.allow_credentials:
+ self.set_header("Access-Control-Allow-Credentials", 'true')
+
+ def get_origin(self):
+ # Handle WebSocket Origin naming convention differences
+ # The difference between version 8 and 13 is that in 8 the
+ # client sends a "Sec-Websocket-Origin" header and in 13 it's
+ # simply "Origin".
+ if "Origin" in self.request.headers:
+ origin = self.request.headers.get("Origin")
+ else:
+ origin = self.request.headers.get("Sec-Websocket-Origin", None)
+ return origin
+
+ # origin_to_satisfy_tornado is present because tornado requires
+ # check_origin to take an origin argument, but we don't use it
+ def check_origin(self, origin_to_satisfy_tornado=""):
+ """Check Origin for cross-site API requests, including websockets
+
+ Copied from WebSocket with changes:
+
+ - allow unspecified host/origin (e.g. scripts)
+ """
+ if self.allow_origin == '*':
+ return True
+
+ host = self.request.headers.get("Host")
+ origin = self.request.headers.get("Origin")
+
+ # If no header is provided, assume it comes from a script/curl.
+ # We are only concerned with cross-site browser stuff here.
+ if origin is None or host is None:
+ return True
+
+ origin = origin.lower()
+ origin_host = urlparse(origin).netloc
+
+ # OK if origin matches host
+ if origin_host == host:
+ return True
+
+ # Check CORS headers
+ if self.allow_origin:
+ allow = self.allow_origin == origin
+ elif self.allow_origin_pat:
+ allow = bool(self.allow_origin_pat.match(origin))
+ else:
+ # No CORS headers deny the request
+ allow = False
+ if not allow:
+ self.log.warn("Blocking Cross Origin API request. Origin: %s, Host: %s",
+ origin, host,
+ )
+ return allow
+
+ #---------------------------------------------------------------
+ # template rendering
+ #---------------------------------------------------------------
+
+ def get_template(self, name):
+ """Return the jinja template object for a given name"""
+ return self.settings['jinja2_env'].get_template(name)
+
+ def render_template(self, name, **ns):
+ ns.update(self.template_namespace)
+ template = self.get_template(name)
+ return template.render(**ns)
+
+ @property
+ def template_namespace(self):
+ return dict(
+ base_url=self.base_url,
+ default_url=self.default_url,
+ ws_url=self.ws_url,
+ logged_in=self.logged_in,
+ login_available=self.login_available,
+ static_url=self.static_url,
+ sys_info=sys_info,
+ contents_js_source=self.contents_js_source,
+ version_hash=self.version_hash,
+ ignore_minified_js=self.ignore_minified_js,
+ **self.jinja_template_vars
+ )
+
+ def get_json_body(self):
+ """Return the body of the request as JSON data."""
+ if not self.request.body:
+ return None
+ # Do we need to call body.decode('utf-8') here?
+ body = self.request.body.strip().decode(u'utf-8')
+ try:
+ model = json.loads(body)
+ except Exception:
+ self.log.debug("Bad JSON: %r", body)
+ self.log.error("Couldn't parse JSON", exc_info=True)
+ raise web.HTTPError(400, u'Invalid JSON in body of request')
+ return model
+
+ def write_error(self, status_code, **kwargs):
+ """render custom error pages"""
+ exc_info = kwargs.get('exc_info')
+ message = ''
+ status_message = responses.get(status_code, 'Unknown HTTP Error')
+ if exc_info:
+ exception = exc_info[1]
+ # get the custom message, if defined
+ try:
+ message = exception.log_message % exception.args
+ except Exception:
+ pass
+
+ # construct the custom reason, if defined
+ reason = getattr(exception, 'reason', '')
+ if reason:
+ status_message = reason
+
+ # build template namespace
+ ns = dict(
+ status_code=status_code,
+ status_message=status_message,
+ message=message,
+ exception=exception,
+ )
+
+ self.set_header('Content-Type', 'text/html')
+ # render the template
+ try:
+ html = self.render_template('%s.html' % status_code, **ns)
+ except TemplateNotFound:
+ self.log.debug("No template for %d", status_code)
+ html = self.render_template('error.html', **ns)
+
+ self.write(html)
+
+
+class APIHandler(IPythonHandler):
+ """Base class for API handlers"""
+
+ def prepare(self):
+ if not self.check_origin():
+ raise web.HTTPError(404)
+ return super(APIHandler, self).prepare()
+
+ @property
+ def content_security_policy(self):
+ csp = '; '.join([
+ super(APIHandler, self).content_security_policy,
+ "default-src 'none'",
+ ])
+ return csp
+
+ def finish(self, *args, **kwargs):
+ self.set_header('Content-Type', 'application/json')
+ return super(APIHandler, self).finish(*args, **kwargs)
+
+ def options(self, *args, **kwargs):
+ self.set_header('Access-Control-Allow-Headers', 'accept, content-type')
+ self.set_header('Access-Control-Allow-Methods',
+ 'GET, PUT, POST, PATCH, DELETE, OPTIONS')
+ self.finish()
+
+
+class Template404(IPythonHandler):
+ """Render our 404 template"""
+ def prepare(self):
+ raise web.HTTPError(404)
+
+
+class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
+ """static files should only be accessible when logged in"""
+
+ @web.authenticated
+ def get(self, path):
+ if os.path.splitext(path)[1] == '.ipynb':
+ name = path.rsplit('/', 1)[-1]
+ self.set_header('Content-Type', 'application/json')
+ self.set_header('Content-Disposition','attachment; filename="%s"' % escape.url_escape(name))
+
+ return web.StaticFileHandler.get(self, path)
+
+ def set_headers(self):
+ super(AuthenticatedFileHandler, self).set_headers()
+ # disable browser caching, rely on 304 replies for savings
+ if "v" not in self.request.arguments:
+ self.add_header("Cache-Control", "no-cache")
+
+ def compute_etag(self):
+ return None
+
+ def validate_absolute_path(self, root, absolute_path):
+ """Validate and return the absolute path.
+
+ Requires tornado 3.1
+
+ Adding to tornado's own handling, forbids the serving of hidden files.
+ """
+ abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
+ abs_root = os.path.abspath(root)
+ if is_hidden(abs_path, abs_root):
+ self.log.info("Refusing to serve hidden file, via 404 Error")
+ raise web.HTTPError(404)
+ return abs_path
+
+
+def json_errors(method):
+ """Decorate methods with this to return GitHub style JSON errors.
+
+ This should be used on any JSON API on any handler method that can raise HTTPErrors.
+
+ This will grab the latest HTTPError exception using sys.exc_info
+ and then:
+
+ 1. Set the HTTP status code based on the HTTPError
+ 2. Create and return a JSON body with a message field describing
+ the error in a human readable form.
+ """
+ @functools.wraps(method)
+ @gen.coroutine
+ def wrapper(self, *args, **kwargs):
+ try:
+ result = yield gen.maybe_future(method(self, *args, **kwargs))
+ except web.HTTPError as e:
+ self.set_header('Content-Type', 'application/json')
+ status = e.status_code
+ message = e.log_message
+ self.log.warn(message)
+ self.set_status(e.status_code)
+ reply = dict(message=message, reason=e.reason)
+ self.finish(json.dumps(reply))
+ except Exception:
+ self.set_header('Content-Type', 'application/json')
+ self.log.error("Unhandled error in API request", exc_info=True)
+ status = 500
+ message = "Unknown server error"
+ t, value, tb = sys.exc_info()
+ self.set_status(status)
+ tb_text = ''.join(traceback.format_exception(t, value, tb))
+ reply = dict(message=message, reason=None, traceback=tb_text)
+ self.finish(json.dumps(reply))
+ else:
+ # FIXME: can use regular return in generators in py3
+ raise gen.Return(result)
+ return wrapper
+
+
+
+#-----------------------------------------------------------------------------
+# File handler
+#-----------------------------------------------------------------------------
+
+# to minimize subclass changes:
+HTTPError = web.HTTPError
+
+class FileFindHandler(IPythonHandler, web.StaticFileHandler):
+ """subclass of StaticFileHandler for serving files from a search path"""
+
+ # cache search results, don't search for files more than once
+ _static_paths = {}
+
+ def set_headers(self):
+ super(FileFindHandler, self).set_headers()
+ # disable browser caching, rely on 304 replies for savings
+ if "v" not in self.request.arguments or \
+ any(self.request.path.startswith(path) for path in self.no_cache_paths):
+ self.set_header("Cache-Control", "no-cache")
+
+ def initialize(self, path, default_filename=None, no_cache_paths=None):
+ self.no_cache_paths = no_cache_paths or []
+
+ if isinstance(path, string_types):
+ path = [path]
+
+ self.root = tuple(
+ os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
+ )
+ self.default_filename = default_filename
+
+ def compute_etag(self):
+ return None
+
+ @classmethod
+ def get_absolute_path(cls, roots, path):
+ """locate a file to serve on our static file search path"""
+ with cls._lock:
+ if path in cls._static_paths:
+ return cls._static_paths[path]
+ try:
+ abspath = os.path.abspath(filefind(path, roots))
+ except IOError:
+ # IOError means not found
+ return ''
+
+ cls._static_paths[path] = abspath
+ return abspath
+
+ def validate_absolute_path(self, root, absolute_path):
+ """check if the file should be served (raises 404, 403, etc.)"""
+ if absolute_path == '':
+ raise web.HTTPError(404)
+
+ for root in self.root:
+ if (absolute_path + os.sep).startswith(root):
+ break
+
+ return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
+
+
+class APIVersionHandler(APIHandler):
+
+ @json_errors
+ def get(self):
+ # not authenticated, so give as few info as possible
+ self.finish(json.dumps({"version":notebook.__version__}))
+
+
+class TrailingSlashHandler(web.RequestHandler):
+ """Simple redirect handler that strips trailing slashes
+
+ This should be the first, highest priority handler.
+ """
+
+ def get(self):
+ self.redirect(self.request.uri.rstrip('/'))
+
+ post = put = get
+
+
+class FilesRedirectHandler(IPythonHandler):
+ """Handler for redirecting relative URLs to the /files/ handler"""
+
+ @staticmethod
+ def redirect_to_files(self, path):
+ """make redirect logic a reusable static method
+
+ so it can be called from other handlers.
+ """
+ cm = self.contents_manager
+ if cm.dir_exists(path):
+ # it's a *directory*, redirect to /tree
+ url = url_path_join(self.base_url, 'tree', url_escape(path))
+ else:
+ orig_path = path
+ # otherwise, redirect to /files
+ parts = path.split('/')
+
+ if not cm.file_exists(path=path) and 'files' in parts:
+ # redirect without files/ iff it would 404
+ # this preserves pre-2.0-style 'files/' links
+ self.log.warn("Deprecated files/ URL: %s", orig_path)
+ parts.remove('files')
+ path = '/'.join(parts)
+
+ if not cm.file_exists(path=path):
+ raise web.HTTPError(404)
+
+ url = url_path_join(self.base_url, 'files', url_escape(path))
+ self.log.debug("Redirecting %s to %s", self.request.path, url)
+ self.redirect(url)
+
+ def get(self, path=''):
+ return self.redirect_to_files(self, path)
+
+
+#-----------------------------------------------------------------------------
+# URL pattern fragments for re-use
+#-----------------------------------------------------------------------------
+
+# path matches any number of `/foo[/bar...]` or just `/` or ''
+path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
+
+#-----------------------------------------------------------------------------
+# URL to handler mappings
+#-----------------------------------------------------------------------------
+
+
+default_handlers = [
+ (r".*/", TrailingSlashHandler),
+ (r"api", APIVersionHandler)
+]
diff --git a/notebook/base/zmqhandlers.py b/notebook/base/zmqhandlers.py
new file mode 100644
index 0000000..1cfa8ec
--- /dev/null
+++ b/notebook/base/zmqhandlers.py
@@ -0,0 +1,299 @@
+# coding: utf-8
+"""Tornado handlers for WebSocket <-> ZMQ sockets."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import os
+import json
+import struct
+import warnings
+import sys
+
+try:
+ from urllib.parse import urlparse # Py 3
+except ImportError:
+ from urlparse import urlparse # Py 2
+
+import tornado
+from tornado import gen, ioloop, web
+from tornado.websocket import WebSocketHandler
+
+from jupyter_client.session import Session
+from jupyter_client.jsonutil import date_default, extract_dates
+from ipython_genutils.py3compat import cast_unicode
+
+from .handlers import IPythonHandler
+
+def serialize_binary_message(msg):
+ """serialize a message as a binary blob
+
+ Header:
+
+ 4 bytes: number of msg parts (nbufs) as 32b int
+ 4 * nbufs bytes: offset for each buffer as integer as 32b int
+
+ Offsets are from the start of the buffer, including the header.
+
+ Returns
+ -------
+
+ The message serialized to bytes.
+
+ """
+ # don't modify msg or buffer list in-place
+ msg = msg.copy()
+ buffers = list(msg.pop('buffers'))
+ if sys.version_info < (3, 4):
+ buffers = [x.tobytes() for x in buffers]
+ bmsg = json.dumps(msg, default=date_default).encode('utf8')
+ buffers.insert(0, bmsg)
+ nbufs = len(buffers)
+ offsets = [4 * (nbufs + 1)]
+ for buf in buffers[:-1]:
+ offsets.append(offsets[-1] + len(buf))
+ offsets_buf = struct.pack('!' + 'I' * (nbufs + 1), nbufs, *offsets)
+ buffers.insert(0, offsets_buf)
+ return b''.join(buffers)
+
+
+def deserialize_binary_message(bmsg):
+ """deserialize a message from a binary blog
+
+ Header:
+
+ 4 bytes: number of msg parts (nbufs) as 32b int
+ 4 * nbufs bytes: offset for each buffer as integer as 32b int
+
+ Offsets are from the start of the buffer, including the header.
+
+ Returns
+ -------
+
+ message dictionary
+ """
+ nbufs = struct.unpack('!i', bmsg[:4])[0]
+ offsets = list(struct.unpack('!' + 'I' * nbufs, bmsg[4:4*(nbufs+1)]))
+ offsets.append(None)
+ bufs = []
+ for start, stop in zip(offsets[:-1], offsets[1:]):
+ bufs.append(bmsg[start:stop])
+ msg = json.loads(bufs[0].decode('utf8'))
+ msg['header'] = extract_dates(msg['header'])
+ msg['parent_header'] = extract_dates(msg['parent_header'])
+ msg['buffers'] = bufs[1:]
+ return msg
+
+# ping interval for keeping websockets alive (30 seconds)
+WS_PING_INTERVAL = 30000
+
+if os.environ.get('IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS', False):
+ warnings.warn("""Allowing draft76 websocket connections!
+ This should only be done for testing with phantomjs!""")
+ from notebook import allow76
+ WebSocketHandler = allow76.AllowDraftWebSocketHandler
+ # draft 76 doesn't support ping
+ WS_PING_INTERVAL = 0
+
+
+class WebSocketMixin(object):
+ """Mixin for common websocket options"""
+ ping_callback = None
+ last_ping = 0
+ last_pong = 0
+ stream = None
+
+ @property
+ def ping_interval(self):
+ """The interval for websocket keep-alive pings.
+
+ Set ws_ping_interval = 0 to disable pings.
+ """
+ return self.settings.get('ws_ping_interval', WS_PING_INTERVAL)
+
+ @property
+ def ping_timeout(self):
+ """If no ping is received in this many milliseconds,
+ close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
+ Default is max of 3 pings or 30 seconds.
+ """
+ return self.settings.get('ws_ping_timeout',
+ max(3 * self.ping_interval, WS_PING_INTERVAL)
+ )
+
+ def check_origin(self, origin=None):
+ """Check Origin == Host or Access-Control-Allow-Origin.
+
+ Tornado >= 4 calls this method automatically, raising 403 if it returns False.
+ """
+ if self.allow_origin == '*':
+ return True
+
+ host = self.request.headers.get("Host")
+ if origin is None:
+ origin = self.get_origin()
+
+ # If no header is provided, assume we can't verify origin
+ if origin is None:
+ self.log.warn("Missing Origin header, rejecting WebSocket connection.")
+ return False
+ if host is None:
+ self.log.warn("Missing Host header, rejecting WebSocket connection.")
+ return False
+
+ origin = origin.lower()
+ origin_host = urlparse(origin).netloc
+
+ # OK if origin matches host
+ if origin_host == host:
+ return True
+
+ # Check CORS headers
+ if self.allow_origin:
+ allow = self.allow_origin == origin
+ elif self.allow_origin_pat:
+ allow = bool(self.allow_origin_pat.match(origin))
+ else:
+ # No CORS headers deny the request
+ allow = False
+ if not allow:
+ self.log.warn("Blocking Cross Origin WebSocket Attempt. Origin: %s, Host: %s",
+ origin, host,
+ )
+ return allow
+
+ def clear_cookie(self, *args, **kwargs):
+ """meaningless for websockets"""
+ pass
+
+ def open(self, *args, **kwargs):
+ self.log.debug("Opening websocket %s", self.request.path)
+
+ # start the pinging
+ if self.ping_interval > 0:
+ loop = ioloop.IOLoop.current()
+ self.last_ping = loop.time() # Remember time of last ping
+ self.last_pong = self.last_ping
+ self.ping_callback = ioloop.PeriodicCallback(
+ self.send_ping, self.ping_interval, io_loop=loop,
+ )
+ self.ping_callback.start()
+ return super(WebSocketMixin, self).open(*args, **kwargs)
+
+ def send_ping(self):
+ """send a ping to keep the websocket alive"""
+ if self.stream.closed() and self.ping_callback is not None:
+ self.ping_callback.stop()
+ return
+
+ # check for timeout on pong. Make sure that we really have sent a recent ping in
+ # case the machine with both server and client has been suspended since the last ping.
+ now = ioloop.IOLoop.current().time()
+ since_last_pong = 1e3 * (now - self.last_pong)
+ since_last_ping = 1e3 * (now - self.last_ping)
+ if since_last_ping < 2*self.ping_interval and since_last_pong > self.ping_timeout:
+ self.log.warn("WebSocket ping timeout after %i ms.", since_last_pong)
+ self.close()
+ return
+
+ self.ping(b'')
+ self.last_ping = now
+
+ def on_pong(self, data):
+ self.last_pong = ioloop.IOLoop.current().time()
+
+
+class ZMQStreamHandler(WebSocketMixin, WebSocketHandler):
+
+ if tornado.version_info < (4,1):
+ """Backport send_error from tornado 4.1 to 4.0"""
+ def send_error(self, *args, **kwargs):
+ if self.stream is None:
+ super(WebSocketHandler, self).send_error(*args, **kwargs)
+ else:
+ # If we get an uncaught exception during the handshake,
+ # we have no choice but to abruptly close the connection.
+ # TODO: for uncaught exceptions after the handshake,
+ # we can close the connection more gracefully.
+ self.stream.close()
+
+
+ def _reserialize_reply(self, msg_or_list, channel=None):
+ """Reserialize a reply message using JSON.
+
+ msg_or_list can be an already-deserialized msg dict or the zmq buffer list.
+ If it is the zmq list, it will be deserialized with self.session.
+
+ This takes the msg list from the ZMQ socket and serializes the result for the websocket.
+ This method should be used by self._on_zmq_reply to build messages that can
+ be sent back to the browser.
+
+ """
+ if isinstance(msg_or_list, dict):
+ # already unpacked
+ msg = msg_or_list
+ else:
+ idents, msg_list = self.session.feed_identities(msg_or_list)
+ msg = self.session.deserialize(msg_list)
+ if channel:
+ msg['channel'] = channel
+ if msg['buffers']:
+ buf = serialize_binary_message(msg)
+ return buf
+ else:
+ smsg = json.dumps(msg, default=date_default)
+ return cast_unicode(smsg)
+
+ def _on_zmq_reply(self, stream, msg_list):
+ # Sometimes this gets triggered when the on_close method is scheduled in the
+ # eventloop but hasn't been called.
+ if self.stream.closed() or stream.closed():
+ self.log.warn("zmq message arrived on closed channel")
+ self.close()
+ return
+ channel = getattr(stream, 'channel', None)
+ try:
+ msg = self._reserialize_reply(msg_list, channel=channel)
+ except Exception:
+ self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
+ else:
+ self.write_message(msg, binary=isinstance(msg, bytes))
+
+
+class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
+
+ def set_default_headers(self):
+ """Undo the set_default_headers in IPythonHandler
+
+ which doesn't make sense for websockets
+ """
+ pass
+
+ def pre_get(self):
+ """Run before finishing the GET request
+
+ Extend this method to add logic that should fire before
+ the websocket finishes completing.
+ """
+ # authenticate the request before opening the websocket
+ if self.get_current_user() is None:
+ self.log.warn("Couldn't authenticate WebSocket connection")
+ raise web.HTTPError(403)
+
+ if self.get_argument('session_id', False):
+ self.session.session = cast_unicode(self.get_argument('session_id'))
+ else:
+ self.log.warn("No session ID specified")
+
+ @gen.coroutine
+ def get(self, *args, **kwargs):
+ # pre_get can be a coroutine in subclasses
+ # assign and yield in two step to avoid tornado 3 issues
+ res = self.pre_get()
+ yield gen.maybe_future(res)
+ super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
+
+ def initialize(self):
+ self.log.debug("Initializing websocket connection %s", self.request.path)
+ self.session = Session(config=self.config)
+
diff --git a/notebook/edit/__init__.py b/notebook/edit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/edit/__init__.py
diff --git a/notebook/edit/handlers.py b/notebook/edit/handlers.py
new file mode 100644
index 0000000..c0864ab
--- /dev/null
+++ b/notebook/edit/handlers.py
@@ -0,0 +1,29 @@
+#encoding: utf-8
+"""Tornado handlers for the terminal emulator."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from tornado import web
+from ..base.handlers import IPythonHandler, path_regex
+from ..utils import url_escape
+
+class EditorHandler(IPythonHandler):
+ """Render the text editor interface."""
+ @web.authenticated
+ def get(self, path):
+ path = path.strip('/')
+ if not self.contents_manager.file_exists(path):
+ raise web.HTTPError(404, u'File does not exist: %s' % path)
+
+ basename = path.rsplit('/', 1)[-1]
+ self.write(self.render_template('edit.html',
+ file_path=url_escape(path),
+ basename=basename,
+ page_title=basename + " (editing)",
+ )
+ )
+
+default_handlers = [
+ (r"/edit%s" % path_regex, EditorHandler),
+] \ No newline at end of file
diff --git a/notebook/files/__init__.py b/notebook/files/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/files/__init__.py
diff --git a/notebook/files/handlers.py b/notebook/files/handlers.py
new file mode 100644
index 0000000..f5c98c9
--- /dev/null
+++ b/notebook/files/handlers.py
@@ -0,0 +1,60 @@
+"""Serve files directly from the ContentsManager."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import os
+import mimetypes
+import json
+import base64
+
+from tornado import web, escape
+
+from notebook.base.handlers import IPythonHandler
+
+class FilesHandler(IPythonHandler):
+ """serve files via ContentsManager"""
+
+ @web.authenticated
+ def get(self, path):
+ cm = self.contents_manager
+ if cm.is_hidden(path):
+ self.log.info("Refusing to serve hidden file, via 404 Error")
+ raise web.HTTPError(404)
+
+ path = path.strip('/')
+ if '/' in path:
+ _, name = path.rsplit('/', 1)
+ else:
+ name = path
+
+ model = cm.get(path, type='file')
+
+ if self.get_argument("download", False):
+ self.set_header('Content-Disposition','attachment; filename="%s"' % escape.url_escape(name))
+
+ # get mimetype from filename
+ if name.endswith('.ipynb'):
+ self.set_header('Content-Type', 'application/json')
+ else:
+ cur_mime = mimetypes.guess_type(name)[0]
+ if cur_mime is not None:
+ self.set_header('Content-Type', cur_mime)
+ else:
+ if model['format'] == 'base64':
+ self.set_header('Content-Type', 'application/octet-stream')
+ else:
+ self.set_header('Content-Type', 'text/plain')
+
+ if model['format'] == 'base64':
+ b64_bytes = model['content'].encode('ascii')
+ self.write(base64.decodestring(b64_bytes))
+ elif model['format'] == 'json':
+ self.write(json.dumps(model['content']))
+ else:
+ self.write(model['content'])
+ self.flush()
+
+default_handlers = [
+ (r"/files/(.*)", FilesHandler),
+] \ No newline at end of file
diff --git a/notebook/jstest.py b/notebook/jstest.py
new file mode 100644
index 0000000..054fdd9
--- /dev/null
+++ b/notebook/jstest.py
@@ -0,0 +1,635 @@
+# -*- coding: utf-8 -*-
+"""Notebook Javascript Test Controller
+
+This module runs one or more subprocesses which will actually run the Javascript
+test suite.
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from __future__ import absolute_import, print_function
+
+import argparse
+import json
+import multiprocessing.pool
+import os
+import re
+import requests
+import signal
+import sys
+import subprocess
+import time
+from io import BytesIO
+from threading import Thread, Lock, Event
+
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch # py3
+
+from jupyter_core.paths import jupyter_runtime_dir
+from ipython_genutils.py3compat import bytes_to_str, which
+from notebook._sysinfo import get_sys_info
+from ipython_genutils.tempdir import TemporaryDirectory
+
+try:
+ # Python >= 3.3
+ from subprocess import TimeoutExpired
+ def popen_wait(p, timeout):
+ return p.wait(timeout)
+except ImportError:
+ class TimeoutExpired(Exception):
+ pass
+ def popen_wait(p, timeout):
+ """backport of Popen.wait from Python 3"""
+ for i in range(int(10 * timeout)):
+ if p.poll() is not None:
+ return
+ time.sleep(0.1)
+ if p.poll() is None:
+ raise TimeoutExpired
+
+NOTEBOOK_SHUTDOWN_TIMEOUT = 10
+
+have = {}
+have['casperjs'] = bool(which('casperjs'))
+have['phantomjs'] = bool(which('phantomjs'))
+have['slimerjs'] = bool(which('slimerjs'))
+
+class StreamCapturer(Thread):
+ daemon = True # Don't hang if main thread crashes
+ started = False
+ def __init__(self, echo=False):
+ super(StreamCapturer, self).__init__()
+ self.echo = echo
+ self.streams = []
+ self.buffer = BytesIO()
+ self.readfd, self.writefd = os.pipe()
+ self.buffer_lock = Lock()
+ self.stop = Event()
+
+ def run(self):
+ self.started = True
+
+ while not self.stop.is_set():
+ chunk = os.read(self.readfd, 1024)
+
+ with self.buffer_lock:
+ self.buffer.write(chunk)
+ if self.echo:
+ sys.stdout.write(bytes_to_str(chunk))
+
+ os.close(self.readfd)
+ os.close(self.writefd)
+
+ def reset_buffer(self):
+ with self.buffer_lock:
+ self.buffer.truncate(0)
+ self.buffer.seek(0)
+
+ def get_buffer(self):
+ with self.buffer_lock:
+ return self.buffer.getvalue()
+
+ def ensure_started(self):
+ if not self.started:
+ self.start()
+
+ def halt(self):
+ """Safely stop the thread."""
+ if not self.started:
+ return
+
+ self.stop.set()
+ os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
+ self.join()
+
+
+class TestController(object):
+ """Run tests in a subprocess
+ """
+ #: str, test group to be executed.
+ section = None
+ #: list, command line arguments to be executed
+ cmd = None
+ #: dict, extra environment variables to set for the subprocess
+ env = None
+ #: list, TemporaryDirectory instances to clear up when the process finishes
+ dirs = None
+ #: subprocess.Popen instance
+ process = None
+ #: str, process stdout+stderr
+ stdout = None
+
+ def __init__(self):
+ self.cmd = []
+ self.env = {}
+ self.dirs = []
+
+ def setup(self):
+ """Create temporary directories etc.
+
+ This is only called when we know the test group will be run. Things
+ created here may be cleaned up by self.cleanup().
+ """
+ pass
+
+ def launch(self, buffer_output=False, capture_output=False):
+ # print('*** ENV:', self.env) # dbg
+ # print('*** CMD:', self.cmd) # dbg
+ env = os.environ.copy()
+ env.update(self.env)
+ if buffer_output:
+ capture_output = True
+ self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
+ c.start()
+ stdout = c.writefd if capture_output else None
+ stderr = subprocess.STDOUT if capture_output else None
+ self.process = subprocess.Popen(self.cmd, stdout=stdout,
+ stderr=stderr, env=env)
+
+ def wait(self):
+ self.process.wait()
+ self.stdout_capturer.halt()
+ self.stdout = self.stdout_capturer.get_buffer()
+ return self.process.returncode
+
+ def print_extra_info(self):
+ """Print extra information about this test run.
+
+ If we're running in parallel and showing the concise view, this is only
+ called if the test group fails. Otherwise, it's called before the test
+ group is started.
+
+ The base implementation does nothing, but it can be overridden by
+ subclasses.
+ """
+ return
+
+ def cleanup_process(self):
+ """Cleanup on exit by killing any leftover processes."""
+ subp = self.process
+ if subp is None or (subp.poll() is not None):
+ return # Process doesn't exist, or is already dead.
+
+ try:
+ print('Cleaning up stale PID: %d' % subp.pid)
+ subp.kill()
+ except: # (OSError, WindowsError) ?
+ # This is just a best effort, if we fail or the process was
+ # really gone, ignore it.
+ pass
+ else:
+ for i in range(10):
+ if subp.poll() is None:
+ time.sleep(0.1)
+ else:
+ break
+
+ if subp.poll() is None:
+ # The process did not die...
+ print('... failed. Manual cleanup may be required.')
+
+ def cleanup(self):
+ "Kill process if it's still alive, and clean up temporary directories"
+ self.cleanup_process()
+ for td in self.dirs:
+ td.cleanup()
+
+ __del__ = cleanup
+
+
+def get_js_test_dir():
+ import notebook.tests as t
+ return os.path.join(os.path.dirname(t.__file__), '')
+
+def all_js_groups():
+ import glob
+ test_dir = get_js_test_dir()
+ all_subdirs = glob.glob(test_dir + '[!_]*/')
+ return [os.path.relpath(x, test_dir) for x in all_subdirs]
+
+class JSController(TestController):
+ """Run CasperJS tests """
+
+ requirements = ['casperjs']
+
+ def __init__(self, section, xunit=True, engine='phantomjs', url=None):
+ """Create new test runner."""
+ TestController.__init__(self)
+ self.engine = engine
+ self.section = section
+ self.xunit = xunit
+ self.url = url
+ # run with a base URL that would be escaped,
+ # to test that we don't double-escape URLs
+ self.base_url = '/a@b/'
+ self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
+ js_test_dir = get_js_test_dir()
+ includes = '--includes=' + os.path.join(js_test_dir,'util.js')
+ test_cases = os.path.join(js_test_dir, self.section)
+ self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
+
+ def setup(self):
+ self.ipydir = TemporaryDirectory()
+ self.config_dir = TemporaryDirectory()
+ self.nbdir = TemporaryDirectory()
+ self.home = TemporaryDirectory()
+ self.env = {
+ 'HOME': self.home.name,
+ 'JUPYTER_CONFIG_DIR': self.config_dir.name,
+ 'IPYTHONDIR': self.ipydir.name,
+ }
+ self.dirs.append(self.ipydir)
+ self.dirs.append(self.home)
+ self.dirs.append(self.config_dir)
+ self.dirs.append(self.nbdir)
+ os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub ∂ir1', u'sub ∂ir 1a')))
+ os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub ∂ir2', u'sub ∂ir 1b')))
+
+ if self.xunit:
+ self.add_xunit()
+
+ # If a url was specified, use that for the testing.
+ if self.url:
+ try:
+ alive = requests.get(self.url).status_code == 200
+ except:
+ alive = False
+
+ if alive:
+ self.cmd.append("--url=%s" % self.url)
+ else:
+ raise Exception('Could not reach "%s".' % self.url)
+ else:
+ # start the ipython notebook, so we get the port number
+ self.server_port = 0
+ self._init_server()
+ if self.server_port:
+ self.cmd.append('--url=http://localhost:%i%s' % (self.server_port, self.base_url))
+ else:
+ # don't launch tests if the server didn't start
+ self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
+
+ def add_xunit(self):
+ xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
+ self.cmd.append('--xunit=%s' % xunit_file)
+
+ def launch(self, buffer_output):
+ # If the engine is SlimerJS, we need to buffer the output because
+ # SlimerJS does not support exit codes, so CasperJS always returns 0.
+ if self.engine == 'slimerjs' and not buffer_output:
+ return super(JSController, self).launch(capture_output=True)
+
+ else:
+ return super(JSController, self).launch(buffer_output=buffer_output)
+
+ def wait(self, *pargs, **kwargs):
+ """Wait for the JSController to finish"""
+ ret = super(JSController, self).wait(*pargs, **kwargs)
+ # If this is a SlimerJS controller, check the captured stdout for
+ # errors. Otherwise, just return the return code.
+ if self.engine == 'slimerjs':
+ stdout = bytes_to_str(self.stdout)
+ if ret != 0:
+ # This could still happen e.g. if it's stopped by SIGINT
+ return ret
+ return bool(self.slimer_failure.search(stdout))
+ else:
+ return ret
+
+ def print_extra_info(self):
+ print("Running tests with notebook directory %r" % self.nbdir.name)
+
+ @property
+ def will_run(self):
+ should_run = all(have[a] for a in self.requirements + [self.engine])
+ return should_run
+
+ def _init_server(self):
+ "Start the notebook server in a separate process"
+ self.server_command = command = [sys.executable,
+ '-m', 'notebook',
+ '--no-browser',
+ '--notebook-dir', self.nbdir.name,
+ '--NotebookApp.base_url=%s' % self.base_url,
+ ]
+ # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
+ # which run afoul of ipc's maximum path length.
+ if sys.platform.startswith('linux'):
+ command.append('--KernelManager.transport=ipc')
+ self.stream_capturer = c = StreamCapturer()
+ c.start()
+ env = os.environ.copy()
+ env.update(self.env)
+ if self.engine == 'phantomjs':
+ env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1'
+ self.server = subprocess.Popen(command,
+ stdout = c.writefd,
+ stderr = subprocess.STDOUT,
+ cwd=self.nbdir.name,
+ env=env,
+ )
+ with patch.dict('os.environ', {'HOME': self.home.name}):
+ runtime_dir = jupyter_runtime_dir()
+ self.server_info_file = os.path.join(runtime_dir,
+ 'nbserver-%i.json' % self.server.pid
+ )
+ self._wait_for_server()
+
+ def _wait_for_server(self):
+ """Wait 30 seconds for the notebook server to start"""
+ for i in range(300):
+ if self.server.poll() is not None:
+ return self._failed_to_start()
+ if os.path.exists(self.server_info_file):
+ try:
+ self._load_server_info()
+ except ValueError:
+ # If the server is halfway through writing the file, we may
+ # get invalid JSON; it should be ready next iteration.
+ pass
+ else:
+ return
+ time.sleep(0.1)
+ print("Notebook server-info file never arrived: %s" % self.server_info_file,
+ file=sys.stderr
+ )
+
+ def _failed_to_start(self):
+ """Notebook server exited prematurely"""
+ captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
+ print("Notebook failed to start: ", file=sys.stderr)
+ print(self.server_command)
+ print(captured, file=sys.stderr)
+
+ def _load_server_info(self):
+ """Notebook server started, load connection info from JSON"""
+ with open(self.server_info_file) as f:
+ info = json.load(f)
+ self.server_port = info['port']
+
+ def cleanup(self):
+ if hasattr(self, 'server'):
+ try:
+ self.server.terminate()
+ except OSError:
+ # already dead
+ pass
+ # wait 10s for the server to shutdown
+ try:
+ popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
+ except TimeoutExpired:
+ # server didn't terminate, kill it
+ try:
+ print("Failed to terminate notebook server, killing it.",
+ file=sys.stderr
+ )
+ self.server.kill()
+ except OSError:
+ # already dead
+ pass
+ # wait another 10s
+ try:
+ popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
+ except TimeoutExpired:
+ print("Notebook server still running (%s)" % self.server_info_file,
+ file=sys.stderr
+ )
+
+ self.stream_capturer.halt()
+ TestController.cleanup(self)
+
+
+def prepare_controllers(options):
+ """Returns two lists of TestController instances, those to run, and those
+ not to run."""
+ testgroups = options.testgroups
+ if not testgroups:
+ testgroups = all_js_groups()
+
+ engine = 'slimerjs' if options.slimerjs else 'phantomjs'
+ c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in testgroups]
+
+ controllers = c_js
+ to_run = [c for c in controllers if c.will_run]
+ not_run = [c for c in controllers if not c.will_run]
+ return to_run, not_run
+
+def do_run(controller, buffer_output=True):
+ """Setup and run a test controller.
+
+ If buffer_output is True, no output is displayed, to avoid it appearing
+ interleaved. In this case, the caller is responsible for displaying test
+ output on failure.
+
+ Returns
+ -------
+ controller : TestController
+ The same controller as passed in, as a convenience for using map() type
+ APIs.
+ exitcode : int
+ The exit code of the test subprocess. Non-zero indicates failure.
+ """
+ try:
+ try:
+ controller.setup()
+ if not buffer_output:
+ controller.print_extra_info()
+ controller.launch(buffer_output=buffer_output)
+ except Exception:
+ import traceback
+ traceback.print_exc()
+ return controller, 1 # signal failure
+
+ exitcode = controller.wait()
+ return controller, exitcode
+
+ except KeyboardInterrupt:
+ return controller, -signal.SIGINT
+ finally:
+ controller.cleanup()
+
+def report():
+ """Return a string with a summary report of test-related variables."""
+ inf = get_sys_info()
+ out = []
+ def _add(name, value):
+ out.append((name, value))
+
+ _add('Python version', inf['sys_version'].replace('\n',''))
+ _add('sys.executable', inf['sys_executable'])
+ _add('Platform', inf['platform'])
+
+ width = max(len(n) for (n,v) in out)
+ out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
+
+ avail = []
+ not_avail = []
+
+ for k, is_avail in have.items():
+ if is_avail:
+ avail.append(k)
+ else:
+ not_avail.append(k)
+
+ if avail:
+ out.append('\nTools and libraries available at test time:\n')
+ avail.sort()
+ out.append(' ' + ' '.join(avail)+'\n')
+
+ if not_avail:
+ out.append('\nTools and libraries NOT available at test time:\n')
+ not_avail.sort()
+ out.append(' ' + ' '.join(not_avail)+'\n')
+
+ return ''.join(out)
+
+def run_jstestall(options):
+ """Run the entire Javascript test suite.
+
+ This function constructs TestControllers and runs them in subprocesses.
+
+ Parameters
+ ----------
+
+ All parameters are passed as attributes of the options object.
+
+ testgroups : list of str
+ Run only these sections of the test suite. If empty, run all the available
+ sections.
+
+ fast : int or None
+ Run the test suite in parallel, using n simultaneous processes. If None
+ is passed, one process is used per CPU core. Default 1 (i.e. sequential)
+
+ inc_slow : bool
+ Include slow tests. By default, these tests aren't run.
+
+ slimerjs : bool
+ Use slimerjs if it's installed instead of phantomjs for casperjs tests.
+
+ url : unicode
+ Address:port to use when running the JS tests.
+
+ xunit : bool
+ Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
+
+ extra_args : list
+ Extra arguments to pass to the test subprocesses, e.g. '-v'
+ """
+ to_run, not_run = prepare_controllers(options)
+
+ def justify(ltext, rtext, width=70, fill='-'):
+ ltext += ' '
+ rtext = (' ' + rtext).rjust(width - len(ltext), fill)
+ return ltext + rtext
+
+ # Run all test runners, tracking execution time
+ failed = []
+ t_start = time.time()
+
+ print()
+ if options.fast == 1:
+ # This actually means sequential, i.e. with 1 job
+ for controller in to_run:
+ print('Test group:', controller.section)
+ sys.stdout.flush() # Show in correct order when output is piped
+ controller, res = do_run(controller, buffer_output=False)
+ if res:
+ failed.append(controller)
+ if res == -signal.SIGINT:
+ print("Interrupted")
+ break
+ print()
+
+ else:
+ # Run tests concurrently
+ try:
+ pool = multiprocessing.pool.ThreadPool(options.fast)
+ for (controller, res) in pool.imap_unordered(do_run, to_run):
+ res_string = 'OK' if res == 0 else 'FAILED'
+ print(justify('Test group: ' + controller.section, res_string))
+ if res:
+ controller.print_extra_info()
+ print(bytes_to_str(controller.stdout))
+ failed.append(controller)
+ if res == -signal.SIGINT:
+ print("Interrupted")
+ break
+ except KeyboardInterrupt:
+ return
+
+ for controller in not_run:
+ print(justify('Test group: ' + controller.section, 'NOT RUN'))
+
+ t_end = time.time()
+ t_tests = t_end - t_start
+ nrunners = len(to_run)
+ nfail = len(failed)
+ # summarize results
+ print('_'*70)
+ print('Test suite completed for system with the following information:')
+ print(report())
+ took = "Took %.3fs." % t_tests
+ print('Status: ', end='')
+ if not failed:
+ print('OK (%d test groups).' % nrunners, took)
+ else:
+ # If anything went wrong, point out what command to rerun manually to
+ # see the actual errors and individual summary
+ failed_sections = [c.section for c in failed]
+ print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
+ nrunners, ', '.join(failed_sections)), took)
+ print()
+ print('You may wish to rerun these, with:')
+ print(' python -m notebook.jstest', *failed_sections)
+ print()
+
+ if failed:
+ # Ensure that our exit code indicates failure
+ sys.exit(1)
+
+argparser = argparse.ArgumentParser(description='Run Jupyter Notebook Javascript tests')
+argparser.add_argument('testgroups', nargs='*',
+ help='Run specified groups of tests. If omitted, run '
+ 'all tests.')
+argparser.add_argument('--slimerjs', action='store_true',
+ help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
+argparser.add_argument('--url', help="URL to use for the JS tests.")
+argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
+ help='Run test sections in parallel. This starts as many '
+ 'processes as you have cores, or you can specify a number.')
+argparser.add_argument('--xunit', action='store_true',
+ help='Produce Xunit XML results')
+argparser.add_argument('--subproc-streams', default='capture',
+ help="What to do with stdout/stderr from subprocesses. "
+ "'capture' (default), 'show' and 'discard' are the options.")
+
+def default_options():
+ """Get an argparse Namespace object with the default arguments, to pass to
+ :func:`run_iptestall`.
+ """
+ options = argparser.parse_args([])
+ options.extra_args = []
+ return options
+
+def main():
+ try:
+ ix = sys.argv.index('--')
+ except ValueError:
+ to_parse = sys.argv[1:]
+ extra_args = []
+ else:
+ to_parse = sys.argv[1:ix]
+ extra_args = sys.argv[ix+1:]
+
+ options = argparser.parse_args(to_parse)
+ options.extra_args = extra_args
+
+ run_jstestall(options)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/notebook/kernelspecs/__init__.py b/notebook/kernelspecs/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/kernelspecs/__init__.py
diff --git a/notebook/kernelspecs/handlers.py b/notebook/kernelspecs/handlers.py
new file mode 100644
index 0000000..9ec642a
--- /dev/null
+++ b/notebook/kernelspecs/handlers.py
@@ -0,0 +1,27 @@
+from tornado import web
+from ..base.handlers import IPythonHandler
+from ..services.kernelspecs.handlers import kernel_name_regex
+
+class KernelSpecResourceHandler(web.StaticFileHandler, IPythonHandler):
+ SUPPORTED_METHODS = ('GET', 'HEAD')
+
+ def initialize(self):
+ web.StaticFileHandler.initialize(self, path='')
+
+ @web.authenticated
+ def get(self, kernel_name, path, include_body=True):
+ ksm = self.kernel_spec_manager
+ try:
+ self.root = ksm.get_kernel_spec(kernel_name).resource_dir
+ except KeyError:
+ raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name)
+ self.log.debug("Serving kernel resource from: %s", self.root)
+ return web.StaticFileHandler.get(self, path, include_body=include_body)
+
+ @web.authenticated
+ def head(self, kernel_name, path):
+ return self.get(kernel_name, path, include_body=False)
+
+default_handlers = [
+ (r"/kernelspecs/%s/(?P<path>.*)" % kernel_name_regex, KernelSpecResourceHandler),
+] \ No newline at end of file
diff --git a/notebook/log.py b/notebook/log.py
new file mode 100644
index 0000000..dab330c
--- /dev/null
+++ b/notebook/log.py
@@ -0,0 +1,48 @@
+#-----------------------------------------------------------------------------
+# Copyright (c) Jupyter Development Team
+#
+# Distributed under the terms of the BSD License. The full license is in
+# the file COPYING, distributed as part of this software.
+#-----------------------------------------------------------------------------
+
+import json
+from tornado.log import access_log
+
+def log_request(handler):
+ """log a bit more information about each request than tornado's default
+
+ - move static file get success to debug-level (reduces noise)
+ - get proxied IP instead of proxy IP
+ - log referer for redirect and failed requests
+ - log user-agent for failed requests
+ """
+ status = handler.get_status()
+ request = handler.request
+ if status < 300 or status == 304:
+ # Successes (or 304 FOUND) are debug-level
+ log_method = access_log.debug
+ elif status < 400:
+ log_method = access_log.info
+ elif status < 500:
+ log_method = access_log.warning
+ else:
+ log_method = access_log.error
+
+ request_time = 1000.0 * handler.request.request_time()
+ ns = dict(
+ status=status,
+ method=request.method,
+ ip=request.remote_ip,
+ uri=request.uri,
+ request_time=request_time,
+ )
+ msg = "{status} {method} {uri} ({ip}) {request_time:.2f}ms"
+ if status >= 400:
+ # log bad referers
+ ns['referer'] = request.headers.get('Referer', 'None')
+ msg = msg + ' referer={referer}'
+ if status >= 500 and status != 502:
+ # log all headers if it caused an error
+ log_method(json.dumps(dict(request.headers), indent=2))
+ log_method(msg.format(**ns))
+
diff --git a/notebook/nbconvert/__init__.py b/notebook/nbconvert/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/nbconvert/__init__.py
diff --git a/notebook/nbconvert/handlers.py b/notebook/nbconvert/handlers.py
new file mode 100644
index 0000000..4dd5352
--- /dev/null
+++ b/notebook/nbconvert/handlers.py
@@ -0,0 +1,168 @@
+"""Tornado handlers for nbconvert."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import io
+import os
+import zipfile
+
+from tornado import web, escape
+from tornado.log import app_log
+
+from ..base.handlers import (
+ IPythonHandler, FilesRedirectHandler,
+ path_regex,
+)
+from nbformat import from_dict
+
+from ipython_genutils.py3compat import cast_bytes
+from ipython_genutils import text
+
+def find_resource_files(output_files_dir):
+ files = []
+ for dirpath, dirnames, filenames in os.walk(output_files_dir):
+ files.extend([os.path.join(dirpath, f) for f in filenames])
+ return files
+
+def respond_zip(handler, name, output, resources):
+ """Zip up the output and resource files and respond with the zip file.
+
+ Returns True if it has served a zip file, False if there are no resource
+ files, in which case we serve the plain output file.
+ """
+ # Check if we have resource files we need to zip
+ output_files = resources.get('outputs', None)
+ if not output_files:
+ return False
+
+ # Headers
+ zip_filename = os.path.splitext(name)[0] + '.zip'
+ handler.set_header('Content-Disposition',
+ 'attachment; filename="%s"' % escape.url_escape(zip_filename))
+ handler.set_header('Content-Type', 'application/zip')
+
+ # Prepare the zip file
+ buffer = io.BytesIO()
+ zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
+ output_filename = os.path.splitext(name)[0] + resources['output_extension']
+ zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
+ for filename, data in output_files.items():
+ zipf.writestr(os.path.basename(filename), data)
+ zipf.close()
+
+ handler.finish(buffer.getvalue())
+ return True
+
+def get_exporter(format, **kwargs):
+ """get an exporter, raising appropriate errors"""
+ # if this fails, will raise 500
+ try:
+ from nbconvert.exporters.export import exporter_map
+ except ImportError as e:
+ raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
+
+ try:
+ Exporter = exporter_map[format]
+ except KeyError:
+ # should this be 400?
+ raise web.HTTPError(404, u"No exporter for format: %s" % format)
+
+ try:
+ return Exporter(**kwargs)
+ except Exception as e:
+ app_log.exception("Could not construct Exporter: %s", Exporter)
+ raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
+
+class NbconvertFileHandler(IPythonHandler):
+
+ SUPPORTED_METHODS = ('GET',)
+
+ @web.authenticated
+ def get(self, format, path):
+
+ exporter = get_exporter(format, config=self.config, log=self.log)
+
+ path = path.strip('/')
+ model = self.contents_manager.get(path=path)
+ name = model['name']
+ if model['type'] != 'notebook':
+ # not a notebook, redirect to files
+ return FilesRedirectHandler.redirect_to_files(self, path)
+
+ self.set_header('Last-Modified', model['last_modified'])
+
+ try:
+ output, resources = exporter.from_notebook_node(
+ model['content'],
+ resources={
+ "metadata": {
+ "name": name[:name.rfind('.')],
+ "modified_date": (model['last_modified']
+ .strftime(text.date_format))
+ },
+ "config_dir": self.application.settings['config_dir'],
+ }
+ )
+ except Exception as e:
+ self.log.exception("nbconvert failed: %s", e)
+ raise web.HTTPError(500, "nbconvert failed: %s" % e)
+
+ if respond_zip(self, name, output, resources):
+ return
+
+ # Force download if requested
+ if self.get_argument('download', 'false').lower() == 'true':
+ filename = os.path.splitext(name)[0] + resources['output_extension']
+ self.set_header('Content-Disposition',
+ 'attachment; filename="%s"' % escape.url_escape(filename))
+
+ # MIME type
+ if exporter.output_mimetype:
+ self.set_header('Content-Type',
+ '%s; charset=utf-8' % exporter.output_mimetype)
+
+ self.finish(output)
+
+class NbconvertPostHandler(IPythonHandler):
+ SUPPORTED_METHODS = ('POST',)
+
+ @web.authenticated
+ def post(self, format):
+ exporter = get_exporter(format, config=self.config)
+
+ model = self.get_json_body()
+ name = model.get('name', 'notebook.ipynb')
+ nbnode = from_dict(model['content'])
+
+ try:
+ output, resources = exporter.from_notebook_node(nbnode, resources={
+ "metadata": {"name": name[:name.rfind('.')],},
+ "config_dir": self.application.settings['config_dir'],
+ })
+ except Exception as e:
+ raise web.HTTPError(500, "nbconvert failed: %s" % e)
+
+ if respond_zip(self, name, output, resources):
+ return
+
+ # MIME type
+ if exporter.output_mimetype:
+ self.set_header('Content-Type',
+ '%s; charset=utf-8' % exporter.output_mimetype)
+
+ self.finish(output)
+
+
+#-----------------------------------------------------------------------------
+# URL to handler mappings
+#-----------------------------------------------------------------------------
+
+_format_regex = r"(?P<format>\w+)"
+
+
+default_handlers = [
+ (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
+ (r"/nbconvert/%s%s" % (_format_regex, path_regex),
+ NbconvertFileHandler),
+]
diff --git a/notebook/nbconvert/tests/__init__.py b/notebook/nbconvert/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/nbconvert/tests/__init__.py
diff --git a/notebook/nbconvert/tests/test_nbconvert_handlers.py b/notebook/nbconvert/tests/test_nbconvert_handlers.py
new file mode 100644
index 0000000..b724009
--- /dev/null
+++ b/notebook/nbconvert/tests/test_nbconvert_handlers.py
@@ -0,0 +1,132 @@
+# coding: utf-8
+import base64
+import io
+import json
+import os
+from os.path import join as pjoin
+import shutil
+
+import requests
+
+from notebook.utils import url_path_join
+from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
+from nbformat import write
+from nbformat.v4 import (
+ new_notebook, new_markdown_cell, new_code_cell, new_output,
+)
+
+from ipython_genutils.testing.decorators import onlyif_cmds_exist
+
+
+class NbconvertAPI(object):
+ """Wrapper for nbconvert API calls."""
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def _req(self, verb, path, body=None, params=None):
+ response = requests.request(verb,
+ url_path_join(self.base_url, 'nbconvert', path),
+ data=body, params=params,
+ )
+ response.raise_for_status()
+ return response
+
+ def from_file(self, format, path, name, download=False):
+ return self._req('GET', url_path_join(format, path, name),
+ params={'download':download})
+
+ def from_post(self, format, nbmodel):
+ body = json.dumps(nbmodel)
+ return self._req('POST', format, body)
+
+ def list_formats(self):
+ return self._req('GET', '')
+
+png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
+b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
+b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82'
+).decode('ascii')
+
+class APITest(NotebookTestBase):
+ def setUp(self):
+ nbdir = self.notebook_dir.name
+
+ if not os.path.isdir(pjoin(nbdir, 'foo')):
+ os.mkdir(pjoin(nbdir, 'foo'))
+
+ nb = new_notebook()
+
+ nb.cells.append(new_markdown_cell(u'Created by test ³'))
+ cc1 = new_code_cell(source=u'print(2*6)')
+ cc1.outputs.append(new_output(output_type="stream", text=u'12'))
+ cc1.outputs.append(new_output(output_type="execute_result",
+ data={'image/png' : png_green_pixel},
+ execution_count=1,
+ ))
+ nb.cells.append(cc1)
+
+ with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
+ encoding='utf-8') as f:
+ write(nb, f, version=4)
+
+ self.nbconvert_api = NbconvertAPI(self.base_url())
+
+ def tearDown(self):
+ nbdir = self.notebook_dir.name
+
+ for dname in ['foo']:
+ shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
+
+ @onlyif_cmds_exist('pandoc')
+ def test_from_file(self):
+ r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
+ self.assertEqual(r.status_code, 200)
+ self.assertIn(u'text/html', r.headers['Content-Type'])
+ self.assertIn(u'Created by test', r.text)
+ self.assertIn(u'print', r.text)
+
+ r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
+ self.assertIn(u'text/x-python', r.headers['Content-Type'])
+ self.assertIn(u'print(2*6)', r.text)
+
+ @onlyif_cmds_exist('pandoc')
+ def test_from_file_404(self):
+ with assert_http_error(404):
+ self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
+
+ @onlyif_cmds_exist('pandoc')
+ def test_from_file_download(self):
+ r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
+ content_disposition = r.headers['Content-Disposition']
+ self.assertIn('attachment', content_disposition)
+ self.assertIn('testnb.py', content_disposition)
+
+ @onlyif_cmds_exist('pandoc')
+ def test_from_file_zip(self):
+ r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
+ self.assertIn(u'application/zip', r.headers['Content-Type'])
+ self.assertIn(u'.zip', r.headers['Content-Disposition'])
+
+ @onlyif_cmds_exist('pandoc')
+ def test_from_post(self):
+ nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
+ nbmodel = requests.get(nbmodel_url).json()
+
+ r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
+ self.assertEqual(r.status_code, 200)
+ self.assertIn(u'text/html', r.headers['Content-Type'])
+ self.assertIn(u'Created by test', r.text)
+ self.assertIn(u'print', r.text)
+
+ r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
+ self.assertIn(u'text/x-python', r.headers['Content-Type'])
+ self.assertIn(u'print(2*6)', r.text)
+
+ @onlyif_cmds_exist('pandoc')
+ def test_from_post_zip(self):
+ nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
+ nbmodel = requests.get(nbmodel_url).json()
+
+ r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
+ self.assertIn(u'application/zip', r.headers['Content-Type'])
+ self.assertIn(u'.zip', r.headers['Content-Disposition'])
diff --git a/notebook/nbextensions.py b/notebook/nbextensions.py
new file mode 100644
index 0000000..8d987d3
--- /dev/null
+++ b/notebook/nbextensions.py
@@ -0,0 +1,1176 @@
+# coding: utf-8
+"""Utilities for installing Javascript extensions for the notebook"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from __future__ import print_function
+
+import os
+import shutil
+import sys
+import tarfile
+import zipfile
+from os.path import basename, join as pjoin, normpath
+
+try:
+ from urllib.parse import urlparse # Py3
+ from urllib.request import urlretrieve
+except ImportError:
+ from urlparse import urlparse
+ from urllib import urlretrieve
+
+from jupyter_core.paths import (
+ jupyter_data_dir, jupyter_config_dir, jupyter_config_path,
+ SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH, ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH
+)
+from ipython_genutils.path import ensure_dir_exists
+from ipython_genutils.py3compat import string_types, cast_unicode_py2
+from ipython_genutils.tempdir import TemporaryDirectory
+from ._version import __version__
+
+from traitlets.config.manager import BaseJSONConfigManager
+from traitlets.utils.importstring import import_item
+
+from tornado.log import LogFormatter
+
+# Constants for pretty print extension listing function.
+# Window doesn't support coloring in the commandline
+GREEN_ENABLED = '\033[32m enabled \033[0m' if os.name != 'nt' else 'enabled '
+RED_DISABLED = '\033[31mdisabled\033[0m' if os.name != 'nt' else 'disabled'
+
+DEPRECATED_ARGUMENT = object()
+
+NBCONFIG_SECTIONS = ['common', 'notebook', 'tree', 'edit', 'terminal']
+
+GREEN_OK = '\033[32mOK\033[0m' if os.name != 'nt' else 'ok'
+RED_X = '\033[31m X\033[0m' if os.name != 'nt' else ' X'
+
+#------------------------------------------------------------------------------
+# Public API
+#------------------------------------------------------------------------------
+
+
+class ArgumentConflict(ValueError):
+ pass
+
+
+def check_nbextension(files, user=False, prefix=None, nbextensions_dir=None, sys_prefix=False):
+ """Check whether nbextension files have been installed
+
+ Returns True if all files are found, False if any are missing.
+
+ Parameters
+ ----------
+
+ files : list(paths)
+ a list of relative paths within nbextensions.
+ user : bool [default: False]
+ Whether to check the user's .jupyter/nbextensions directory.
+ Otherwise check a system-wide install (e.g. /usr/local/share/jupyter/nbextensions).
+ prefix : str [optional]
+ Specify install prefix, if it should differ from default (e.g. /usr/local).
+ Will check prefix/share/jupyter/nbextensions
+ nbextensions_dir : str [optional]
+ Specify absolute path of nbextensions directory explicitly.
+ sys_prefix : bool [default: False]
+ Install into the sys.prefix, i.e. environment
+ """
+ nbext = _get_nbextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir)
+ # make sure nbextensions dir exists
+ if not os.path.exists(nbext):
+ return False
+
+ if isinstance(files, string_types):
+ # one file given, turn it into a list
+ files = [files]
+
+ return all(os.path.exists(pjoin(nbext, f)) for f in files)
+
+
+def install_nbextension(path, overwrite=False, symlink=False,
+ user=False, prefix=None, nbextensions_dir=None,
+ destination=None, verbose=DEPRECATED_ARGUMENT,
+ logger=None, sys_prefix=False
+ ):
+ """Install a Javascript extension for the notebook
+
+ Stages files and/or directories into the nbextensions directory.
+ By default, this compares modification time, and only stages files that need updating.
+ If `overwrite` is specified, matching files are purged before proceeding.
+
+ Parameters
+ ----------
+
+ path : path to file, directory, zip or tarball archive, or URL to install
+ By default, the file will be installed with its base name, so '/path/to/foo'
+ will install to 'nbextensions/foo'. See the destination argument below to change this.
+ Archives (zip or tarballs) will be extracted into the nbextensions directory.
+ overwrite : bool [default: False]
+ If True, always install the files, regardless of what may already be installed.
+ symlink : bool [default: False]
+ If True, create a symlink in nbextensions, rather than copying files.
+ Not allowed with URLs or archives. Windows support for symlinks requires
+ Vista or above, Python 3, and a permission bit which only admin users
+ have by default, so don't rely on it.
+ user : bool [default: False]
+ Whether to install to the user's nbextensions directory.
+ Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/nbextensions).
+ prefix : str [optional]
+ Specify install prefix, if it should differ from default (e.g. /usr/local).
+ Will install to ``<prefix>/share/jupyter/nbextensions``
+ nbextensions_dir : str [optional]
+ Specify absolute path of nbextensions directory explicitly.
+ destination : str [optional]
+ name the nbextension is installed to. For example, if destination is 'foo', then
+ the source file will be installed to 'nbextensions/foo', regardless of the source name.
+ This cannot be specified if an archive is given as the source.
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ if verbose != DEPRECATED_ARGUMENT:
+ import warnings
+ warnings.warn("`install_nbextension`'s `verbose` parameter is deprecated, it will have no effects and will be removed in Notebook 5.0", DeprecationWarning)
+
+ # the actual path to which we eventually installed
+ full_dest = None
+
+ nbext = _get_nbextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir)
+ # make sure nbextensions dir exists
+ ensure_dir_exists(nbext)
+
+ # forcing symlink parameter to False if os.symlink does not exist (e.g., on Windows machines running python 2)
+ if not hasattr(os, 'symlink'):
+ symlink = False
+
+ if isinstance(path, (list, tuple)):
+ raise TypeError("path must be a string pointing to a single extension to install; call this function multiple times to install multiple extensions")
+
+ path = cast_unicode_py2(path)
+
+ if path.startswith(('https://', 'http://')):
+ if symlink:
+ raise ValueError("Cannot symlink from URLs")
+ # Given a URL, download it
+ with TemporaryDirectory() as td:
+ filename = urlparse(path).path.split('/')[-1]
+ local_path = os.path.join(td, filename)
+ if logger:
+ logger.info("Downloading: %s -> %s" % (path, local_path))
+ urlretrieve(path, local_path)
+ # now install from the local copy
+ full_dest = install_nbextension(local_path, overwrite=overwrite, symlink=symlink,
+ nbextensions_dir=nbext, destination=destination, logger=logger)
+ elif path.endswith('.zip') or _safe_is_tarfile(path):
+ if symlink:
+ raise ValueError("Cannot symlink from archives")
+ if destination:
+ raise ValueError("Cannot give destination for archives")
+ if logger:
+ logger.info("Extracting: %s -> %s" % (path, nbext))
+
+ if path.endswith('.zip'):
+ archive = zipfile.ZipFile(path)
+ elif _safe_is_tarfile(path):
+ archive = tarfile.open(path)
+ archive.extractall(nbext)
+ archive.close()
+ # TODO: what to do here
+ full_dest = None
+ else:
+ if not destination:
+ destination = basename(path)
+ destination = cast_unicode_py2(destination)
+ full_dest = normpath(pjoin(nbext, destination))
+ if overwrite and os.path.lexists(full_dest):
+ if logger:
+ logger.info("Removing: %s" % full_dest)
+ if os.path.isdir(full_dest) and not os.path.islink(full_dest):
+ shutil.rmtree(full_dest)
+ else:
+ os.remove(full_dest)
+
+ if symlink:
+ path = os.path.abspath(path)
+ if not os.path.exists(full_dest):
+ if logger:
+ logger.info("Symlinking: %s -> %s" % (full_dest, path))
+ os.symlink(path, full_dest)
+ elif os.path.isdir(path):
+ path = pjoin(os.path.abspath(path), '') # end in path separator
+ for parent, dirs, files in os.walk(path):
+ dest_dir = pjoin(full_dest, parent[len(path):])
+ if not os.path.exists(dest_dir):
+ if logger:
+ logger.info("Making directory: %s" % dest_dir)
+ os.makedirs(dest_dir)
+ for file in files:
+ src = pjoin(parent, file)
+ dest_file = pjoin(dest_dir, file)
+ _maybe_copy(src, dest_file, logger=logger)
+ else:
+ src = path
+ _maybe_copy(src, full_dest, logger=logger)
+
+ return full_dest
+
+
+def install_nbextension_python(module, overwrite=False, symlink=False,
+ user=False, sys_prefix=False, prefix=None, nbextensions_dir=None, logger=None):
+ """Install an nbextension bundled in a Python package.
+
+ Returns a list of installed/updated directories.
+
+ See install_nbextension for parameter information."""
+ m, nbexts = _get_nbextension_metadata(module)
+ base_path = os.path.split(m.__file__)[0]
+
+ full_dests = []
+
+ for nbext in nbexts:
+ src = os.path.join(base_path, nbext['src'])
+ dest = nbext['dest']
+
+ if logger:
+ logger.info("Installing %s -> %s" % (src, dest))
+ full_dest = install_nbextension(
+ src, overwrite=overwrite, symlink=symlink,
+ user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir,
+ destination=dest, logger=logger
+ )
+ validate_nbextension_python(nbext, full_dest, logger)
+ full_dests.append(full_dest)
+
+ return full_dests
+
+
+def uninstall_nbextension(dest, require=None, user=False, sys_prefix=False, prefix=None,
+ nbextensions_dir=None, logger=None):
+ """Uninstall a Javascript extension of the notebook
+
+ Removes staged files and/or directories in the nbextensions directory and
+ removes the extension from the frontend config.
+
+ Parameters
+ ----------
+
+ dest : str
+ path to file, directory, zip or tarball archive, or URL to install
+ name the nbextension is installed to. For example, if destination is 'foo', then
+ the source file will be installed to 'nbextensions/foo', regardless of the source name.
+ This cannot be specified if an archive is given as the source.
+ require : str [optional]
+ require.js path used to load the extension.
+ If specified, frontend config loading extension will be removed.
+ user : bool [default: False]
+ Whether to install to the user's nbextensions directory.
+ Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/nbextensions).
+ prefix : str [optional]
+ Specify install prefix, if it should differ from default (e.g. /usr/local).
+ Will install to ``<prefix>/share/jupyter/nbextensions``
+ nbextensions_dir : str [optional]
+ Specify absolute path of nbextensions directory explicitly.
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ nbext = _get_nbextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir)
+ dest = cast_unicode_py2(dest)
+ full_dest = pjoin(nbext, dest)
+ if os.path.lexists(full_dest):
+ if logger:
+ logger.info("Removing: %s" % full_dest)
+ if os.path.isdir(full_dest) and not os.path.islink(full_dest):
+ shutil.rmtree(full_dest)
+ else:
+ os.remove(full_dest)
+
+ # Look through all of the config sections making sure that the nbextension
+ # doesn't exist.
+ config_dir = os.path.join(_get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig')
+ cm = BaseJSONConfigManager(config_dir=config_dir)
+ if require:
+ for section in NBCONFIG_SECTIONS:
+ cm.update(section, {"load_extensions": {require: None}})
+
+
+def uninstall_nbextension_python(module,
+ user=False, sys_prefix=False, prefix=None, nbextensions_dir=None,
+ logger=None):
+ """Uninstall an nbextension bundled in a Python package.
+
+ See parameters of `install_nbextension_python`
+ """
+ m, nbexts = _get_nbextension_metadata(module)
+ for nbext in nbexts:
+ dest = nbext['dest']
+ require = nbext['require']
+ if logger:
+ logger.info("Uninstalling {} {}".format(dest, require))
+ uninstall_nbextension(dest, require, user=user, sys_prefix=sys_prefix,
+ prefix=prefix, nbextensions_dir=nbextensions_dir, logger=logger)
+
+
+def _set_nbextension_state(section, require, state,
+ user=True, sys_prefix=False, logger=None):
+ """Set whether the section's frontend should require the named nbextension
+
+ Returns True if the final state is the one requested.
+
+ Parameters
+ ----------
+ section : string
+ The section of the server to change, one of NBCONFIG_SECTIONS
+ require : string
+ An importable AMD module inside the nbextensions static path
+ state : bool
+ The state in which to leave the extension
+ user : bool [default: True]
+ Whether to update the user's .jupyter/nbextensions directory
+ sys_prefix : bool [default: False]
+ Whether to update the sys.prefix, i.e. environment. Will override
+ `user`.
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ user = False if sys_prefix else user
+ config_dir = os.path.join(
+ _get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig')
+ cm = BaseJSONConfigManager(config_dir=config_dir)
+ if logger:
+ logger.info("{} {} extension {}...".format(
+ "Enabling" if state else "Disabling",
+ section,
+ require
+ ))
+ cm.update(section, {"load_extensions": {require: state}})
+
+ validate_nbextension(require, logger=logger)
+
+ return cm.get(section).get(require) == state
+
+
+def _set_nbextension_state_python(state, module, user, sys_prefix,
+ logger=None):
+ """Enable or disable some nbextensions stored in a Python package
+
+ Returns a list of whether the state was achieved (i.e. changed, or was
+ already right)
+
+ Parameters
+ ----------
+
+ state : Bool
+ Whether the extensions should be enabled
+ module : str
+ Importable Python module exposing the
+ magic-named `_jupyter_nbextension_paths` function
+ user : bool
+ Whether to enable in the user's nbextensions directory.
+ sys_prefix : bool
+ Enable/disable in the sys.prefix, i.e. environment
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ m, nbexts = _get_nbextension_metadata(module)
+ return [_set_nbextension_state(section=nbext["section"],
+ require=nbext["require"],
+ state=state,
+ user=user, sys_prefix=sys_prefix,
+ logger=logger)
+ for nbext in nbexts]
+
+
+def enable_nbextension(section, require, user=True, sys_prefix=False,
+ logger=None):
+ """Enable a named nbextension
+
+ Returns True if the final state is the one requested.
+
+ Parameters
+ ----------
+
+ section : string
+ The section of the server to change, one of NBCONFIG_SECTIONS
+ require : string
+ An importable AMD module inside the nbextensions static path
+ user : bool [default: True]
+ Whether to enable in the user's nbextensions directory.
+ sys_prefix : bool [default: False]
+ Whether to enable in the sys.prefix, i.e. environment. Will override
+ `user`
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ return _set_nbextension_state(section=section, require=require,
+ state=True,
+ user=user, sys_prefix=sys_prefix,
+ logger=logger)
+
+
+def disable_nbextension(section, require, user=True, sys_prefix=False,
+ logger=None):
+ """Disable a named nbextension
+
+ Returns True if the final state is the one requested.
+
+ Parameters
+ ----------
+
+ section : string
+ The section of the server to change, one of NBCONFIG_SECTIONS
+ require : string
+ An importable AMD module inside the nbextensions static path
+ user : bool [default: True]
+ Whether to enable in the user's nbextensions directory.
+ sys_prefix : bool [default: False]
+ Whether to enable in the sys.prefix, i.e. environment. Will override
+ `user`.
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ return _set_nbextension_state(section=section, require=require,
+ state=False,
+ user=user, sys_prefix=sys_prefix,
+ logger=logger)
+
+
+def enable_nbextension_python(module, user=True, sys_prefix=False,
+ logger=None):
+ """Enable some nbextensions associated with a Python module.
+
+ Returns a list of whether the state was achieved (i.e. changed, or was
+ already right)
+
+ Parameters
+ ----------
+
+ module : str
+ Importable Python module exposing the
+ magic-named `_jupyter_nbextension_paths` function
+ user : bool [default: True]
+ Whether to enable in the user's nbextensions directory.
+ sys_prefix : bool [default: False]
+ Whether to enable in the sys.prefix, i.e. environment. Will override
+ `user`
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ return _set_nbextension_state_python(True, module, user, sys_prefix,
+ logger=logger)
+
+
+def disable_nbextension_python(module, user=True, sys_prefix=False,
+ logger=None):
+ """Disable some nbextensions associated with a Python module.
+
+ Returns True if the final state is the one requested.
+
+ Parameters
+ ----------
+
+ module : str
+ Importable Python module exposing the
+ magic-named `_jupyter_nbextension_paths` function
+ user : bool [default: True]
+ Whether to enable in the user's nbextensions directory.
+ sys_prefix : bool [default: False]
+ Whether to enable in the sys.prefix, i.e. environment
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ return _set_nbextension_state_python(False, module, user, sys_prefix,
+ logger=logger)
+
+
+def validate_nbextension(require, logger=None):
+ """Validate a named nbextension.
+
+ Looks across all of the nbextension directories.
+
+ Returns a list of warnings.
+
+ require : str
+ require.js path used to load the extension
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ warnings = []
+ infos = []
+
+ js_exists = False
+ for exts in _nbextension_dirs():
+ # Does the Javascript entrypoint actually exist on disk?
+ js = u"{}.js".format(os.path.join(exts, *require.split("/")))
+ js_exists = os.path.exists(js)
+ if js_exists:
+ break
+
+ require_tmpl = u" - require? {} {}"
+ if js_exists:
+ infos.append(require_tmpl.format(GREEN_OK, require))
+ else:
+ warnings.append(require_tmpl.format(RED_X, require))
+
+ if logger:
+ if warnings:
+ logger.warning(u" - Validating: problems found:")
+ for msg in warnings:
+ logger.warning(msg)
+ for msg in infos:
+ logger.info(msg)
+ else:
+ logger.info(u" - Validating: {}".format(GREEN_OK))
+
+ return warnings
+
+
+def validate_nbextension_python(spec, full_dest, logger=None):
+ """Assess the health of an installed nbextension
+
+ Returns a list of warnings.
+
+ Parameters
+ ----------
+
+ spec : dict
+ A single entry of _jupyter_nbextension_paths():
+ [{
+ 'section': 'notebook',
+ 'src': 'mockextension',
+ 'dest': '_mockdestination',
+ 'require': '_mockdestination/index'
+ }]
+ full_dest : str
+ The on-disk location of the installed nbextension: this should end
+ with `nbextensions/<dest>`
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ infos = []
+ warnings = []
+
+ section = spec.get("section", None)
+ if section in NBCONFIG_SECTIONS:
+ infos.append(u" {} section: {}".format(GREEN_OK, section))
+ else:
+ warnings.append(u" {} section: {}".format(RED_X, section))
+
+ require = spec.get("require", None)
+ if require is not None:
+ require_path = os.path.join(
+ full_dest[0:-len(spec["dest"])],
+ u"{}.js".format(require))
+ if os.path.exists(require_path):
+ infos.append(u" {} require: {}".format(GREEN_OK, require_path))
+ else:
+ warnings.append(u" {} require: {}".format(RED_X, require_path))
+
+ if logger:
+ if warnings:
+ logger.warning("- Validating: problems found:")
+ for msg in warnings:
+ logger.warning(msg)
+ for msg in infos:
+ logger.info(msg)
+ logger.warning(u"Full spec: {}".format(spec))
+ else:
+ logger.info(u"- Validating: {}".format(GREEN_OK))
+
+ return warnings
+
+
+#----------------------------------------------------------------------
+# Applications
+#----------------------------------------------------------------------
+
+from traitlets import Bool, Unicode, Any
+from jupyter_core.application import JupyterApp
+
+_base_flags = {}
+_base_flags.update(JupyterApp.flags)
+_base_flags.pop("y", None)
+_base_flags.pop("generate-config", None)
+_base_flags.update({
+ "user" : ({
+ "BaseNBExtensionApp" : {
+ "user" : True,
+ }}, "Apply the operation only for the given user"
+ ),
+ "system" : ({
+ "BaseNBExtensionApp" : {
+ "user" : False,
+ "sys_prefix": False,
+ }}, "Apply the operation system-wide"
+ ),
+ "sys-prefix" : ({
+ "BaseNBExtensionApp" : {
+ "sys_prefix" : True,
+ }}, "Use sys.prefix as the prefix for installing nbextensions (for environments, packaging)"
+ ),
+ "py" : ({
+ "BaseNBExtensionApp" : {
+ "python" : True,
+ }}, "Install from a Python package"
+ )
+})
+_base_flags['python'] = _base_flags['py']
+
+class BaseNBExtensionApp(JupyterApp):
+ """Base nbextension installer app"""
+ _log_formatter_cls = LogFormatter
+ flags = _base_flags
+ version = __version__
+
+ user = Bool(False, config=True, help="Whether to do a user install")
+ sys_prefix = Bool(False, config=True, help="Use the sys.prefix as the prefix")
+ python = Bool(False, config=True, help="Install from a Python package")
+
+ # Remove for 5.0...
+ verbose = Any(None, config=True, help="DEPRECATED: Verbosity level")
+
+ def _verbose_changed(self):
+ """Warn about verbosity changes"""
+ import warnings
+ warnings.warn("`verbose` traits of `{}` has been deprecated, has no effects and will be removed in notebook 5.0.".format(type(self).__name__), DeprecationWarning)
+
+ def _log_format_default(self):
+ """A default format for messages"""
+ return "%(message)s"
+
+
+flags = {}
+flags.update(_base_flags)
+flags.update({
+ "overwrite" : ({
+ "InstallNBExtensionApp" : {
+ "overwrite" : True,
+ }}, "Force overwrite of existing files"
+ ),
+ "symlink" : ({
+ "InstallNBExtensionApp" : {
+ "symlink" : True,
+ }}, "Create symlink instead of copying files"
+ ),
+})
+
+flags['s'] = flags['symlink']
+
+aliases = {
+ "prefix" : "InstallNBExtensionApp.prefix",
+ "nbextensions" : "InstallNBExtensionApp.nbextensions_dir",
+ "destination" : "InstallNBExtensionApp.destination",
+}
+
+class InstallNBExtensionApp(BaseNBExtensionApp):
+ """Entry point for installing notebook extensions"""
+ description = """Install Jupyter notebook extensions
+
+ Usage
+
+ jupyter nbextension install path|url [--user|--sys-prefix]
+
+ This copies a file or a folder into the Jupyter nbextensions directory.
+ If a URL is given, it will be downloaded.
+ If an archive is given, it will be extracted into nbextensions.
+ If the requested files are already up to date, no action is taken
+ unless --overwrite is specified.
+ """
+
+ examples = """
+ jupyter nbextension install /path/to/myextension
+ """
+ aliases = aliases
+ flags = flags
+
+ overwrite = Bool(False, config=True, help="Force overwrite of existing files")
+ symlink = Bool(False, config=True, help="Create symlinks instead of copying files")
+
+ prefix = Unicode('', config=True, help="Installation prefix")
+ nbextensions_dir = Unicode('', config=True,
+ help="Full path to nbextensions dir (probably use prefix or user)")
+ destination = Unicode('', config=True, help="Destination for the copy or symlink")
+
+ def _config_file_name_default(self):
+ """The default config file name."""
+ return 'jupyter_notebook_config'
+
+ def install_extensions(self):
+ """Perform the installation of nbextension(s)"""
+ if len(self.extra_args)>1:
+ raise ValueError("Only one nbextension allowed at a time. "
+ "Call multiple times to install multiple extensions.")
+
+ if self.python:
+ install = install_nbextension_python
+ kwargs = {}
+ else:
+ install = install_nbextension
+ kwargs = {'destination': self.destination}
+
+ full_dests = install(self.extra_args[0],
+ overwrite=self.overwrite,
+ symlink=self.symlink,
+ user=self.user,
+ sys_prefix=self.sys_prefix,
+ prefix=self.prefix,
+ nbextensions_dir=self.nbextensions_dir,
+ logger=self.log,
+ **kwargs
+ )
+
+ if full_dests:
+ self.log.info(
+ u"\nTo initialize this nbextension in the browser every time"
+ " the notebook (or other app) loads:\n\n"
+ " jupyter nbextension enable {}{}{}{}\n".format(
+ self.extra_args[0] if self.python else "<the entry point>",
+ " --user" if self.user else "",
+ " --py" if self.python else "",
+ " --sys-prefix" if self.sys_prefix else ""
+ )
+ )
+
+ def start(self):
+ """Perform the App's function as configured"""
+ if not self.extra_args:
+ sys.exit('Please specify an nbextension to install')
+ else:
+ try:
+ self.install_extensions()
+ except ArgumentConflict as e:
+ sys.exit(str(e))
+
+
+class UninstallNBExtensionApp(BaseNBExtensionApp):
+ """Entry point for uninstalling notebook extensions"""
+ version = __version__
+ description = """Uninstall Jupyter notebook extensions
+
+ Usage
+
+ jupyter nbextension uninstall path/url path/url/entrypoint
+ jupyter nbextension uninstall --py pythonPackageName
+
+ This uninstalls an nbextension.
+ """
+
+ examples = """
+ jupyter nbextension uninstall dest/dir dest/dir/extensionjs
+ jupyter nbextension uninstall --py extensionPyPackage
+ """
+
+ aliases = {
+ "prefix" : "UninstallNBExtensionApp.prefix",
+ "nbextensions" : "UninstallNBExtensionApp.nbextensions_dir",
+ "require": "UninstallNBExtensionApp.require",
+ }
+
+ prefix = Unicode('', config=True, help="Installation prefix")
+ nbextensions_dir = Unicode('', config=True, help="Full path to nbextensions dir (probably use prefix or user)")
+ require = Unicode('', config=True, help="require.js module to load.")
+
+ def _config_file_name_default(self):
+ """The default config file name."""
+ return 'jupyter_notebook_config'
+
+ def uninstall_extensions(self):
+ """Uninstall some nbextensions"""
+ kwargs = {
+ 'user': self.user,
+ 'sys_prefix': self.sys_prefix,
+ 'prefix': self.prefix,
+ 'nbextensions_dir': self.nbextensions_dir,
+ 'logger': self.log
+ }
+
+ arg_count = 1
+ if len(self.extra_args) > arg_count:
+ raise ValueError("only one nbextension allowed at a time. Call multiple times to uninstall multiple extensions.")
+ if len(self.extra_args) < arg_count:
+ raise ValueError("not enough arguments")
+
+ if self.python:
+ uninstall_nbextension_python(self.extra_args[0], **kwargs)
+ else:
+ if self.require:
+ kwargs['require'] = self.require
+ uninstall_nbextension(self.extra_args[0], **kwargs)
+
+ def start(self):
+ if not self.extra_args:
+ sys.exit('Please specify an nbextension to uninstall')
+ else:
+ try:
+ self.uninstall_extensions()
+ except ArgumentConflict as e:
+ sys.exit(str(e))
+
+
+class ToggleNBExtensionApp(BaseNBExtensionApp):
+ """A base class for apps that enable/disable extensions"""
+ name = "jupyter nbextension enable/disable"
+ version = __version__
+ description = "Enable/disable an nbextension in configuration."
+
+ section = Unicode('notebook', config=True,
+ help="""Which config section to add the extension to, 'common' will affect all pages."""
+ )
+ user = Bool(True, config=True, help="Apply the configuration only for the current user (default)")
+
+ aliases = {'section': 'ToggleNBExtensionApp.section'}
+
+ _toggle_value = None
+
+ def _config_file_name_default(self):
+ """The default config file name."""
+ return 'jupyter_notebook_config'
+
+ def toggle_nbextension_python(self, module):
+ """Toggle some extensions in an importable Python module.
+
+ Returns a list of booleans indicating whether the state was changed as
+ requested.
+
+ Parameters
+ ----------
+ module : str
+ Importable Python module exposing the
+ magic-named `_jupyter_nbextension_paths` function
+ """
+ toggle = (enable_nbextension_python if self._toggle_value
+ else disable_nbextension_python)
+ return toggle(module,
+ user=self.user,
+ sys_prefix=self.sys_prefix,
+ logger=self.log)
+
+ def toggle_nbextension(self, require):
+ """Toggle some a named nbextension by require-able AMD module.
+
+ Returns whether the state was changed as requested.
+
+ Parameters
+ ----------
+ require : str
+ require.js path used to load the nbextension
+ """
+ toggle = (enable_nbextension if self._toggle_value
+ else disable_nbextension)
+ return toggle(self.section, require,
+ user=self.user, sys_prefix=self.sys_prefix,
+ logger=self.log)
+
+ def start(self):
+ if not self.extra_args:
+ sys.exit('Please specify an nbextension/package to enable or disable')
+ elif len(self.extra_args) > 1:
+ sys.exit('Please specify one nbextension/package at a time')
+ if self.python:
+ self.toggle_nbextension_python(self.extra_args[0])
+ else:
+ self.toggle_nbextension(self.extra_args[0])
+
+
+class EnableNBExtensionApp(ToggleNBExtensionApp):
+ """An App that enables nbextensions"""
+ name = "jupyter nbextension enable"
+ description = """
+ Enable an nbextension in frontend configuration.
+
+ Usage
+ jupyter nbextension enable [--system|--sys-prefix]
+ """
+ _toggle_value = True
+
+
+class DisableNBExtensionApp(ToggleNBExtensionApp):
+ """An App that disables nbextensions"""
+ name = "jupyter nbextension disable"
+ description = """
+ Enable an nbextension in frontend configuration.
+
+ Usage
+ jupyter nbextension disable [--system|--sys-prefix]
+ """
+ _toggle_value = None
+
+
+class ListNBExtensionsApp(BaseNBExtensionApp):
+ """An App that lists and validates nbextensions"""
+ name = "jupyter nbextension list"
+ version = __version__
+ description = "List all nbextensions known by the configuration system"
+
+ def list_nbextensions(self):
+ """List all the nbextensions"""
+ config_dirs = [os.path.join(p, 'nbconfig') for p in jupyter_config_path()]
+
+ print("Known nbextensions:")
+
+ for config_dir in config_dirs:
+ head = u' config dir: {}'.format(config_dir)
+ head_shown = False
+
+ cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
+ for section in NBCONFIG_SECTIONS:
+ data = cm.get(section)
+ if 'load_extensions' in data:
+ if not head_shown:
+ # only show heading if there is an nbextension here
+ print(head)
+ head_shown = True
+ print(u' {} section'.format(section))
+
+ for require, enabled in data['load_extensions'].items():
+ print(u' {} {}'.format(
+ require,
+ GREEN_ENABLED if enabled else RED_DISABLED))
+ if enabled:
+ validate_nbextension(require, logger=self.log)
+
+ def start(self):
+ """Perform the App's functions as configured"""
+ self.list_nbextensions()
+
+
+_examples = """
+jupyter nbextension list # list all configured nbextensions
+jupyter nbextension install --py <packagename> # install an nbextension from a Python package
+jupyter nbextension enable --py <packagename> # enable all nbextensions in a Python package
+jupyter nbextension disable --py <packagename> # disable all nbextensions in a Python package
+jupyter nbextension uninstall --py <packagename> # uninstall an nbextension in a Python package
+"""
+
+class NBExtensionApp(BaseNBExtensionApp):
+ """Base jupyter nbextension command entry point"""
+ name = "jupyter nbextension"
+ version = __version__
+ description = "Work with Jupyter notebook extensions"
+ examples = _examples
+
+ subcommands = dict(
+ install=(InstallNBExtensionApp,"Install an nbextension"),
+ enable=(EnableNBExtensionApp, "Enable an nbextension"),
+ disable=(DisableNBExtensionApp, "Disable an nbextension"),
+ uninstall=(UninstallNBExtensionApp, "Uninstall an nbextension"),
+ list=(ListNBExtensionsApp, "List nbextensions")
+ )
+
+ def start(self):
+ """Perform the App's functions as configured"""
+ super(NBExtensionApp, self).start()
+
+ # The above should have called a subcommand and raised NoStart; if we
+ # get here, it didn't, so we should self.log.info a message.
+ subcmds = ", ".join(sorted(self.subcommands))
+ sys.exit("Please supply at least one subcommand: %s" % subcmds)
+
+main = NBExtensionApp.launch_instance
+
+#------------------------------------------------------------------------------
+# Private API
+#------------------------------------------------------------------------------
+
+
+def _should_copy(src, dest, logger=None):
+ """Should a file be copied, if it doesn't exist, or is newer?
+
+ Returns whether the file needs to be updated.
+
+ Parameters
+ ----------
+
+ src : string
+ A path that should exist from which to copy a file
+ src : string
+ A path that might exist to which to copy a file
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ if not os.path.exists(dest):
+ return True
+ if os.stat(src).st_mtime - os.stat(dest).st_mtime > 1e-6:
+ # we add a fudge factor to work around a bug in python 2.x
+ # that was fixed in python 3.x: http://bugs.python.org/issue12904
+ if logger:
+ logger.warn("Out of date: %s" % dest)
+ return True
+ if logger:
+ logger.info("Up to date: %s" % dest)
+ return False
+
+
+def _maybe_copy(src, dest, logger=None):
+ """Copy a file if it needs updating.
+
+ Parameters
+ ----------
+
+ src : string
+ A path that should exist from which to copy a file
+ src : string
+ A path that might exist to which to copy a file
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ if _should_copy(src, dest, logger=logger):
+ if logger:
+ logger.info("Copying: %s -> %s" % (src, dest))
+ shutil.copy2(src, dest)
+
+
+def _safe_is_tarfile(path):
+ """Safe version of is_tarfile, return False on IOError.
+
+ Returns whether the file exists and is a tarfile.
+
+ Parameters
+ ----------
+
+ path : string
+ A path that might not exist and or be a tarfile
+ """
+ try:
+ return tarfile.is_tarfile(path)
+ except IOError:
+ return False
+
+
+def _get_nbextension_dir(user=False, sys_prefix=False, prefix=None, nbextensions_dir=None):
+ """Return the nbextension directory specified
+
+ Parameters
+ ----------
+
+ user : bool [default: False]
+ Get the user's .jupyter/nbextensions directory
+ sys_prefix : bool [default: False]
+ Get sys.prefix, i.e. ~/.envs/my-env/share/jupyter/nbextensions
+ prefix : str [optional]
+ Get custom prefix
+ nbextensions_dir : str [optional]
+ Get what you put in
+ """
+ conflicting = [
+ ('user', user),
+ ('prefix', prefix),
+ ('nbextensions_dir', nbextensions_dir),
+ ('sys_prefix', sys_prefix),
+ ]
+ conflicting_set = ['{}={!r}'.format(n, v) for n, v in conflicting if v]
+ if len(conflicting_set) > 1:
+ raise ArgumentConflict(
+ "cannot specify more than one of user, sys_prefix, prefix, or nbextensions_dir, but got: {}"
+ .format(', '.join(conflicting_set)))
+ if user:
+ nbext = pjoin(jupyter_data_dir(), u'nbextensions')
+ elif sys_prefix:
+ nbext = pjoin(ENV_JUPYTER_PATH[0], u'nbextensions')
+ elif prefix:
+ nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions')
+ elif nbextensions_dir:
+ nbext = nbextensions_dir
+ else:
+ nbext = pjoin(SYSTEM_JUPYTER_PATH[0], 'nbextensions')
+ return nbext
+
+
+def _nbextension_dirs():
+ """The possible locations of nbextensions.
+
+ Returns a list of known base extension locations
+ """
+ return [
+ pjoin(jupyter_data_dir(), u'nbextensions'),
+ pjoin(ENV_JUPYTER_PATH[0], u'nbextensions'),
+ pjoin(SYSTEM_JUPYTER_PATH[0], 'nbextensions')
+ ]
+
+
+def _get_config_dir(user=False, sys_prefix=False):
+ """Get the location of config files for the current context
+
+ Returns the string to the enviornment
+
+ Parameters
+ ----------
+
+ user : bool [default: False]
+ Get the user's .jupyter config directory
+ sys_prefix : bool [default: False]
+ Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
+ """
+ user = False if sys_prefix else user
+ if user and sys_prefix:
+ raise ArgumentConflict("Cannot specify more than one of user or sys_prefix")
+ if user:
+ nbext = jupyter_config_dir()
+ elif sys_prefix:
+ nbext = ENV_CONFIG_PATH[0]
+ else:
+ nbext = SYSTEM_CONFIG_PATH[0]
+ return nbext
+
+
+def _get_nbextension_metadata(module):
+ """Get the list of nbextension paths associated with a Python module.
+
+ Returns a tuple of (the module, [{
+ 'section': 'notebook',
+ 'src': 'mockextension',
+ 'dest': '_mockdestination',
+ 'require': '_mockdestination/index'
+ }])
+
+ Parameters
+ ----------
+
+ module : str
+ Importable Python module exposing the
+ magic-named `_jupyter_nbextension_paths` function
+ """
+ m = import_item(module)
+ if not hasattr(m, '_jupyter_nbextension_paths'):
+ raise KeyError('The Python module {} is not a valid nbextension, '
+ 'it is missing the `_jupyter_nbextension_paths()` method.'.format(module))
+ nbexts = m._jupyter_nbextension_paths()
+ return m, nbexts
+
+
+def _read_config_data(user=False, sys_prefix=False):
+ """Get the config for the current context
+
+ Returns the string to the enviornment
+
+ Parameters
+ ----------
+
+ user : bool [default: False]
+ Get the user's .jupyter config directory
+ sys_prefix : bool [default: False]
+ Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
+ """
+ config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
+ config_man = BaseJSONConfigManager(config_dir=config_dir)
+ return config_man.get('jupyter_notebook_config')
+
+
+def _write_config_data(data, user=False, sys_prefix=False):
+ """Update the config for the current context
+
+ Parameters
+ ----------
+ data : object
+ An object which can be accepted by ConfigManager.update
+ user : bool [default: False]
+ Get the user's .jupyter config directory
+ sys_prefix : bool [default: False]
+ Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
+ """
+ config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
+ config_man = BaseJSONConfigManager(config_dir=config_dir)
+ config_man.update('jupyter_notebook_config', data)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/notebook/notebook/__init__.py b/notebook/notebook/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/notebook/__init__.py
diff --git a/notebook/notebook/handlers.py b/notebook/notebook/handlers.py
new file mode 100644
index 0000000..72bbd5a
--- /dev/null
+++ b/notebook/notebook/handlers.py
@@ -0,0 +1,55 @@
+"""Tornado handlers for the live notebook view."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import os
+from tornado import web
+HTTPError = web.HTTPError
+
+from ..base.handlers import (
+ IPythonHandler, FilesRedirectHandler, path_regex,
+)
+from ..utils import url_escape
+
+
+class NotebookHandler(IPythonHandler):
+
+ @web.authenticated
+ def get(self, path):
+ """get renders the notebook template if a name is given, or
+ redirects to the '/files/' handler if the name is not given."""
+ path = path.strip('/')
+ cm = self.contents_manager
+
+ # will raise 404 on not found
+ try:
+ model = cm.get(path, content=False)
+ except web.HTTPError as e:
+ if e.status_code == 404 and 'files' in path.split('/'):
+ # 404, but '/files/' in URL, let FilesRedirect take care of it
+ return FilesRedirectHandler.redirect_to_files(self, path)
+ else:
+ raise
+ if model['type'] != 'notebook':
+ # not a notebook, redirect to files
+ return FilesRedirectHandler.redirect_to_files(self, path)
+ name = path.rsplit('/', 1)[-1]
+ self.write(self.render_template('notebook.html',
+ notebook_path=path,
+ notebook_name=name,
+ kill_kernel=False,
+ mathjax_url=self.mathjax_url,
+ )
+ )
+
+
+#-----------------------------------------------------------------------------
+# URL to handler mappings
+#-----------------------------------------------------------------------------
+
+
+default_handlers = [
+ (r"/notebooks%s" % path_regex, NotebookHandler),
+]
+
diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py
new file mode 100644
index 0000000..ab73020
--- /dev/null
+++ b/notebook/notebookapp.py
@@ -0,0 +1,1210 @@
+# coding: utf-8
+"""A tornado based Jupyter notebook server."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from __future__ import absolute_import, print_function
+
+import base64
+import datetime
+import errno
+import importlib
+import io
+import json
+import logging
+import mimetypes
+import os
+import random
+import re
+import select
+import signal
+import socket
+import sys
+import threading
+import webbrowser
+
+from jinja2 import Environment, FileSystemLoader
+
+# Install the pyzmq ioloop. This has to be done before anything else from
+# tornado is imported.
+from zmq.eventloop import ioloop
+ioloop.install()
+
+# check for tornado 3.1.0
+msg = "The Jupyter Notebook requires tornado >= 4.0"
+try:
+ import tornado
+except ImportError:
+ raise ImportError(msg)
+try:
+ version_info = tornado.version_info
+except AttributeError:
+ raise ImportError(msg + ", but you have < 1.1.0")
+if version_info < (4,0):
+ raise ImportError(msg + ", but you have %s" % tornado.version)
+
+from tornado import httpserver
+from tornado import web
+from tornado.log import LogFormatter, app_log, access_log, gen_log
+
+from notebook import (
+ DEFAULT_STATIC_FILES_PATH,
+ DEFAULT_TEMPLATE_PATH_LIST,
+ __version__,
+)
+from .base.handlers import Template404
+from .log import log_request
+from .services.kernels.kernelmanager import MappingKernelManager
+from .services.config import ConfigManager
+from .services.contents.manager import ContentsManager
+from .services.contents.filemanager import FileContentsManager
+from .services.sessions.sessionmanager import SessionManager
+
+from .auth.login import LoginHandler
+from .auth.logout import LogoutHandler
+from .base.handlers import FileFindHandler, IPythonHandler
+
+from traitlets.config import Config
+from traitlets.config.application import catch_config_error, boolean_flag
+from jupyter_core.application import (
+ JupyterApp, base_flags, base_aliases,
+)
+from jupyter_client import KernelManager
+from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel, NATIVE_KERNEL_NAME
+from jupyter_client.session import Session
+from nbformat.sign import NotebookNotary
+from traitlets import (
+ Dict, Unicode, Integer, List, Bool, Bytes, Instance,
+ TraitError, Type, Float
+)
+from ipython_genutils import py3compat
+from jupyter_core.paths import jupyter_runtime_dir, jupyter_path
+from notebook._sysinfo import get_sys_info
+
+from .utils import url_path_join, check_pid, url_escape
+
+#-----------------------------------------------------------------------------
+# Module globals
+#-----------------------------------------------------------------------------
+
+_examples = """
+jupyter notebook # start the notebook
+jupyter notebook --certfile=mycert.pem # use SSL/TLS certificate
+"""
+
+#-----------------------------------------------------------------------------
+# Helper functions
+#-----------------------------------------------------------------------------
+
+def random_ports(port, n):
+ """Generate a list of n random ports near the given port.
+
+ The first 5 ports will be sequential, and the remaining n-5 will be
+ randomly selected in the range [port-2*n, port+2*n].
+ """
+ for i in range(min(5, n)):
+ yield port + i
+ for i in range(n-5):
+ yield max(1, port + random.randint(-2*n, 2*n))
+
+def load_handlers(name):
+ """Load the (URL pattern, handler) tuples for each component."""
+ name = 'notebook.' + name
+ mod = __import__(name, fromlist=['default_handlers'])
+ return mod.default_handlers
+
+
+#-----------------------------------------------------------------------------
+# The Tornado web application
+#-----------------------------------------------------------------------------
+
+class NotebookWebApplication(web.Application):
+
+ def __init__(self, ipython_app, kernel_manager, contents_manager,
+ session_manager, kernel_spec_manager,
+ config_manager, log,
+ base_url, default_url, settings_overrides, jinja_env_options):
+
+ settings = self.init_settings(
+ ipython_app, kernel_manager, contents_manager,
+ session_manager, kernel_spec_manager, config_manager, log, base_url,
+ default_url, settings_overrides, jinja_env_options)
+ handlers = self.init_handlers(settings)
+
+ super(NotebookWebApplication, self).__init__(handlers, **settings)
+
+ def init_settings(self, ipython_app, kernel_manager, contents_manager,
+ session_manager, kernel_spec_manager,
+ config_manager,
+ log, base_url, default_url, settings_overrides,
+ jinja_env_options=None):
+
+ _template_path = settings_overrides.get(
+ "template_path",
+ ipython_app.template_file_path,
+ )
+ if isinstance(_template_path, py3compat.string_types):
+ _template_path = (_template_path,)
+ template_path = [os.path.expanduser(path) for path in _template_path]
+
+ jenv_opt = {"autoescape": True}
+ jenv_opt.update(jinja_env_options if jinja_env_options else {})
+
+ env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
+
+ sys_info = get_sys_info()
+ if sys_info['commit_source'] == 'repository':
+ # don't cache (rely on 304) when working from master
+ version_hash = ''
+ else:
+ # reset the cache on server restart
+ version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
+
+ settings = dict(
+ # basics
+ log_function=log_request,
+ base_url=base_url,
+ default_url=default_url,
+ template_path=template_path,
+ static_path=ipython_app.static_file_path,
+ static_custom_path=ipython_app.static_custom_path,
+ static_handler_class = FileFindHandler,
+ static_url_prefix = url_path_join(base_url,'/static/'),
+ static_handler_args = {
+ # don't cache custom.js
+ 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
+ },
+ version_hash=version_hash,
+ ignore_minified_js=ipython_app.ignore_minified_js,
+
+ # rate limits
+ iopub_msg_rate_limit=ipython_app.iopub_msg_rate_limit,
+ iopub_data_rate_limit=ipython_app.iopub_data_rate_limit,
+ rate_limit_window=ipython_app.rate_limit_window,
+
+ # authentication
+ cookie_secret=ipython_app.cookie_secret,
+ login_url=url_path_join(base_url,'/login'),
+ login_handler_class=ipython_app.login_handler_class,
+ logout_handler_class=ipython_app.logout_handler_class,
+ password=ipython_app.password,
+
+ # managers
+ kernel_manager=kernel_manager,
+ contents_manager=contents_manager,
+ session_manager=session_manager,
+ kernel_spec_manager=kernel_spec_manager,
+ config_manager=config_manager,
+
+ # IPython stuff
+ jinja_template_vars=ipython_app.jinja_template_vars,
+ nbextensions_path=ipython_app.nbextensions_path,
+ websocket_url=ipython_app.websocket_url,
+ mathjax_url=ipython_app.mathjax_url,
+ config=ipython_app.config,
+ config_dir=ipython_app.config_dir,
+ jinja2_env=env,
+ terminals_available=False, # Set later if terminals are available
+ )
+
+ # allow custom overrides for the tornado web app.
+ settings.update(settings_overrides)
+ return settings
+
+ def init_handlers(self, settings):
+ """Load the (URL pattern, handler) tuples for each component."""
+
+ # Order matters. The first handler to match the URL will handle the request.
+ handlers = []
+ handlers.extend(load_handlers('tree.handlers'))
+ handlers.extend([(r"/login", settings['login_handler_class'])])
+ handlers.extend([(r"/logout", settings['logout_handler_class'])])
+ handlers.extend(load_handlers('files.handlers'))
+ handlers.extend(load_handlers('notebook.handlers'))
+ handlers.extend(load_handlers('nbconvert.handlers'))
+ handlers.extend(load_handlers('kernelspecs.handlers'))
+ handlers.extend(load_handlers('edit.handlers'))
+ handlers.extend(load_handlers('services.api.handlers'))
+ handlers.extend(load_handlers('services.config.handlers'))
+ handlers.extend(load_handlers('services.kernels.handlers'))
+ handlers.extend(load_handlers('services.contents.handlers'))
+ handlers.extend(load_handlers('services.sessions.handlers'))
+ handlers.extend(load_handlers('services.nbconvert.handlers'))
+ handlers.extend(load_handlers('services.kernelspecs.handlers'))
+ handlers.extend(load_handlers('services.security.handlers'))
+
+ # BEGIN HARDCODED WIDGETS HACK
+ # TODO: Remove on notebook 5.0
+ try:
+ import widgetsnbextension
+ except:
+ try:
+ import ipywidgets as widgets
+ handlers.append(
+ (r"/nbextensions/widgets/(.*)", FileFindHandler, {
+ 'path': widgets.find_static_assets(),
+ 'no_cache_paths': ['/'], # don't cache anything in nbextensions
+ }),
+ )
+ except:
+ app_log.warning('Widgets are unavailable. Please install widgetsnbextension or ipywidgets 4.0')
+ # END HARDCODED WIDGETS HACK
+
+ handlers.append(
+ (r"/nbextensions/(.*)", FileFindHandler, {
+ 'path': settings['nbextensions_path'],
+ 'no_cache_paths': ['/'], # don't cache anything in nbextensions
+ }),
+ )
+ handlers.append(
+ (r"/custom/(.*)", FileFindHandler, {
+ 'path': settings['static_custom_path'],
+ 'no_cache_paths': ['/'], # don't cache anything in custom
+ })
+ )
+ # register base handlers last
+ handlers.extend(load_handlers('base.handlers'))
+ # set the URL that will be redirected from `/`
+ handlers.append(
+ (r'/?', web.RedirectHandler, {
+ 'url' : settings['default_url'],
+ 'permanent': False, # want 302, not 301
+ })
+ )
+
+ # prepend base_url onto the patterns that we match
+ new_handlers = []
+ for handler in handlers:
+ pattern = url_path_join(settings['base_url'], handler[0])
+ new_handler = tuple([pattern] + list(handler[1:]))
+ new_handlers.append(new_handler)
+ # add 404 on the end, which will catch everything that falls through
+ new_handlers.append((r'(.*)', Template404))
+ return new_handlers
+
+
+class NbserverListApp(JupyterApp):
+ version = __version__
+ description="List currently running notebook servers."
+
+ flags = dict(
+ json=({'NbserverListApp': {'json': True}},
+ "Produce machine-readable JSON output."),
+ )
+
+ json = Bool(False, config=True,
+ help="If True, each line of output will be a JSON object with the "
+ "details from the server info file.")
+
+ def start(self):
+ if not self.json:
+ print("Currently running servers:")
+ for serverinfo in list_running_servers(self.runtime_dir):
+ if self.json:
+ print(json.dumps(serverinfo))
+ else:
+ print(serverinfo['url'], "::", serverinfo['notebook_dir'])
+
+#-----------------------------------------------------------------------------
+# Aliases and Flags
+#-----------------------------------------------------------------------------
+
+flags = dict(base_flags)
+flags['no-browser']=(
+ {'NotebookApp' : {'open_browser' : False}},
+ "Don't open the notebook in a browser after startup."
+)
+flags['pylab']=(
+ {'NotebookApp' : {'pylab' : 'warn'}},
+ "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
+)
+flags['no-mathjax']=(
+ {'NotebookApp' : {'enable_mathjax' : False}},
+ """Disable MathJax
+
+ MathJax is the javascript library Jupyter uses to render math/LaTeX. It is
+ very large, so you may want to disable it if you have a slow internet
+ connection, or for offline use of the notebook.
+
+ When disabled, equations etc. will appear as their untransformed TeX source.
+ """
+)
+
+# Add notebook manager flags
+flags.update(boolean_flag('script', 'FileContentsManager.save_script',
+ 'DEPRECATED, IGNORED',
+ 'DEPRECATED, IGNORED'))
+
+aliases = dict(base_aliases)
+
+aliases.update({
+ 'ip': 'NotebookApp.ip',
+ 'port': 'NotebookApp.port',
+ 'port-retries': 'NotebookApp.port_retries',
+ 'transport': 'KernelManager.transport',
+ 'keyfile': 'NotebookApp.keyfile',
+ 'certfile': 'NotebookApp.certfile',
+ 'client-ca': 'NotebookApp.client_ca',
+ 'notebook-dir': 'NotebookApp.notebook_dir',
+ 'browser': 'NotebookApp.browser',
+ 'pylab': 'NotebookApp.pylab',
+})
+
+#-----------------------------------------------------------------------------
+# NotebookApp
+#-----------------------------------------------------------------------------
+
+class NotebookApp(JupyterApp):
+
+ name = 'jupyter-notebook'
+ version = __version__
+ description = """
+ The Jupyter HTML Notebook.
+
+ This launches a Tornado based HTML Notebook Server that serves up an
+ HTML5/Javascript Notebook client.
+ """
+ examples = _examples
+ aliases = aliases
+ flags = flags
+
+ classes = [
+ KernelManager, Session, MappingKernelManager,
+ ContentsManager, FileContentsManager, NotebookNotary,
+ KernelSpecManager,
+ ]
+ flags = Dict(flags)
+ aliases = Dict(aliases)
+
+ subcommands = dict(
+ list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
+ )
+
+ _log_formatter_cls = LogFormatter
+
+ def _log_level_default(self):
+ return logging.INFO
+
+ def _log_datefmt_default(self):
+ """Exclude date from default date format"""
+ return "%H:%M:%S"
+
+ def _log_format_default(self):
+ """override default log format to include time"""
+ return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
+
+ ignore_minified_js = Bool(False,
+ config=True,
+ help='Use minified JS file or not, mainly use during dev to avoid JS recompilation',
+ )
+
+ # file to be opened in the notebook server
+ file_to_run = Unicode('', config=True)
+
+ # Network related information
+
+ allow_origin = Unicode('', config=True,
+ help="""Set the Access-Control-Allow-Origin header
+
+ Use '*' to allow any origin to access your server.
+
+ Takes precedence over allow_origin_pat.
+ """
+ )
+
+ allow_origin_pat = Unicode('', config=True,
+ help="""Use a regular expression for the Access-Control-Allow-Origin header
+
+ Requests from an origin matching the expression will get replies with:
+
+ Access-Control-Allow-Origin: origin
+
+ where `origin` is the origin of the request.
+
+ Ignored if allow_origin is set.
+ """
+ )
+
+ allow_credentials = Bool(False, config=True,
+ help="Set the Access-Control-Allow-Credentials: true header"
+ )
+
+ default_url = Unicode('/tree', config=True,
+ help="The default URL to redirect to from `/`"
+ )
+
+ ip = Unicode('localhost', config=True,
+ help="The IP address the notebook server will listen on."
+ )
+ def _ip_default(self):
+ """Return localhost if available, 127.0.0.1 otherwise.
+
+ On some (horribly broken) systems, localhost cannot be bound.
+ """
+ s = socket.socket()
+ try:
+ s.bind(('localhost', 0))
+ except socket.error as e:
+ self.log.warn("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s", e)
+ return '127.0.0.1'
+ else:
+ s.close()
+ return 'localhost'
+
+ def _ip_changed(self, name, old, new):
+ if new == u'*': self.ip = u''
+
+ port = Integer(8888, config=True,
+ help="The port the notebook server will listen on."
+ )
+ port_retries = Integer(50, config=True,
+ help="The number of additional ports to try if the specified port is not available."
+ )
+
+ certfile = Unicode(u'', config=True,
+ help="""The full path to an SSL/TLS certificate file."""
+ )
+
+ keyfile = Unicode(u'', config=True,
+ help="""The full path to a private key file for usage with SSL/TLS."""
+ )
+
+ client_ca = Unicode(u'', config=True,
+ help="""The full path to a certificate authority certificate for SSL/TLS client authentication."""
+ )
+
+ cookie_secret_file = Unicode(config=True,
+ help="""The file where the cookie secret is stored."""
+ )
+ def _cookie_secret_file_default(self):
+ return os.path.join(self.runtime_dir, 'notebook_cookie_secret')
+
+ cookie_secret = Bytes(b'', config=True,
+ help="""The random bytes used to secure cookies.
+ By default this is a new random number every time you start the Notebook.
+ Set it to a value in a config file to enable logins to persist across server sessions.
+
+ Note: Cookie secrets should be kept private, do not share config files with
+ cookie_secret stored in plaintext (you can read the value from a file).
+ """
+ )
+ def _cookie_secret_default(self):
+ if os.path.exists(self.cookie_secret_file):
+ with io.open(self.cookie_secret_file, 'rb') as f:
+ return f.read()
+ else:
+ secret = base64.encodestring(os.urandom(1024))
+ self._write_cookie_secret_file(secret)
+ return secret
+
+ def _write_cookie_secret_file(self, secret):
+ """write my secret to my secret_file"""
+ self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
+ with io.open(self.cookie_secret_file, 'wb') as f:
+ f.write(secret)
+ try:
+ os.chmod(self.cookie_secret_file, 0o600)
+ except OSError:
+ self.log.warn(
+ "Could not set permissions on %s",
+ self.cookie_secret_file
+ )
+
+ password = Unicode(u'', config=True,
+ help="""Hashed password to use for web authentication.
+
+ To generate, type in a python/IPython shell:
+
+ from notebook.auth import passwd; passwd()
+
+ The string should be of the form type:salt:hashed-password.
+ """
+ )
+
+ open_browser = Bool(True, config=True,
+ help="""Whether to open in a browser after starting.
+ The specific browser used is platform dependent and
+ determined by the python standard library `webbrowser`
+ module, unless it is overridden using the --browser
+ (NotebookApp.browser) configuration option.
+ """)
+
+ browser = Unicode(u'', config=True,
+ help="""Specify what command to use to invoke a web
+ browser when opening the notebook. If not specified, the
+ default browser will be determined by the `webbrowser`
+ standard library module, which allows setting of the
+ BROWSER environment variable to override it.
+ """)
+
+ webapp_settings = Dict(config=True,
+ help="DEPRECATED, use tornado_settings"
+ )
+ def _webapp_settings_changed(self, name, old, new):
+ self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
+ self.tornado_settings = new
+
+ tornado_settings = Dict(config=True,
+ help="Supply overrides for the tornado.web.Application that the "
+ "Jupyter notebook uses.")
+
+ cookie_options = Dict(config=True,
+ help="Extra keyword arguments to pass to `set_secure_cookie`."
+ " See tornado's set_secure_cookie docs for details."
+ )
+ ssl_options = Dict(config=True,
+ help="""Supply SSL options for the tornado HTTPServer.
+ See the tornado docs for details.""")
+
+ jinja_environment_options = Dict(config=True,
+ help="Supply extra arguments that will be passed to Jinja environment.")
+
+ jinja_template_vars = Dict(
+ config=True,
+ help="Extra variables to supply to jinja templates when rendering.",
+ )
+
+ enable_mathjax = Bool(True, config=True,
+ help="""Whether to enable MathJax for typesetting math/TeX
+
+ MathJax is the javascript library Jupyter uses to render math/LaTeX. It is
+ very large, so you may want to disable it if you have a slow internet
+ connection, or for offline use of the notebook.
+
+ When disabled, equations etc. will appear as their untransformed TeX source.
+ """
+ )
+ def _enable_mathjax_changed(self, name, old, new):
+ """set mathjax url to empty if mathjax is disabled"""
+ if not new:
+ self.mathjax_url = u''
+
+ base_url = Unicode('/', config=True,
+ help='''The base URL for the notebook server.
+
+ Leading and trailing slashes can be omitted,
+ and will automatically be added.
+ ''')
+ def _base_url_changed(self, name, old, new):
+ if not new.startswith('/'):
+ self.base_url = '/'+new
+ elif not new.endswith('/'):
+ self.base_url = new+'/'
+
+ base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
+ def _base_project_url_changed(self, name, old, new):
+ self.log.warn("base_project_url is deprecated, use base_url")
+ self.base_url = new
+
+ extra_static_paths = List(Unicode(), config=True,
+ help="""Extra paths to search for serving static files.
+
+ This allows adding javascript/css to be available from the notebook server machine,
+ or overriding individual files in the IPython"""
+ )
+
+ @property
+ def static_file_path(self):
+ """return extra paths + the default location"""
+ return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
+
+ static_custom_path = List(Unicode(),
+ help="""Path to search for custom.js, css"""
+ )
+ def _static_custom_path_default(self):
+ return [
+ os.path.join(d, 'custom') for d in (
+ self.config_dir,
+ DEFAULT_STATIC_FILES_PATH)
+ ]
+
+ extra_template_paths = List(Unicode(), config=True,
+ help="""Extra paths to search for serving jinja templates.
+
+ Can be used to override templates from notebook.templates."""
+ )
+
+ @property
+ def template_file_path(self):
+ """return extra paths + the default locations"""
+ return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
+
+ extra_nbextensions_path = List(Unicode(), config=True,
+ help="""extra paths to look for Javascript notebook extensions"""
+ )
+
+ @property
+ def nbextensions_path(self):
+ """The path to look for Javascript notebook extensions"""
+ path = self.extra_nbextensions_path + jupyter_path('nbextensions')
+ # FIXME: remove IPython nbextensions path after a migration period
+ try:
+ from IPython.paths import get_ipython_dir
+ except ImportError:
+ pass
+ else:
+ path.append(os.path.join(get_ipython_dir(), 'nbextensions'))
+ return path
+
+ websocket_url = Unicode("", config=True,
+ help="""The base URL for websockets,
+ if it differs from the HTTP server (hint: it almost certainly doesn't).
+
+ Should be in the form of an HTTP origin: ws[s]://hostname[:port]
+ """
+ )
+ mathjax_url = Unicode("", config=True,
+ help="""The url for MathJax.js."""
+ )
+ def _mathjax_url_default(self):
+ if not self.enable_mathjax:
+ return u''
+ static_url_prefix = self.tornado_settings.get("static_url_prefix", "static")
+ return url_path_join(static_url_prefix, 'components', 'MathJax', 'MathJax.js')
+
+ def _mathjax_url_changed(self, name, old, new):
+ if new and not self.enable_mathjax:
+ # enable_mathjax=False overrides mathjax_url
+ self.mathjax_url = u''
+ else:
+ self.log.info("Using MathJax: %s", new)
+
+ contents_manager_class = Type(
+ default_value=FileContentsManager,
+ klass=ContentsManager,
+ config=True,
+ help='The notebook manager class to use.'
+ )
+ kernel_manager_class = Type(
+ default_value=MappingKernelManager,
+ config=True,
+ help='The kernel manager class to use.'
+ )
+ session_manager_class = Type(
+ default_value=SessionManager,
+ config=True,
+ help='The session manager class to use.'
+ )
+
+ config_manager_class = Type(
+ default_value=ConfigManager,
+ config = True,
+ help='The config manager class to use'
+ )
+
+ kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
+
+ kernel_spec_manager_class = Type(
+ default_value=KernelSpecManager,
+ config=True,
+ help="""
+ The kernel spec manager class to use. Should be a subclass
+ of `jupyter_client.kernelspec.KernelSpecManager`.
+
+ The Api of KernelSpecManager is provisional and might change
+ without warning between this version of Jupyter and the next stable one.
+ """
+ )
+
+ login_handler_class = Type(
+ default_value=LoginHandler,
+ klass=web.RequestHandler,
+ config=True,
+ help='The login handler class to use.',
+ )
+
+ logout_handler_class = Type(
+ default_value=LogoutHandler,
+ klass=web.RequestHandler,
+ config=True,
+ help='The logout handler class to use.',
+ )
+
+ trust_xheaders = Bool(False, config=True,
+ help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
+ "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
+ )
+
+ info_file = Unicode()
+
+ def _info_file_default(self):
+ info_file = "nbserver-%s.json" % os.getpid()
+ return os.path.join(self.runtime_dir, info_file)
+
+ pylab = Unicode('disabled', config=True,
+ help="""
+ DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
+ """
+ )
+ def _pylab_changed(self, name, old, new):
+ """when --pylab is specified, display a warning and exit"""
+ if new != 'warn':
+ backend = ' %s' % new
+ else:
+ backend = ''
+ self.log.error("Support for specifying --pylab on the command line has been removed.")
+ self.log.error(
+ "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
+ )
+ self.exit(1)
+
+ notebook_dir = Unicode(config=True,
+ help="The directory to use for notebooks and kernels."
+ )
+
+ def _notebook_dir_default(self):
+ if self.file_to_run:
+ return os.path.dirname(os.path.abspath(self.file_to_run))
+ else:
+ return py3compat.getcwd()
+
+ def _notebook_dir_validate(self, value, trait):
+ # Strip any trailing slashes
+ # *except* if it's root
+ _, path = os.path.splitdrive(value)
+ if path == os.sep:
+ return value
+
+ value = value.rstrip(os.sep)
+
+ if not os.path.isabs(value):
+ # If we receive a non-absolute path, make it absolute.
+ value = os.path.abspath(value)
+ if not os.path.isdir(value):
+ raise TraitError("No such notebook dir: %r" % value)
+ return value
+
+ def _notebook_dir_changed(self, name, old, new):
+ """Do a bit of validation of the notebook dir."""
+ # setting App.notebook_dir implies setting notebook and kernel dirs as well
+ self.config.FileContentsManager.root_dir = new
+ self.config.MappingKernelManager.root_dir = new
+
+ # TODO: Remove me in notebook 5.0
+ server_extensions = List(Unicode(), config=True,
+ help=("DEPRECATED use the nbserver_extensions dict instead")
+ )
+ def _server_extensions_changed(self, name, old, new):
+ self.log.warning("server_extensions is deprecated, use nbserver_extensions")
+ self.server_extensions = new
+
+ nbserver_extensions = Dict({}, config=True,
+ help=("Dict of Python modules to load as notebook server extensions."
+ "Entry values can be used to enable and disable the loading of"
+ "the extensions.")
+ )
+
+ reraise_server_extension_failures = Bool(
+ False,
+ config=True,
+ help="Reraise exceptions encountered loading server extensions?",
+ )
+
+ iopub_msg_rate_limit = Float(0, config=True, help="""(msg/sec)
+ Maximum rate at which messages can be sent on iopub before they are
+ limited.""")
+
+ iopub_data_rate_limit = Float(0, config=True, help="""(bytes/sec)
+ Maximum rate at which messages can be sent on iopub before they are
+ limited.""")
+
+ rate_limit_window = Float(1.0, config=True, help="""(sec) Time window used to
+ check the message and data rate limits.""")
+
+ def parse_command_line(self, argv=None):
+ super(NotebookApp, self).parse_command_line(argv)
+
+ if self.extra_args:
+ arg0 = self.extra_args[0]
+ f = os.path.abspath(arg0)
+ self.argv.remove(arg0)
+ if not os.path.exists(f):
+ self.log.critical("No such file or directory: %s", f)
+ self.exit(1)
+
+ # Use config here, to ensure that it takes higher priority than
+ # anything that comes from the config dirs.
+ c = Config()
+ if os.path.isdir(f):
+ c.NotebookApp.notebook_dir = f
+ elif os.path.isfile(f):
+ c.NotebookApp.file_to_run = f
+ self.update_config(c)
+
+ def init_configurables(self):
+ self.kernel_spec_manager = self.kernel_spec_manager_class(
+ parent=self,
+ )
+ self.kernel_manager = self.kernel_manager_class(
+ parent=self,
+ log=self.log,
+ connection_dir=self.runtime_dir,
+ kernel_spec_manager=self.kernel_spec_manager,
+ )
+ self.contents_manager = self.contents_manager_class(
+ parent=self,
+ log=self.log,
+ )
+ self.session_manager = self.session_manager_class(
+ parent=self,
+ log=self.log,
+ kernel_manager=self.kernel_manager,
+ contents_manager=self.contents_manager,
+ )
+ self.config_manager = self.config_manager_class(
+ parent=self,
+ log=self.log,
+ config_dir=os.path.join(self.config_dir, 'nbconfig'),
+ )
+
+ def init_logging(self):
+ # This prevents double log messages because tornado use a root logger that
+ # self.log is a child of. The logging module dipatches log messages to a log
+ # and all of its ancenstors until propagate is set to False.
+ self.log.propagate = False
+
+ for log in app_log, access_log, gen_log:
+ # consistent log output name (NotebookApp instead of tornado.access, etc.)
+ log.name = self.log.name
+ # hook up tornado 3's loggers to our app handlers
+ logger = logging.getLogger('tornado')
+ logger.propagate = True
+ logger.parent = self.log
+ logger.setLevel(self.log.level)
+
+ def init_webapp(self):
+ """initialize tornado webapp and httpserver"""
+ self.tornado_settings['allow_origin'] = self.allow_origin
+ if self.allow_origin_pat:
+ self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
+ self.tornado_settings['allow_credentials'] = self.allow_credentials
+ self.tornado_settings['cookie_options'] = self.cookie_options
+ # ensure default_url starts with base_url
+ if not self.default_url.startswith(self.base_url):
+ self.default_url = url_path_join(self.base_url, self.default_url)
+
+ self.web_app = NotebookWebApplication(
+ self, self.kernel_manager, self.contents_manager,
+ self.session_manager, self.kernel_spec_manager,
+ self.config_manager,
+ self.log, self.base_url, self.default_url, self.tornado_settings,
+ self.jinja_environment_options
+ )
+ ssl_options = self.ssl_options
+ if self.certfile:
+ ssl_options['certfile'] = self.certfile
+ if self.keyfile:
+ ssl_options['keyfile'] = self.keyfile
+ if self.client_ca:
+ ssl_options['ca_certs'] = self.client_ca
+ if not ssl_options:
+ # None indicates no SSL config
+ ssl_options = None
+ else:
+ # SSL may be missing, so only import it if it's to be used
+ import ssl
+ # Disable SSLv3 by default, since its use is discouraged.
+ ssl_options.setdefault('ssl_version', ssl.PROTOCOL_TLSv1)
+ if ssl_options.get('ca_certs', False):
+ ssl_options.setdefault('cert_reqs', ssl.CERT_REQUIRED)
+
+ self.login_handler_class.validate_security(self, ssl_options=ssl_options)
+ self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
+ xheaders=self.trust_xheaders)
+
+ success = None
+ for port in random_ports(self.port, self.port_retries+1):
+ try:
+ self.http_server.listen(port, self.ip)
+ except socket.error as e:
+ if e.errno == errno.EADDRINUSE:
+ self.log.info('The port %i is already in use, trying another port.' % port)
+ continue
+ elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
+ self.log.warn("Permission to listen on port %i denied" % port)
+ continue
+ else:
+ raise
+ else:
+ self.port = port
+ success = True
+ break
+ if not success:
+ self.log.critical('ERROR: the notebook server could not be started because '
+ 'no available port could be found.')
+ self.exit(1)
+
+ @property
+ def display_url(self):
+ ip = self.ip if self.ip else '[all ip addresses on your system]'
+ return self._url(ip)
+
+ @property
+ def connection_url(self):
+ ip = self.ip if self.ip else 'localhost'
+ return self._url(ip)
+
+ def _url(self, ip):
+ proto = 'https' if self.certfile else 'http'
+ return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
+
+ def init_terminals(self):
+ try:
+ from .terminal import initialize
+ initialize(self.web_app, self.notebook_dir, self.connection_url)
+ self.web_app.settings['terminals_available'] = True
+ except ImportError as e:
+ log = self.log.debug if sys.platform == 'win32' else self.log.warn
+ log("Terminals not available (error was %s)", e)
+
+ def init_signal(self):
+ if not sys.platform.startswith('win') and sys.stdin.isatty():
+ signal.signal(signal.SIGINT, self._handle_sigint)
+ signal.signal(signal.SIGTERM, self._signal_stop)
+ if hasattr(signal, 'SIGUSR1'):
+ # Windows doesn't support SIGUSR1
+ signal.signal(signal.SIGUSR1, self._signal_info)
+ if hasattr(signal, 'SIGINFO'):
+ # only on BSD-based systems
+ signal.signal(signal.SIGINFO, self._signal_info)
+
+ def _handle_sigint(self, sig, frame):
+ """SIGINT handler spawns confirmation dialog"""
+ # register more forceful signal handler for ^C^C case
+ signal.signal(signal.SIGINT, self._signal_stop)
+ # request confirmation dialog in bg thread, to avoid
+ # blocking the App
+ thread = threading.Thread(target=self._confirm_exit)
+ thread.daemon = True
+ thread.start()
+
+ def _restore_sigint_handler(self):
+ """callback for restoring original SIGINT handler"""
+ signal.signal(signal.SIGINT, self._handle_sigint)
+
+ def _confirm_exit(self):
+ """confirm shutdown on ^C
+
+ A second ^C, or answering 'y' within 5s will cause shutdown,
+ otherwise original SIGINT handler will be restored.
+
+ This doesn't work on Windows.
+ """
+ info = self.log.info
+ info('interrupted')
+ print(self.notebook_info())
+ sys.stdout.write("Shutdown this notebook server (y/[n])? ")
+ sys.stdout.flush()
+ r,w,x = select.select([sys.stdin], [], [], 5)
+ if r:
+ line = sys.stdin.readline()
+ if line.lower().startswith('y') and 'n' not in line.lower():
+ self.log.critical("Shutdown confirmed")
+ ioloop.IOLoop.current().stop()
+ return
+ else:
+ print("No answer for 5s:", end=' ')
+ print("resuming operation...")
+ # no answer, or answer is no:
+ # set it back to original SIGINT handler
+ # use IOLoop.add_callback because signal.signal must be called
+ # from main thread
+ ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
+
+ def _signal_stop(self, sig, frame):
+ self.log.critical("received signal %s, stopping", sig)
+ ioloop.IOLoop.current().stop()
+
+ def _signal_info(self, sig, frame):
+ print(self.notebook_info())
+
+ def init_components(self):
+ """Check the components submodule, and warn if it's unclean"""
+ # TODO: this should still check, but now we use bower, not git submodule
+ pass
+
+ def init_server_extensions(self):
+ """Load any extensions specified by config.
+
+ Import the module, then call the load_jupyter_server_extension function,
+ if one exists.
+
+ The extension API is experimental, and may change in future releases.
+ """
+
+ # TODO: Remove me in notebook 5.0
+ for modulename in self.server_extensions:
+ # Don't override disable state of the extension if it already exist
+ # in the new traitlet
+ if not modulename in self.nbserver_extensions:
+ self.nbserver_extensions[modulename] = True
+
+ for modulename in self.nbserver_extensions:
+ if self.nbserver_extensions[modulename]:
+ try:
+ mod = importlib.import_module(modulename)
+ func = getattr(mod, 'load_jupyter_server_extension', None)
+ if func is not None:
+ func(self)
+ except Exception:
+ if self.reraise_server_extension_failures:
+ raise
+ self.log.warning("Error loading server extension %s", modulename,
+ exc_info=True)
+
+ def init_mime_overrides(self):
+ # On some Windows machines, an application has registered an incorrect
+ # mimetype for CSS in the registry. Tornado uses this when serving
+ # .css files, causing browsers to reject the stylesheet. We know the
+ # mimetype always needs to be text/css, so we override it here.
+ mimetypes.add_type('text/css', '.css')
+
+ @catch_config_error
+ def initialize(self, argv=None):
+ super(NotebookApp, self).initialize(argv)
+ self.init_logging()
+ if self._dispatching:
+ return
+ self.init_configurables()
+ self.init_components()
+ self.init_webapp()
+ self.init_terminals()
+ self.init_signal()
+ self.init_server_extensions()
+ self.init_mime_overrides()
+
+ def cleanup_kernels(self):
+ """Shutdown all kernels.
+
+ The kernels will shutdown themselves when this process no longer exists,
+ but explicit shutdown allows the KernelManagers to cleanup the connection files.
+ """
+ self.log.info('Shutting down kernels')
+ self.kernel_manager.shutdown_all()
+
+ def notebook_info(self):
+ "Return the current working directory and the server url information"
+ info = self.contents_manager.info_string() + "\n"
+ info += "%d active kernels \n" % len(self.kernel_manager._kernels)
+ return info + "The Jupyter Notebook is running at: %s" % self.display_url
+
+ def server_info(self):
+ """Return a JSONable dict of information about this server."""
+ return {'url': self.connection_url,
+ 'hostname': self.ip if self.ip else 'localhost',
+ 'port': self.port,
+ 'secure': bool(self.certfile),
+ 'base_url': self.base_url,
+ 'notebook_dir': os.path.abspath(self.notebook_dir),
+ 'pid': os.getpid()
+ }
+
+ def write_server_info_file(self):
+ """Write the result of server_info() to the JSON file info_file."""
+ with open(self.info_file, 'w') as f:
+ json.dump(self.server_info(), f, indent=2)
+
+ def remove_server_info_file(self):
+ """Remove the nbserver-<pid>.json file created for this server.
+
+ Ignores the error raised when the file has already been removed.
+ """
+ try:
+ os.unlink(self.info_file)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+
+ def start(self):
+ """ Start the Notebook server app, after initialization
+
+ This method takes no arguments so all configuration and initialization
+ must be done prior to calling this method."""
+ super(NotebookApp, self).start()
+
+ info = self.log.info
+ for line in self.notebook_info().split("\n"):
+ info(line)
+ info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
+
+ self.write_server_info_file()
+
+ if self.open_browser or self.file_to_run:
+ try:
+ browser = webbrowser.get(self.browser or None)
+ except webbrowser.Error as e:
+ self.log.warn('No web browser found: %s.' % e)
+ browser = None
+
+ if self.file_to_run:
+ if not os.path.exists(self.file_to_run):
+ self.log.critical("%s does not exist" % self.file_to_run)
+ self.exit(1)
+
+ relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
+ uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep)))
+ else:
+ # default_url contains base_url, but so does connection_url
+ uri = self.default_url[len(self.base_url):]
+ if browser:
+ b = lambda : browser.open(url_path_join(self.connection_url, uri),
+ new=2)
+ threading.Thread(target=b).start()
+
+ self.io_loop = ioloop.IOLoop.current()
+ if sys.platform.startswith('win'):
+ # add no-op to wake every 5s
+ # to handle signals that may be ignored by the inner loop
+ pc = ioloop.PeriodicCallback(lambda : None, 5000)
+ pc.start()
+ try:
+ self.io_loop.start()
+ except KeyboardInterrupt:
+ info("Interrupted...")
+ finally:
+ self.cleanup_kernels()
+ self.remove_server_info_file()
+
+ def stop(self):
+ def _stop():
+ self.http_server.stop()
+ self.io_loop.stop()
+ self.io_loop.add_callback(_stop)
+
+
+def list_running_servers(runtime_dir=None):
+ """Iterate over the server info files of running notebook servers.
+
+ Given a runtime directory, find nbserver-* files in the security directory,
+ and yield dicts of their information, each one pertaining to
+ a currently running notebook server instance.
+ """
+ if runtime_dir is None:
+ runtime_dir = jupyter_runtime_dir()
+
+ # The runtime dir might not exist
+ if not os.path.isdir(runtime_dir):
+ return
+
+ for file in os.listdir(runtime_dir):
+ if file.startswith('nbserver-'):
+ with io.open(os.path.join(runtime_dir, file), encoding='utf-8') as f:
+ info = json.load(f)
+
+ # Simple check whether that process is really still running
+ # Also remove leftover files from IPython 2.x without a pid field
+ if ('pid' in info) and check_pid(info['pid']):
+ yield info
+ else:
+ # If the process has died, try to delete its info file
+ try:
+ os.unlink(file)
+ except OSError:
+ pass # TODO: This should warn or log or something
+#-----------------------------------------------------------------------------
+# Main entry point
+#-----------------------------------------------------------------------------
+
+main = launch_new_instance = NotebookApp.launch_instance
+
diff --git a/notebook/serverextensions.py b/notebook/serverextensions.py
new file mode 100644
index 0000000..1b9a188
--- /dev/null
+++ b/notebook/serverextensions.py
@@ -0,0 +1,341 @@
+# coding: utf-8
+"""Utilities for installing server extensions for the notebook"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from __future__ import print_function
+
+import importlib
+import sys
+
+
+from jupyter_core.paths import jupyter_config_path
+from ._version import __version__
+from .nbextensions import (
+ JupyterApp, BaseNBExtensionApp, _get_config_dir,
+ GREEN_ENABLED, RED_DISABLED,
+ GREEN_OK, RED_X,
+)
+
+from traitlets import Bool
+from traitlets.utils.importstring import import_item
+from traitlets.config.manager import BaseJSONConfigManager
+
+
+# ------------------------------------------------------------------------------
+# Public API
+# ------------------------------------------------------------------------------
+class ArgumentConflict(ValueError):
+ pass
+
+
+def toggle_serverextension_python(import_name, enabled=None, parent=None,
+ user=True, sys_prefix=False, logger=None):
+ """Toggle a server extension.
+
+ By default, toggles the extension in the system-wide Jupyter configuration
+ location (e.g. /usr/local/etc/jupyter).
+
+ Parameters
+ ----------
+
+ import_name : str
+ Importable Python module (dotted-notation) exposing the magic-named
+ `load_jupyter_server_extension` function
+ enabled : bool [default: None]
+ Toggle state for the extension. Set to None to toggle, True to enable,
+ and False to disable the extension.
+ parent : Configurable [default: None]
+ user : bool [default: True]
+ Toggle in the user's configuration location (e.g. ~/.jupyter).
+ sys_prefix : bool [default: False]
+ Toggle in the current Python environment's configuration location
+ (e.g. ~/.envs/my-env/etc/jupyter). Will override `user`.
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+ user = False if sys_prefix else user
+ config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
+ cm = BaseJSONConfigManager(parent=parent, config_dir=config_dir)
+ cfg = cm.get("jupyter_notebook_config")
+ server_extensions = (
+ cfg.setdefault("NotebookApp", {})
+ .setdefault("nbserver_extensions", {})
+ )
+
+ old_enabled = server_extensions.get(import_name, None)
+ new_enabled = enabled if enabled is not None else not old_enabled
+
+ if logger:
+ if new_enabled:
+ logger.info(u"Enabling: %s" % (import_name))
+ else:
+ logger.info(u"Disabling: %s" % (import_name))
+
+ server_extensions[import_name] = new_enabled
+
+ if logger:
+ logger.info(u"- Writing config: {}".format(config_dir))
+
+ cm.update("jupyter_notebook_config", cfg)
+
+ if new_enabled:
+ validate_serverextension(import_name, logger)
+
+
+def validate_serverextension(import_name, logger=None):
+ """Assess the health of an installed server extension
+
+ Returns a list of validation warnings.
+
+ Parameters
+ ----------
+
+ import_name : str
+ Importable Python module (dotted-notation) exposing the magic-named
+ `load_jupyter_server_extension` function
+ logger : Jupyter logger [optional]
+ Logger instance to use
+ """
+
+ warnings = []
+ infos = []
+
+ func = None
+
+ if logger:
+ logger.info(" - Validating...")
+
+ try:
+ mod = importlib.import_module(import_name)
+ func = getattr(mod, 'load_jupyter_server_extension', None)
+ except Exception:
+ logger.warning("Error loading server extension %s", import_name)
+
+ import_msg = u" {} is {} importable?"
+ if func is not None:
+ infos.append(import_msg.format(GREEN_OK, import_name))
+ else:
+ warnings.append(import_msg.format(RED_X, import_name))
+
+ post_mortem = u" {} {} {}"
+ if logger:
+ if warnings:
+ [logger.info(info) for info in infos]
+ [logger.warn(warning) for warning in warnings]
+ else:
+ logger.info(post_mortem.format(import_name, "", GREEN_OK))
+
+ return warnings
+
+
+# ----------------------------------------------------------------------
+# Applications
+# ----------------------------------------------------------------------
+
+flags = {}
+flags.update(JupyterApp.flags)
+flags.pop("y", None)
+flags.pop("generate-config", None)
+flags.update({
+ "user" : ({
+ "ToggleServerExtensionApp" : {
+ "user" : True,
+ }}, "Perform the operation for the current user"
+ ),
+ "system" : ({
+ "ToggleServerExtensionApp" : {
+ "user" : False,
+ "sys_prefix": False,
+ }}, "Perform the operation system-wide"
+ ),
+ "sys-prefix" : ({
+ "ToggleServerExtensionApp" : {
+ "sys_prefix" : True,
+ }}, "Use sys.prefix as the prefix for installing server extensions"
+ ),
+ "py" : ({
+ "ToggleServerExtensionApp" : {
+ "python" : True,
+ }}, "Install from a Python package"
+ ),
+})
+flags['python'] = flags['py']
+
+
+class ToggleServerExtensionApp(BaseNBExtensionApp):
+ """A base class for enabling/disabling extensions"""
+ name = "jupyter serverextension enable/disable"
+ description = "Enable/disable a server extension using frontend configuration files."
+
+ aliases = {}
+ flags = flags
+
+ user = Bool(True, config=True, help="Whether to do a user install")
+ sys_prefix = Bool(False, config=True, help="Use the sys.prefix as the prefix")
+ python = Bool(False, config=True, help="Install from a Python package")
+
+ def toggle_server_extension(self, import_name):
+ """Change the status of a named server extension.
+
+ Uses the value of `self._toggle_value`.
+
+ Parameters
+ ---------
+
+ import_name : str
+ Importable Python module (dotted-notation) exposing the magic-named
+ `load_jupyter_server_extension` function
+ """
+ toggle_serverextension_python(
+ import_name, self._toggle_value, parent=self, user=self.user,
+ sys_prefix=self.sys_prefix, logger=self.log)
+
+ def toggle_server_extension_python(self, package):
+ """Change the status of some server extensions in a Python package.
+
+ Uses the value of `self._toggle_value`.
+
+ Parameters
+ ---------
+
+ package : str
+ Importable Python module exposing the
+ magic-named `_jupyter_server_extension_paths` function
+ """
+ m, server_exts = _get_server_extension_metadata(package)
+ for server_ext in server_exts:
+ module = server_ext['module']
+ self.toggle_server_extension(module)
+
+ def start(self):
+ """Perform the App's actions as configured"""
+ if not self.extra_args:
+ sys.exit('Please specify a server extension/package to enable or disable')
+ for arg in self.extra_args:
+ if self.python:
+ self.toggle_server_extension_python(arg)
+ else:
+ self.toggle_server_extension(arg)
+
+
+class EnableServerExtensionApp(ToggleServerExtensionApp):
+ """An App that enables (and validates) Server Extensions"""
+ name = "jupyter serverextension enable"
+ description = """
+ Enable a serverextension in configuration.
+
+ Usage
+ jupyter serverextension enable [--system|--sys-prefix]
+ """
+ _toggle_value = True
+
+
+class DisableServerExtensionApp(ToggleServerExtensionApp):
+ """An App that disables Server Extensions"""
+ name = "jupyter serverextension disable"
+ description = """
+ Disable a serverextension in configuration.
+
+ Usage
+ jupyter serverextension disable [--system|--sys-prefix]
+ """
+ _toggle_value = False
+
+
+class ListServerExtensionsApp(BaseNBExtensionApp):
+ """An App that lists (and validates) Server Extensions"""
+ name = "jupyter serverextension list"
+ version = __version__
+ description = "List all server extensions known by the configuration system"
+
+ def list_server_extensions(self):
+ """List all enabled and disabled server extensions, by config path
+
+ Enabled extensions are validated, potentially generating warnings.
+ """
+ config_dirs = jupyter_config_path()
+ for config_dir in config_dirs:
+ cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
+ data = cm.get("jupyter_notebook_config")
+ server_extensions = (
+ data.setdefault("NotebookApp", {})
+ .setdefault("nbserver_extensions", {})
+ )
+ if server_extensions:
+ print(u'config dir: {}'.format(config_dir))
+ for import_name, enabled in server_extensions.items():
+ print(u' {} {}'.format(
+ import_name,
+ GREEN_ENABLED if enabled else RED_DISABLED))
+ validate_serverextension(import_name, self.log)
+
+ def start(self):
+ """Perform the App's actions as configured"""
+ self.list_server_extensions()
+
+
+_examples = """
+jupyter serverextension list # list all configured server extensions
+jupyter serverextension enable --py <packagename> # enable all server extensions in a Python package
+jupyter serverextension disable --py <packagename> # disable all server extensions in a Python package
+"""
+
+
+class ServerExtensionApp(BaseNBExtensionApp):
+ """Root level server extension app"""
+ name = "jupyter serverextension"
+ version = __version__
+ description = "Work with Jupyter server extensions"
+ examples = _examples
+
+ subcommands = dict(
+ enable=(EnableServerExtensionApp, "Enable an server extension"),
+ disable=(DisableServerExtensionApp, "Disable an server extension"),
+ list=(ListServerExtensionsApp, "List server extensions")
+ )
+
+ def start(self):
+ """Perform the App's actions as configured"""
+ super(ServerExtensionApp, self).start()
+
+ # The above should have called a subcommand and raised NoStart; if we
+ # get here, it didn't, so we should self.log.info a message.
+ subcmds = ", ".join(sorted(self.subcommands))
+ sys.exit("Please supply at least one subcommand: %s" % subcmds)
+
+
+main = ServerExtensionApp.launch_instance
+
+# ------------------------------------------------------------------------------
+# Private API
+# ------------------------------------------------------------------------------
+
+
+def _get_server_extension_metadata(module):
+ """Load server extension metadata from a module.
+
+ Returns a tuple of (
+ the package as loaded
+ a list of server extension specs: [
+ {
+ "module": "mockextension"
+ }
+ ]
+ )
+
+ Parameters
+ ----------
+
+ module : str
+ Importable Python module exposing the
+ magic-named `_jupyter_server_extension_paths` function
+ """
+ m = import_item(module)
+ if not hasattr(m, '_jupyter_server_extension_paths'):
+ raise KeyError(u'The Python module {} does not include any valid server extensions'.format(module))
+ return m, m._jupyter_server_extension_paths()
+
+if __name__ == '__main__':
+ main()
diff --git a/notebook/services/__init__.py b/notebook/services/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/__init__.py
diff --git a/notebook/services/api/__init__.py b/notebook/services/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/api/__init__.py
diff --git a/notebook/services/api/api.yaml b/notebook/services/api/api.yaml
new file mode 100644
index 0000000..1c8b7c9
--- /dev/null
+++ b/notebook/services/api/api.yaml
@@ -0,0 +1,365 @@
+swagger: '2.0'
+info:
+ title: Jupyter Notebook API
+ description: Notebook API
+ version: "4"
+ contact:
+ name: Jupyter Project
+ url: jupyter.org
+# will be prefixed to all paths
+basePath: /api
+produces:
+ - application/json
+consumes:
+ - application/json
+parameters:
+ kernel:
+ name: kernel
+ required: true
+ in: path
+ description: kernel uuid
+ type: string
+ format: uuid
+ session:
+ name: session
+ required: true
+ in: path
+ description: session uuid
+ type: string
+ format: uuid
+
+paths:
+ /sessions/{session}:
+ parameters:
+ - $ref: '#/parameters/session'
+ get:
+ summary: Get session
+ tags:
+ - sessions
+ responses:
+ 200:
+ description: Session
+ schema:
+ $ref: '#/definitions/Session'
+ patch:
+ summary: This can be used to rename the notebook, or move it to a new directory.
+ tags:
+ - sessions
+ parameters:
+ - name: model
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ notebook:
+ type: object
+ properties:
+ path:
+ type: string
+ format: path
+ description: new path for notebook
+ responses:
+ 200:
+ description: Session
+ schema:
+ $ref: '#/definitions/Session'
+ 400:
+ description: No data provided
+ delete:
+ summary: Delete a session
+ tags:
+ - sessions
+ responses:
+ 204:
+ description: Session (and kernel) were deleted
+ 410:
+ description: Kernel was deleted before the session, and the session was *not* deleted (TODO - check to make sure session wasn't deleted)
+ /sessions:
+ get:
+ summary: List available sessions
+ tags:
+ - sessions
+ responses:
+ 200:
+ description: List of current sessions
+ schema:
+ type: array
+ items:
+ $ref: '#/definitions/Session'
+ post:
+ summary: Create a new session, or return an existing session if a session for the notebook path already exists
+ tags:
+ - sessions
+ parameters:
+ - name: session
+ in: body
+ schema:
+ type: object
+ properties:
+ notebook:
+ type: object
+ required:
+ - path
+ properties:
+ path:
+ type: string
+ description: path to notebook file
+ kernel:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Kernel spec name, defaults to default kernel spec
+ responses:
+ 201:
+ description: Session created or returned
+ schema:
+ $ref: '#/definitions/Session'
+ headers:
+ Location:
+ description: URL for session commands
+ type: string
+ format: url
+ 501:
+ description: Kernel not available
+ schema:
+ type: object
+ description: error message
+ properties:
+ message:
+ type: string
+ short_message:
+ type: string
+
+ /kernels:
+ get:
+ summary: List the JSON data for all kernels that are currently running
+ tags:
+ - kernels
+ responses:
+ 200:
+ description: List of currently-running kernel uuids
+ schema:
+ type: array
+ items:
+ $ref: '#/definitions/Kernel'
+ post:
+ summary: Start a kernel and return the uuid
+ tags:
+ - kernels
+ parameters:
+ - name: name
+ in: body
+ description: Kernel spec name (defaults to default kernel spec for server)
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ responses:
+ 201:
+ description: Kernel started
+ headers:
+ Location:
+ description: URL for kernel commands
+ type: string
+ format: url
+ /kernels/{kernel}:
+ parameters:
+ - $ref: '#/parameters/kernel'
+ get:
+ summary: Get kernel information
+ tags:
+ - kernels
+ responses:
+ 200:
+ description: Kernel information
+ schema:
+ $ref: '#/definitions/Kernel'
+ delete:
+ summary: Kill a kernel and delete the kernel id
+ tags:
+ - kernels
+ responses:
+ 204:
+ description: Kernel deleted
+ /kernels/{kernel}/interrupt:
+ parameters:
+ - $ref: '#/parameters/kernel'
+ post:
+ summary: Interrupt a kernel
+ tags:
+ - kernels
+ responses:
+ 204:
+ description: Kernel interrupted
+ /kernels/{kernel}/restart:
+ parameters:
+ - $ref: '#/parameters/kernel'
+ post:
+ summary: Restart a kernel
+ tags:
+ - kernels
+ responses:
+ 200:
+ description: Kernel interrupted
+ headers:
+ Location:
+ description: URL for kernel commands
+ type: string
+ format: url
+ schema:
+ $ref: '#/definitions/Kernel'
+
+ /kernelspecs:
+ get:
+ summary: List kernel specs
+ tags:
+ - kernelspecs
+ responses:
+ 200:
+ description: Kernel specs
+ schema:
+ type: object
+ properties:
+ default:
+ type: string
+ description: Default kernel name
+ kernelspecs:
+ type: array
+ items:
+ $ref: '#/definitions/KernelSpec'
+ /kernelspecs/{kernel}:
+ parameters:
+ - $ref: '#/parameters/kernel'
+ get:
+ summary: Kernel information
+ tags:
+ - kernelspecs
+ responses:
+ 200:
+ description: The contents of kernel.json
+ schema:
+ $ref: '#/definitions/KernelSpec'
+ 404:
+ description: Kernel spec not found
+ /kernelspecs/{kernel}/{filename}:
+ get:
+ summary: Retrieve a file from the kernel directory
+ tags:
+ - kernelspecs
+ parameters:
+ - name: kernel
+ in: path
+ description: Kernel uuid
+ type: string
+ required: true
+ - name: filename
+ in: path
+ description: filename
+ type: string
+ required: true
+ responses:
+ 200:
+ description: file
+
+definitions:
+ KernelSpec:
+ description: Kernel spec (contents of kernel.json)
+ properties:
+ name:
+ type: string
+ description: Unique name for kernel
+ spec:
+ $ref: '#/definitions/KernelSpecFile'
+ description: Kernel spec json file
+ resources:
+ type: object
+ properties:
+ kernel.js:
+ type: string
+ format: filename
+ description: path for kernel.js file
+ kernel.css:
+ type: string
+ format: filename
+ description: path for kernel.css file
+ logo-*:
+ type: string
+ format: filename
+ description: path for logo file. Logo filenames are of the form `logo-widthxheight`
+ KernelSpecFile:
+ description: Kernel spec json file
+ required:
+ - argv
+ - display_name
+ - language
+ properties:
+ language:
+ type: string
+ description: The programming language which this kernel runs. This will be stored in notebook metadata.
+ argv:
+ type: array
+ description: A list of command line arguments used to start the kernel. The text `{connection_file}` in any argument will be replaced with the path to the connection file.
+ items:
+ type: string
+ display_name:
+ type: string
+ description: The kernel's name as it should be displayed in the UI. Unlike the kernel name used in the API, this can contain arbitrary unicode characters.
+ codemirror_mode:
+ type: string
+ description: Codemirror mode. Can be a string *or* an valid Codemirror mode object. This defaults to the string from the `language` property.
+ env:
+ type: object
+ description: A dictionary of environment variables to set for the kernel. These will be added to the current environment variables.
+ additionalProperties:
+ type: string
+ help_links:
+ type: array
+ description: Help items to be displayed in the help menu in the notebook UI.
+ items:
+ type: object
+ required:
+ - text
+ - url
+ properties:
+ text:
+ type: string
+ description: menu item link text
+ url:
+ type: string
+ format: URL
+ description: menu item link url
+ Kernel:
+ description: Kernel information
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: string
+ format: uuid
+ description: uuid of kernel
+ name:
+ type: string
+ description: kernel spec name
+ Session:
+ description: A session
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ notebook:
+ type: object
+ properties:
+ path:
+ type: string
+ description: path to notebook
+ kernel:
+ $ref: '#/definitions/Kernel'
+
+
+
+
diff --git a/notebook/services/api/handlers.py b/notebook/services/api/handlers.py
new file mode 100644
index 0000000..76c662e
--- /dev/null
+++ b/notebook/services/api/handlers.py
@@ -0,0 +1,23 @@
+"""Tornado handlers for api specifications."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from tornado import web
+from ...base.handlers import IPythonHandler
+import os
+
+class APISpecHandler(web.StaticFileHandler, IPythonHandler):
+
+ def initialize(self):
+ web.StaticFileHandler.initialize(self, path=os.path.dirname(__file__))
+
+ @web.authenticated
+ def get(self):
+ self.log.warn("Serving api spec (experimental, incomplete)")
+ self.set_header('Content-Type', 'text/x-yaml')
+ return web.StaticFileHandler.get(self, 'api.yaml')
+
+default_handlers = [
+ (r"/api/spec.yaml", APISpecHandler)
+]
diff --git a/notebook/services/config/__init__.py b/notebook/services/config/__init__.py
new file mode 100644
index 0000000..d8d9380
--- /dev/null
+++ b/notebook/services/config/__init__.py
@@ -0,0 +1 @@
+from .manager import ConfigManager
diff --git a/notebook/services/config/handlers.py b/notebook/services/config/handlers.py
new file mode 100644
index 0000000..7656464
--- /dev/null
+++ b/notebook/services/config/handlers.py
@@ -0,0 +1,43 @@
+"""Tornado handlers for frontend config storage."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+import json
+import os
+import io
+import errno
+from tornado import web
+
+from ipython_genutils.py3compat import PY3
+from ...base.handlers import APIHandler, json_errors
+
+class ConfigHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ def get(self, section_name):
+ self.set_header("Content-Type", 'application/json')
+ self.finish(json.dumps(self.config_manager.get(section_name)))
+
+ @web.authenticated
+ @json_errors
+ def put(self, section_name):
+ data = self.get_json_body() # Will raise 400 if content is not valid JSON
+ self.config_manager.set(section_name, data)
+ self.set_status(204)
+
+ @web.authenticated
+ @json_errors
+ def patch(self, section_name):
+ new_data = self.get_json_body()
+ section = self.config_manager.update(section_name, new_data)
+ self.finish(json.dumps(section))
+
+
+# URL to handler mappings
+
+section_name_regex = r"(?P<section_name>\w+)"
+
+default_handlers = [
+ (r"/api/config/%s" % section_name_regex, ConfigHandler),
+]
diff --git a/notebook/services/config/manager.py b/notebook/services/config/manager.py
new file mode 100644
index 0000000..2258e4f
--- /dev/null
+++ b/notebook/services/config/manager.py
@@ -0,0 +1,51 @@
+"""Manager to read and modify frontend config data in JSON files.
+"""
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import os.path
+
+from traitlets.config.manager import BaseJSONConfigManager, recursive_update
+from jupyter_core.paths import jupyter_config_dir, jupyter_config_path
+from traitlets import Unicode, Instance, List
+from traitlets.config import LoggingConfigurable
+
+
+class ConfigManager(LoggingConfigurable):
+ """Config Manager used for storing notebook frontend config"""
+
+ # Public API
+
+ def get(self, section_name):
+ """Get the config from all config sections."""
+ config = {}
+ # step through back to front, to ensure front of the list is top priority
+ for p in self.read_config_path[::-1]:
+ cm = BaseJSONConfigManager(config_dir=p)
+ recursive_update(config, cm.get(section_name))
+ return config
+
+ def set(self, section_name, data):
+ """Set the config only to the user's config."""
+ return self.write_config_manager.set(section_name, data)
+
+ def update(self, section_name, new_data):
+ """Update the config only to the user's config."""
+ return self.write_config_manager.update(section_name, new_data)
+
+ # Private API
+
+ read_config_path = List(Unicode())
+ def _read_config_path_default(self):
+ return [os.path.join(p, 'nbconfig') for p in jupyter_config_path()]
+
+ write_config_dir = Unicode()
+ def _write_config_dir_default(self):
+ return os.path.join(jupyter_config_dir(), 'nbconfig')
+
+ write_config_manager = Instance(BaseJSONConfigManager)
+ def _write_config_manager_default(self):
+ return BaseJSONConfigManager(config_dir=self.write_config_dir)
+
+ def _write_config_dir_changed(self, name, old, new):
+ self.write_config_manager = BaseJSONConfigManager(config_dir=self.write_config_dir)
diff --git a/notebook/services/config/tests/__init__.py b/notebook/services/config/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/config/tests/__init__.py
diff --git a/notebook/services/config/tests/test_config_api.py b/notebook/services/config/tests/test_config_api.py
new file mode 100644
index 0000000..a7778d1
--- /dev/null
+++ b/notebook/services/config/tests/test_config_api.py
@@ -0,0 +1,68 @@
+# coding: utf-8
+"""Test the config webservice API."""
+
+import json
+
+import requests
+
+from notebook.utils import url_path_join
+from notebook.tests.launchnotebook import NotebookTestBase
+
+
+class ConfigAPI(object):
+ """Wrapper for notebook API calls."""
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def _req(self, verb, section, body=None):
+ response = requests.request(verb,
+ url_path_join(self.base_url, 'api/config', section),
+ data=body,
+ )
+ response.raise_for_status()
+ return response
+
+ def get(self, section):
+ return self._req('GET', section)
+
+ def set(self, section, values):
+ return self._req('PUT', section, json.dumps(values))
+
+ def modify(self, section, values):
+ return self._req('PATCH', section, json.dumps(values))
+
+class APITest(NotebookTestBase):
+ """Test the config web service API"""
+ def setUp(self):
+ self.config_api = ConfigAPI(self.base_url())
+
+ def test_create_retrieve_config(self):
+ sample = {'foo': 'bar', 'baz': 73}
+ r = self.config_api.set('example', sample)
+ self.assertEqual(r.status_code, 204)
+
+ r = self.config_api.get('example')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.json(), sample)
+
+ def test_modify(self):
+ sample = {'foo': 'bar', 'baz': 73,
+ 'sub': {'a': 6, 'b': 7}, 'sub2': {'c': 8}}
+ self.config_api.set('example', sample)
+
+ r = self.config_api.modify('example', {'foo': None, # should delete foo
+ 'baz': 75,
+ 'wib': [1,2,3],
+ 'sub': {'a': 8, 'b': None, 'd': 9},
+ 'sub2': {'c': None} # should delete sub2
+ })
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.json(), {'baz': 75, 'wib': [1,2,3],
+ 'sub': {'a': 8, 'd': 9}})
+
+ def test_get_unknown(self):
+ # We should get an empty config dictionary instead of a 404
+ r = self.config_api.get('nonexistant')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.json(), {})
+
diff --git a/notebook/services/contents/__init__.py b/notebook/services/contents/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/contents/__init__.py
diff --git a/notebook/services/contents/checkpoints.py b/notebook/services/contents/checkpoints.py
new file mode 100644
index 0000000..c29a669
--- /dev/null
+++ b/notebook/services/contents/checkpoints.py
@@ -0,0 +1,142 @@
+"""
+Classes for managing Checkpoints.
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from tornado.web import HTTPError
+
+from traitlets.config.configurable import LoggingConfigurable
+
+
+class Checkpoints(LoggingConfigurable):
+ """
+ Base class for managing checkpoints for a ContentsManager.
+
+ Subclasses are required to implement:
+
+ create_checkpoint(self, contents_mgr, path)
+ restore_checkpoint(self, contents_mgr, checkpoint_id, path)
+ rename_checkpoint(self, checkpoint_id, old_path, new_path)
+ delete_checkpoint(self, checkpoint_id, path)
+ list_checkpoints(self, path)
+ """
+ def create_checkpoint(self, contents_mgr, path):
+ """Create a checkpoint."""
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
+ """Restore a checkpoint"""
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def rename_checkpoint(self, checkpoint_id, old_path, new_path):
+ """Rename a single checkpoint from old_path to new_path."""
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def delete_checkpoint(self, checkpoint_id, path):
+ """delete a checkpoint for a file"""
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def list_checkpoints(self, path):
+ """Return a list of checkpoints for a given file"""
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def rename_all_checkpoints(self, old_path, new_path):
+ """Rename all checkpoints for old_path to new_path."""
+ for cp in self.list_checkpoints(old_path):
+ self.rename_checkpoint(cp['id'], old_path, new_path)
+
+ def delete_all_checkpoints(self, path):
+ """Delete all checkpoints for the given path."""
+ for checkpoint in self.list_checkpoints(path):
+ self.delete_checkpoint(checkpoint['id'], path)
+
+
+class GenericCheckpointsMixin(object):
+ """
+ Helper for creating Checkpoints subclasses that can be used with any
+ ContentsManager.
+
+ Provides a ContentsManager-agnostic implementation of `create_checkpoint`
+ and `restore_checkpoint` in terms of the following operations:
+
+ - create_file_checkpoint(self, content, format, path)
+ - create_notebook_checkpoint(self, nb, path)
+ - get_file_checkpoint(self, checkpoint_id, path)
+ - get_notebook_checkpoint(self, checkpoint_id, path)
+
+ To create a generic CheckpointManager, add this mixin to a class that
+ implement the above four methods plus the remaining Checkpoints API
+ methods:
+
+ - delete_checkpoint(self, checkpoint_id, path)
+ - list_checkpoints(self, path)
+ - rename_checkpoint(self, checkpoint_id, old_path, new_path)
+ """
+
+ def create_checkpoint(self, contents_mgr, path):
+ model = contents_mgr.get(path, content=True)
+ type = model['type']
+ if type == 'notebook':
+ return self.create_notebook_checkpoint(
+ model['content'],
+ path,
+ )
+ elif type == 'file':
+ return self.create_file_checkpoint(
+ model['content'],
+ model['format'],
+ path,
+ )
+ else:
+ raise HTTPError(500, u'Unexpected type %s' % type)
+
+ def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
+ """Restore a checkpoint."""
+ type = contents_mgr.get(path, content=False)['type']
+ if type == 'notebook':
+ model = self.get_notebook_checkpoint(checkpoint_id, path)
+ elif type == 'file':
+ model = self.get_file_checkpoint(checkpoint_id, path)
+ else:
+ raise HTTPError(500, u'Unexpected type %s' % type)
+ contents_mgr.save(model, path)
+
+ # Required Methods
+ def create_file_checkpoint(self, content, format, path):
+ """Create a checkpoint of the current state of a file
+
+ Returns a checkpoint model for the new checkpoint.
+ """
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def create_notebook_checkpoint(self, nb, path):
+ """Create a checkpoint of the current state of a file
+
+ Returns a checkpoint model for the new checkpoint.
+ """
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def get_file_checkpoint(self, checkpoint_id, path):
+ """Get the content of a checkpoint for a non-notebook file.
+
+ Returns a dict of the form:
+ {
+ 'type': 'file',
+ 'content': <str>,
+ 'format': {'text','base64'},
+ }
+ """
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def get_notebook_checkpoint(self, checkpoint_id, path):
+ """Get the content of a checkpoint for a notebook.
+
+ Returns a dict of the form:
+ {
+ 'type': 'notebook',
+ 'content': <output of nbformat.read>,
+ }
+ """
+ raise NotImplementedError("must be implemented in a subclass")
diff --git a/notebook/services/contents/filecheckpoints.py b/notebook/services/contents/filecheckpoints.py
new file mode 100644
index 0000000..46e32ea
--- /dev/null
+++ b/notebook/services/contents/filecheckpoints.py
@@ -0,0 +1,201 @@
+"""
+File-based Checkpoints implementations.
+"""
+import os
+import shutil
+
+from tornado.web import HTTPError
+
+from .checkpoints import (
+ Checkpoints,
+ GenericCheckpointsMixin,
+)
+from .fileio import FileManagerMixin
+
+from . import tz
+from ipython_genutils.path import ensure_dir_exists
+from ipython_genutils.py3compat import getcwd
+from traitlets import Unicode
+
+
+class FileCheckpoints(FileManagerMixin, Checkpoints):
+ """
+ A Checkpoints that caches checkpoints for files in adjacent
+ directories.
+
+ Only works with FileContentsManager. Use GenericFileCheckpoints if
+ you want file-based checkpoints with another ContentsManager.
+ """
+
+ checkpoint_dir = Unicode(
+ '.ipynb_checkpoints',
+ config=True,
+ help="""The directory name in which to keep file checkpoints
+
+ This is a path relative to the file's own directory.
+
+ By default, it is .ipynb_checkpoints
+ """,
+ )
+
+ root_dir = Unicode(config=True)
+
+ def _root_dir_default(self):
+ try:
+ return self.parent.root_dir
+ except AttributeError:
+ return getcwd()
+
+ # ContentsManager-dependent checkpoint API
+ def create_checkpoint(self, contents_mgr, path):
+ """Create a checkpoint."""
+ checkpoint_id = u'checkpoint'
+ src_path = contents_mgr._get_os_path(path)
+ dest_path = self.checkpoint_path(checkpoint_id, path)
+ self._copy(src_path, dest_path)
+ return self.checkpoint_model(checkpoint_id, dest_path)
+
+ def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
+ """Restore a checkpoint."""
+ src_path = self.checkpoint_path(checkpoint_id, path)
+ dest_path = contents_mgr._get_os_path(path)
+ self._copy(src_path, dest_path)
+
+ # ContentsManager-independent checkpoint API
+ def rename_checkpoint(self, checkpoint_id, old_path, new_path):
+ """Rename a checkpoint from old_path to new_path."""
+ old_cp_path = self.checkpoint_path(checkpoint_id, old_path)
+ new_cp_path = self.checkpoint_path(checkpoint_id, new_path)
+ if os.path.isfile(old_cp_path):
+ self.log.debug(
+ "Renaming checkpoint %s -> %s",
+ old_cp_path,
+ new_cp_path,
+ )
+ with self.perm_to_403():
+ shutil.move(old_cp_path, new_cp_path)
+
+ def delete_checkpoint(self, checkpoint_id, path):
+ """delete a file's checkpoint"""
+ path = path.strip('/')
+ cp_path = self.checkpoint_path(checkpoint_id, path)
+ if not os.path.isfile(cp_path):
+ self.no_such_checkpoint(path, checkpoint_id)
+
+ self.log.debug("unlinking %s", cp_path)
+ with self.perm_to_403():
+ os.unlink(cp_path)
+
+ def list_checkpoints(self, path):
+ """list the checkpoints for a given file
+
+ This contents manager currently only supports one checkpoint per file.
+ """
+ path = path.strip('/')
+ checkpoint_id = "checkpoint"
+ os_path = self.checkpoint_path(checkpoint_id, path)
+ if not os.path.isfile(os_path):
+ return []
+ else:
+ return [self.checkpoint_model(checkpoint_id, os_path)]
+
+ # Checkpoint-related utilities
+ def checkpoint_path(self, checkpoint_id, path):
+ """find the path to a checkpoint"""
+ path = path.strip('/')
+ parent, name = ('/' + path).rsplit('/', 1)
+ parent = parent.strip('/')
+ basename, ext = os.path.splitext(name)
+ filename = u"{name}-{checkpoint_id}{ext}".format(
+ name=basename,
+ checkpoint_id=checkpoint_id,
+ ext=ext,
+ )
+ os_path = self._get_os_path(path=parent)
+ cp_dir = os.path.join(os_path, self.checkpoint_dir)
+ with self.perm_to_403():
+ ensure_dir_exists(cp_dir)
+ cp_path = os.path.join(cp_dir, filename)
+ return cp_path
+
+ def checkpoint_model(self, checkpoint_id, os_path):
+ """construct the info dict for a given checkpoint"""
+ stats = os.stat(os_path)
+ last_modified = tz.utcfromtimestamp(stats.st_mtime)
+ info = dict(
+ id=checkpoint_id,
+ last_modified=last_modified,
+ )
+ return info
+
+ # Error Handling
+ def no_such_checkpoint(self, path, checkpoint_id):
+ raise HTTPError(
+ 404,
+ u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
+ )
+
+
+class GenericFileCheckpoints(GenericCheckpointsMixin, FileCheckpoints):
+ """
+ Local filesystem Checkpoints that works with any conforming
+ ContentsManager.
+ """
+ def create_file_checkpoint(self, content, format, path):
+ """Create a checkpoint from the current content of a file."""
+ path = path.strip('/')
+ # only the one checkpoint ID:
+ checkpoint_id = u"checkpoint"
+ os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
+ self.log.debug("creating checkpoint for %s", path)
+ with self.perm_to_403():
+ self._save_file(os_checkpoint_path, content, format=format)
+
+ # return the checkpoint info
+ return self.checkpoint_model(checkpoint_id, os_checkpoint_path)
+
+ def create_notebook_checkpoint(self, nb, path):
+ """Create a checkpoint from the current content of a notebook."""
+ path = path.strip('/')
+ # only the one checkpoint ID:
+ checkpoint_id = u"checkpoint"
+ os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
+ self.log.debug("creating checkpoint for %s", path)
+ with self.perm_to_403():
+ self._save_notebook(os_checkpoint_path, nb)
+
+ # return the checkpoint info
+ return self.checkpoint_model(checkpoint_id, os_checkpoint_path)
+
+ def get_notebook_checkpoint(self, checkpoint_id, path):
+ """Get a checkpoint for a notebook."""
+ path = path.strip('/')
+ self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
+ os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
+
+ if not os.path.isfile(os_checkpoint_path):
+ self.no_such_checkpoint(path, checkpoint_id)
+
+ return {
+ 'type': 'notebook',
+ 'content': self._read_notebook(
+ os_checkpoint_path,
+ as_version=4,
+ ),
+ }
+
+ def get_file_checkpoint(self, checkpoint_id, path):
+ """Get a checkpoint for a file."""
+ path = path.strip('/')
+ self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
+ os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
+
+ if not os.path.isfile(os_checkpoint_path):
+ self.no_such_checkpoint(path, checkpoint_id)
+
+ content, format = self._read_file(os_checkpoint_path, format=None)
+ return {
+ 'type': 'file',
+ 'content': content,
+ 'format': format,
+ }
diff --git a/notebook/services/contents/fileio.py b/notebook/services/contents/fileio.py
new file mode 100644
index 0000000..9dd2835
--- /dev/null
+++ b/notebook/services/contents/fileio.py
@@ -0,0 +1,305 @@
+"""
+Utilities for file-based Contents/Checkpoints managers.
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import base64
+from contextlib import contextmanager
+import errno
+import io
+import os
+import shutil
+
+from tornado.web import HTTPError
+
+from notebook.utils import (
+ to_api_path,
+ to_os_path,
+)
+import nbformat
+
+from ipython_genutils.py3compat import str_to_unicode
+
+from traitlets.config import Configurable
+from traitlets import Bool
+
+def copy2_safe(src, dst, log=None):
+ """copy src to dst
+
+ like shutil.copy2, but log errors in copystat instead of raising
+ """
+ shutil.copyfile(src, dst)
+ try:
+ shutil.copystat(src, dst)
+ except OSError:
+ if log:
+ log.debug("copystat on %s failed", dst, exc_info=True)
+
+
+@contextmanager
+def atomic_writing(path, text=True, encoding='utf-8', log=None, **kwargs):
+ """Context manager to write to a file only if the entire write is successful.
+
+ This works by copying the previous file contents to a temporary file in the
+ same directory, and renaming that file back to the target if the context
+ exits with an error. If the context is successful, the new data is synced to
+ disk and the temporary file is removed.
+
+ Parameters
+ ----------
+ path : str
+ The target file to write to.
+
+ text : bool, optional
+ Whether to open the file in text mode (i.e. to write unicode). Default is
+ True.
+
+ encoding : str, optional
+ The encoding to use for files opened in text mode. Default is UTF-8.
+
+ **kwargs
+ Passed to :func:`io.open`.
+ """
+ # realpath doesn't work on Windows: http://bugs.python.org/issue9949
+ # Luckily, we only need to resolve the file itself being a symlink, not
+ # any of its directories, so this will suffice:
+ if os.path.islink(path):
+ path = os.path.join(os.path.dirname(path), os.readlink(path))
+
+ dirname, basename = os.path.split(path)
+ # The .~ prefix will make Dropbox ignore the temporary file.
+ tmp_path = os.path.join(dirname, '.~'+basename)
+ if os.path.isfile(path):
+ copy2_safe(path, tmp_path, log=log)
+
+ if text:
+ # Make sure that text files have Unix linefeeds by default
+ kwargs.setdefault('newline', '\n')
+ fileobj = io.open(path, 'w', encoding=encoding, **kwargs)
+ else:
+ fileobj = io.open(path, 'wb', **kwargs)
+
+ try:
+ yield fileobj
+ except:
+ # Failed! Move the backup file back to the real path to avoid corruption
+ fileobj.close()
+ if os.name == 'nt' and os.path.exists(path):
+ # Rename over existing file doesn't work on Windows
+ os.remove(path)
+ os.rename(tmp_path, path)
+ raise
+
+ # Flush to disk
+ fileobj.flush()
+ os.fsync(fileobj.fileno())
+ fileobj.close()
+
+ # Written successfully, now remove the backup copy
+ if os.path.isfile(tmp_path):
+ os.remove(tmp_path)
+
+
+
+@contextmanager
+def _simple_writing(path, text=True, encoding='utf-8', log=None, **kwargs):
+ """Context manager to write file without doing atomic writing
+ ( for weird filesystem eg: nfs).
+
+ Parameters
+ ----------
+ path : str
+ The target file to write to.
+
+ text : bool, optional
+ Whether to open the file in text mode (i.e. to write unicode). Default is
+ True.
+
+ encoding : str, optional
+ The encoding to use for files opened in text mode. Default is UTF-8.
+
+ **kwargs
+ Passed to :func:`io.open`.
+ """
+ # realpath doesn't work on Windows: http://bugs.python.org/issue9949
+ # Luckily, we only need to resolve the file itself being a symlink, not
+ # any of its directories, so this will suffice:
+ if os.path.islink(path):
+ path = os.path.join(os.path.dirname(path), os.readlink(path))
+
+ if text:
+ # Make sure that text files have Unix linefeeds by default
+ kwargs.setdefault('newline', '\n')
+ fileobj = io.open(path, 'w', encoding=encoding, **kwargs)
+ else:
+ fileobj = io.open(path, 'wb', **kwargs)
+
+ try:
+ yield fileobj
+ except:
+ fileobj.close()
+ raise
+
+ fileobj.close()
+
+
+
+
+class FileManagerMixin(Configurable):
+ """
+ Mixin for ContentsAPI classes that interact with the filesystem.
+
+ Provides facilities for reading, writing, and copying both notebooks and
+ generic files.
+
+ Shared by FileContentsManager and FileCheckpoints.
+
+ Note
+ ----
+ Classes using this mixin must provide the following attributes:
+
+ root_dir : unicode
+ A directory against against which API-style paths are to be resolved.
+
+ log : logging.Logger
+ """
+
+ use_atomic_writing = Bool(True, config=True, help=
+ """By default notebooks are saved on disk on a temporary file and then if succefully written, it replaces the old ones.
+ This procedure, namely 'atomic_writing', causes some bugs on file system whitout operation order enforcement (like some networked fs).
+ If set to False, the new notebook is written directly on the old one which could fail (eg: full filesystem or quota )""")
+
+ @contextmanager
+ def open(self, os_path, *args, **kwargs):
+ """wrapper around io.open that turns permission errors into 403"""
+ with self.perm_to_403(os_path):
+ with io.open(os_path, *args, **kwargs) as f:
+ yield f
+
+ @contextmanager
+ def atomic_writing(self, os_path, *args, **kwargs):
+ """wrapper around atomic_writing that turns permission errors to 403.
+ Depending on flag 'use_atomic_writing', the wrapper perform an actual atomic writing or
+ simply writes the file (whatever an old exists or not)"""
+ with self.perm_to_403(os_path):
+ if self.use_atomic_writing:
+ with atomic_writing(os_path, *args, log=self.log, **kwargs) as f:
+ yield f
+ else:
+ with _simple_writing(os_path, *args, log=self.log, **kwargs) as f:
+ yield f
+
+ @contextmanager
+ def perm_to_403(self, os_path=''):
+ """context manager for turning permission errors into 403."""
+ try:
+ yield
+ except (OSError, IOError) as e:
+ if e.errno in {errno.EPERM, errno.EACCES}:
+ # make 403 error message without root prefix
+ # this may not work perfectly on unicode paths on Python 2,
+ # but nobody should be doing that anyway.
+ if not os_path:
+ os_path = str_to_unicode(e.filename or 'unknown file')
+ path = to_api_path(os_path, root=self.root_dir)
+ raise HTTPError(403, u'Permission denied: %s' % path)
+ else:
+ raise
+
+ def _copy(self, src, dest):
+ """copy src to dest
+
+ like shutil.copy2, but log errors in copystat
+ """
+ copy2_safe(src, dest, log=self.log)
+
+ def _get_os_path(self, path):
+ """Given an API path, return its file system path.
+
+ Parameters
+ ----------
+ path : string
+ The relative API path to the named file.
+
+ Returns
+ -------
+ path : string
+ Native, absolute OS path to for a file.
+
+ Raises
+ ------
+ 404: if path is outside root
+ """
+ root = os.path.abspath(self.root_dir)
+ os_path = to_os_path(path, root)
+ if not (os.path.abspath(os_path) + os.path.sep).startswith(root):
+ raise HTTPError(404, "%s is outside root contents directory" % path)
+ return os_path
+
+ def _read_notebook(self, os_path, as_version=4):
+ """Read a notebook from an os path."""
+ with self.open(os_path, 'r', encoding='utf-8') as f:
+ try:
+ return nbformat.read(f, as_version=as_version)
+ except Exception as e:
+ raise HTTPError(
+ 400,
+ u"Unreadable Notebook: %s %r" % (os_path, e),
+ )
+
+ def _save_notebook(self, os_path, nb):
+ """Save a notebook to an os_path."""
+ with self.atomic_writing(os_path, encoding='utf-8') as f:
+ nbformat.write(nb, f, version=nbformat.NO_CONVERT)
+
+ def _read_file(self, os_path, format):
+ """Read a non-notebook file.
+
+ os_path: The path to be read.
+ format:
+ If 'text', the contents will be decoded as UTF-8.
+ If 'base64', the raw bytes contents will be encoded as base64.
+ If not specified, try to decode as UTF-8, and fall back to base64
+ """
+ if not os.path.isfile(os_path):
+ raise HTTPError(400, "Cannot read non-file %s" % os_path)
+
+ with self.open(os_path, 'rb') as f:
+ bcontent = f.read()
+
+ if format is None or format == 'text':
+ # Try to interpret as unicode if format is unknown or if unicode
+ # was explicitly requested.
+ try:
+ return bcontent.decode('utf8'), 'text'
+ except UnicodeError:
+ if format == 'text':
+ raise HTTPError(
+ 400,
+ "%s is not UTF-8 encoded" % os_path,
+ reason='bad format',
+ )
+ return base64.encodestring(bcontent).decode('ascii'), 'base64'
+
+ def _save_file(self, os_path, content, format):
+ """Save content of a generic file."""
+ if format not in {'text', 'base64'}:
+ raise HTTPError(
+ 400,
+ "Must specify format of file contents as 'text' or 'base64'",
+ )
+ try:
+ if format == 'text':
+ bcontent = content.encode('utf8')
+ else:
+ b64_bytes = content.encode('ascii')
+ bcontent = base64.decodestring(b64_bytes)
+ except Exception as e:
+ raise HTTPError(
+ 400, u'Encoding error saving %s: %s' % (os_path, e)
+ )
+
+ with self.atomic_writing(os_path, text=False) as f:
+ f.write(bcontent)
diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py
new file mode 100644
index 0000000..239eadb
--- /dev/null
+++ b/notebook/services/contents/filemanager.py
@@ -0,0 +1,484 @@
+"""A contents manager that uses the local file system for storage."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+
+import io
+import os
+import shutil
+import warnings
+import mimetypes
+import nbformat
+
+from tornado import web
+
+from .filecheckpoints import FileCheckpoints
+from .fileio import FileManagerMixin
+from .manager import ContentsManager
+
+
+from ipython_genutils.importstring import import_item
+from traitlets import Any, Unicode, Bool, TraitError
+from ipython_genutils.py3compat import getcwd, string_types
+from . import tz
+from notebook.utils import (
+ is_hidden,
+ to_api_path,
+)
+
+_script_exporter = None
+
+
+def _post_save_script(model, os_path, contents_manager, **kwargs):
+ """convert notebooks to Python script after save with nbconvert
+
+ replaces `jupyter notebook --script`
+ """
+ from nbconvert.exporters.script import ScriptExporter
+ warnings.warn("`_post_save_script` is deprecated and will be removed in Notebook 5.0", DeprecationWarning)
+
+ if model['type'] != 'notebook':
+ return
+
+ global _script_exporter
+ if _script_exporter is None:
+ _script_exporter = ScriptExporter(parent=contents_manager)
+ log = contents_manager.log
+
+ base, ext = os.path.splitext(os_path)
+ script, resources = _script_exporter.from_filename(os_path)
+ script_fname = base + resources.get('output_extension', '.txt')
+ log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
+ with io.open(script_fname, 'w', encoding='utf-8') as f:
+ f.write(script)
+
+
+class FileContentsManager(FileManagerMixin, ContentsManager):
+
+ root_dir = Unicode(config=True)
+
+ def _root_dir_default(self):
+ try:
+ return self.parent.notebook_dir
+ except AttributeError:
+ return getcwd()
+
+ save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook. Will be removed in Notebook 5.0')
+ def _save_script_changed(self):
+ self.log.warn("""
+ `--script` is deprecated and will be removed in notebook 5.0.
+
+ You can trigger nbconvert via pre- or post-save hooks:
+
+ ContentsManager.pre_save_hook
+ FileContentsManager.post_save_hook
+
+ A post-save hook has been registered that calls:
+
+ jupyter nbconvert --to script [notebook]
+
+ which behaves similarly to `--script`.
+ """)
+
+ self.post_save_hook = _post_save_script
+
+ post_save_hook = Any(None, config=True,
+ help="""Python callable or importstring thereof
+
+ to be called on the path of a file just saved.
+
+ This can be used to process the file on disk,
+ such as converting the notebook to a script or HTML via nbconvert.
+
+ It will be called as (all arguments passed by keyword)::
+
+ hook(os_path=os_path, model=model, contents_manager=instance)
+
+ - path: the filesystem path to the file just written
+ - model: the model representing the file
+ - contents_manager: this ContentsManager instance
+ """
+ )
+ def _post_save_hook_changed(self, name, old, new):
+ if new and isinstance(new, string_types):
+ self.post_save_hook = import_item(self.post_save_hook)
+ elif new:
+ if not callable(new):
+ raise TraitError("post_save_hook must be callable")
+
+ def run_post_save_hook(self, model, os_path):
+ """Run the post-save hook if defined, and log errors"""
+ if self.post_save_hook:
+ try:
+ self.log.debug("Running post-save hook on %s", os_path)
+ self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
+ except Exception:
+ self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
+
+ def _root_dir_changed(self, name, old, new):
+ """Do a bit of validation of the root_dir."""
+ if not os.path.isabs(new):
+ # If we receive a non-absolute path, make it absolute.
+ self.root_dir = os.path.abspath(new)
+ return
+ if not os.path.isdir(new):
+ raise TraitError("%r is not a directory" % new)
+
+ def _checkpoints_class_default(self):
+ return FileCheckpoints
+
+ def is_hidden(self, path):
+ """Does the API style path correspond to a hidden directory or file?
+
+ Parameters
+ ----------
+ path : string
+ The path to check. This is an API path (`/` separated,
+ relative to root_dir).
+
+ Returns
+ -------
+ hidden : bool
+ Whether the path exists and is hidden.
+ """
+ path = path.strip('/')
+ os_path = self._get_os_path(path=path)
+ return is_hidden(os_path, self.root_dir)
+
+ def file_exists(self, path):
+ """Returns True if the file exists, else returns False.
+
+ API-style wrapper for os.path.isfile
+
+ Parameters
+ ----------
+ path : string
+ The relative path to the file (with '/' as separator)
+
+ Returns
+ -------
+ exists : bool
+ Whether the file exists.
+ """
+ path = path.strip('/')
+ os_path = self._get_os_path(path)
+ return os.path.isfile(os_path)
+
+ def dir_exists(self, path):
+ """Does the API-style path refer to an extant directory?
+
+ API-style wrapper for os.path.isdir
+
+ Parameters
+ ----------
+ path : string
+ The path to check. This is an API path (`/` separated,
+ relative to root_dir).
+
+ Returns
+ -------
+ exists : bool
+ Whether the path is indeed a directory.
+ """
+ path = path.strip('/')
+ os_path = self._get_os_path(path=path)
+ return os.path.isdir(os_path)
+
+ def exists(self, path):
+ """Returns True if the path exists, else returns False.
+
+ API-style wrapper for os.path.exists
+
+ Parameters
+ ----------
+ path : string
+ The API path to the file (with '/' as separator)
+
+ Returns
+ -------
+ exists : bool
+ Whether the target exists.
+ """
+ path = path.strip('/')
+ os_path = self._get_os_path(path=path)
+ return os.path.exists(os_path)
+
+ def _base_model(self, path):
+ """Build the common base of a contents model"""
+ os_path = self._get_os_path(path)
+ info = os.stat(os_path)
+ last_modified = tz.utcfromtimestamp(info.st_mtime)
+ created = tz.utcfromtimestamp(info.st_ctime)
+ # Create the base model.
+ model = {}
+ model['name'] = path.rsplit('/', 1)[-1]
+ model['path'] = path
+ model['last_modified'] = last_modified
+ model['created'] = created
+ model['content'] = None
+ model['format'] = None
+ model['mimetype'] = None
+ try:
+ model['writable'] = os.access(os_path, os.W_OK)
+ except OSError:
+ self.log.error("Failed to check write permissions on %s", os_path)
+ model['writable'] = False
+ return model
+
+ def _dir_model(self, path, content=True):
+ """Build a model for a directory
+
+ if content is requested, will include a listing of the directory
+ """
+ os_path = self._get_os_path(path)
+
+ four_o_four = u'directory does not exist: %r' % path
+
+ if not os.path.isdir(os_path):
+ raise web.HTTPError(404, four_o_four)
+ elif is_hidden(os_path, self.root_dir):
+ self.log.info("Refusing to serve hidden directory %r, via 404 Error",
+ os_path
+ )
+ raise web.HTTPError(404, four_o_four)
+
+ model = self._base_model(path)
+ model['type'] = 'directory'
+ if content:
+ model['content'] = contents = []
+ os_dir = self._get_os_path(path)
+ for name in os.listdir(os_dir):
+ try:
+ os_path = os.path.join(os_dir, name)
+ except UnicodeDecodeError as e:
+ self.log.warn(
+ "failed to decode filename '%s': %s", name, e)
+ continue
+ # skip over broken symlinks in listing
+ if not os.path.exists(os_path):
+ self.log.warn("%s doesn't exist", os_path)
+ continue
+ elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
+ self.log.debug("%s not a regular file", os_path)
+ continue
+ if self.should_list(name) and not is_hidden(os_path, self.root_dir):
+ contents.append(self.get(
+ path='%s/%s' % (path, name),
+ content=False)
+ )
+
+ model['format'] = 'json'
+
+ return model
+
+ def _file_model(self, path, content=True, format=None):
+ """Build a model for a file
+
+ if content is requested, include the file contents.
+
+ format:
+ If 'text', the contents will be decoded as UTF-8.
+ If 'base64', the raw bytes contents will be encoded as base64.
+ If not specified, try to decode as UTF-8, and fall back to base64
+ """
+ model = self._base_model(path)
+ model['type'] = 'file'
+
+ os_path = self._get_os_path(path)
+ model['mimetype'] = mimetypes.guess_type(os_path)[0]
+
+ if content:
+ content, format = self._read_file(os_path, format)
+ if model['mimetype'] is None:
+ default_mime = {
+ 'text': 'text/plain',
+ 'base64': 'application/octet-stream'
+ }[format]
+ model['mimetype'] = default_mime
+
+ model.update(
+ content=content,
+ format=format,
+ )
+
+ return model
+
+ def _notebook_model(self, path, content=True):
+ """Build a notebook model
+
+ if content is requested, the notebook content will be populated
+ as a JSON structure (not double-serialized)
+ """
+ model = self._base_model(path)
+ model['type'] = 'notebook'
+ if content:
+ os_path = self._get_os_path(path)
+ nb = self._read_notebook(os_path, as_version=4)
+ self.mark_trusted_cells(nb, path)
+ model['content'] = nb
+ model['format'] = 'json'
+ self.validate_notebook_model(model)
+ return model
+
+ def get(self, path, content=True, type=None, format=None):
+ """ Takes a path for an entity and returns its model
+
+ Parameters
+ ----------
+ path : str
+ the API path that describes the relative path for the target
+ content : bool
+ Whether to include the contents in the reply
+ type : str, optional
+ The requested type - 'file', 'notebook', or 'directory'.
+ Will raise HTTPError 400 if the content doesn't match.
+ format : str, optional
+ The requested format for file contents. 'text' or 'base64'.
+ Ignored if this returns a notebook or directory model.
+
+ Returns
+ -------
+ model : dict
+ the contents model. If content=True, returns the contents
+ of the file or directory as well.
+ """
+ path = path.strip('/')
+
+ if not self.exists(path):
+ raise web.HTTPError(404, u'No such file or directory: %s' % path)
+
+ os_path = self._get_os_path(path)
+ if os.path.isdir(os_path):
+ if type not in (None, 'directory'):
+ raise web.HTTPError(400,
+ u'%s is a directory, not a %s' % (path, type), reason='bad type')
+ model = self._dir_model(path, content=content)
+ elif type == 'notebook' or (type is None and path.endswith('.ipynb')):
+ model = self._notebook_model(path, content=content)
+ else:
+ if type == 'directory':
+ raise web.HTTPError(400,
+ u'%s is not a directory' % path, reason='bad type')
+ model = self._file_model(path, content=content, format=format)
+ return model
+
+ def _save_directory(self, os_path, model, path=''):
+ """create a directory"""
+ if is_hidden(os_path, self.root_dir):
+ raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
+ if not os.path.exists(os_path):
+ with self.perm_to_403():
+ os.mkdir(os_path)
+ elif not os.path.isdir(os_path):
+ raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
+ else:
+ self.log.debug("Directory %r already exists", os_path)
+
+ def save(self, model, path=''):
+ """Save the file model and return the model with no content."""
+ path = path.strip('/')
+
+ if 'type' not in model:
+ raise web.HTTPError(400, u'No file type provided')
+ if 'content' not in model and model['type'] != 'directory':
+ raise web.HTTPError(400, u'No file content provided')
+
+ os_path = self._get_os_path(path)
+ self.log.debug("Saving %s", os_path)
+
+ self.run_pre_save_hook(model=model, path=path)
+
+ try:
+ if model['type'] == 'notebook':
+ nb = nbformat.from_dict(model['content'])
+ self.check_and_sign(nb, path)
+ self._save_notebook(os_path, nb)
+ # One checkpoint should always exist for notebooks.
+ if not self.checkpoints.list_checkpoints(path):
+ self.create_checkpoint(path)
+ elif model['type'] == 'file':
+ # Missing format will be handled internally by _save_file.
+ self._save_file(os_path, model['content'], model.get('format'))
+ elif model['type'] == 'directory':
+ self._save_directory(os_path, model, path)
+ else:
+ raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
+ except web.HTTPError:
+ raise
+ except Exception as e:
+ self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
+ raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
+
+ validation_message = None
+ if model['type'] == 'notebook':
+ self.validate_notebook_model(model)
+ validation_message = model.get('message', None)
+
+ model = self.get(path, content=False)
+ if validation_message:
+ model['message'] = validation_message
+
+ self.run_post_save_hook(model=model, os_path=os_path)
+
+ return model
+
+ def delete_file(self, path):
+ """Delete file at path."""
+ path = path.strip('/')
+ os_path = self._get_os_path(path)
+ rm = os.unlink
+ if os.path.isdir(os_path):
+ listing = os.listdir(os_path)
+ # Don't delete non-empty directories.
+ # A directory containing only leftover checkpoints is
+ # considered empty.
+ cp_dir = getattr(self.checkpoints, 'checkpoint_dir', None)
+ for entry in listing:
+ if entry != cp_dir:
+ raise web.HTTPError(400, u'Directory %s not empty' % os_path)
+ elif not os.path.isfile(os_path):
+ raise web.HTTPError(404, u'File does not exist: %s' % os_path)
+
+ if os.path.isdir(os_path):
+ self.log.debug("Removing directory %s", os_path)
+ with self.perm_to_403():
+ shutil.rmtree(os_path)
+ else:
+ self.log.debug("Unlinking file %s", os_path)
+ with self.perm_to_403():
+ rm(os_path)
+
+ def rename_file(self, old_path, new_path):
+ """Rename a file."""
+ old_path = old_path.strip('/')
+ new_path = new_path.strip('/')
+ if new_path == old_path:
+ return
+
+ new_os_path = self._get_os_path(new_path)
+ old_os_path = self._get_os_path(old_path)
+
+ # Should we proceed with the move?
+ if os.path.exists(new_os_path):
+ raise web.HTTPError(409, u'File already exists: %s' % new_path)
+
+ # Move the file
+ try:
+ with self.perm_to_403():
+ shutil.move(old_os_path, new_os_path)
+ except web.HTTPError:
+ raise
+ except Exception as e:
+ raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
+
+ def info_string(self):
+ return "Serving notebooks from local directory: %s" % self.root_dir
+
+ def get_kernel_path(self, path, model=None):
+ """Return the initial API path of a kernel associated with a given notebook"""
+ if '/' in path:
+ parent_dir = path.rsplit('/', 1)[0]
+ else:
+ parent_dir = ''
+ return parent_dir
diff --git a/notebook/services/contents/handlers.py b/notebook/services/contents/handlers.py
new file mode 100644
index 0000000..185649a
--- /dev/null
+++ b/notebook/services/contents/handlers.py
@@ -0,0 +1,336 @@
+"""Tornado handlers for the contents web service.
+
+Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-27%3A-Contents-Service
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import json
+
+from tornado import gen, web
+
+from notebook.utils import url_path_join, url_escape
+from jupyter_client.jsonutil import date_default
+
+from notebook.base.handlers import (
+ IPythonHandler, APIHandler, json_errors, path_regex,
+)
+
+
+def sort_key(model):
+ """key function for case-insensitive sort by name and type"""
+ iname = model['name'].lower()
+ type_key = {
+ 'directory' : '0',
+ 'notebook' : '1',
+ 'file' : '2',
+ }.get(model['type'], '9')
+ return u'%s%s' % (type_key, iname)
+
+
+def validate_model(model, expect_content):
+ """
+ Validate a model returned by a ContentsManager method.
+
+ If expect_content is True, then we expect non-null entries for 'content'
+ and 'format'.
+ """
+ required_keys = {
+ "name",
+ "path",
+ "type",
+ "writable",
+ "created",
+ "last_modified",
+ "mimetype",
+ "content",
+ "format",
+ }
+ missing = required_keys - set(model.keys())
+ if missing:
+ raise web.HTTPError(
+ 500,
+ u"Missing Model Keys: {missing}".format(missing=missing),
+ )
+
+ maybe_none_keys = ['content', 'format']
+ if expect_content:
+ errors = [key for key in maybe_none_keys if model[key] is None]
+ if errors:
+ raise web.HTTPError(
+ 500,
+ u"Keys unexpectedly None: {keys}".format(keys=errors),
+ )
+ else:
+ errors = {
+ key: model[key]
+ for key in maybe_none_keys
+ if model[key] is not None
+ }
+ if errors:
+ raise web.HTTPError(
+ 500,
+ u"Keys unexpectedly not None: {keys}".format(keys=errors),
+ )
+
+
+class ContentsHandler(APIHandler):
+
+ def location_url(self, path):
+ """Return the full URL location of a file.
+
+ Parameters
+ ----------
+ path : unicode
+ The API path of the file, such as "foo/bar.txt".
+ """
+ return url_path_join(
+ self.base_url, 'api', 'contents', url_escape(path)
+ )
+
+ def _finish_model(self, model, location=True):
+ """Finish a JSON request with a model, setting relevant headers, etc."""
+ if location:
+ location = self.location_url(model['path'])
+ self.set_header('Location', location)
+ self.set_header('Last-Modified', model['last_modified'])
+ self.set_header('Content-Type', 'application/json')
+ self.finish(json.dumps(model, default=date_default))
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def get(self, path=''):
+ """Return a model for a file or directory.
+
+ A directory model contains a list of models (without content)
+ of the files and directories it contains.
+ """
+ path = path or ''
+ type = self.get_query_argument('type', default=None)
+ if type not in {None, 'directory', 'file', 'notebook'}:
+ raise web.HTTPError(400, u'Type %r is invalid' % type)
+
+ format = self.get_query_argument('format', default=None)
+ if format not in {None, 'text', 'base64'}:
+ raise web.HTTPError(400, u'Format %r is invalid' % format)
+ content = self.get_query_argument('content', default='1')
+ if content not in {'0', '1'}:
+ raise web.HTTPError(400, u'Content %r is invalid' % content)
+ content = int(content)
+
+ model = yield gen.maybe_future(self.contents_manager.get(
+ path=path, type=type, format=format, content=content,
+ ))
+ if model['type'] == 'directory' and content:
+ # group listing by type, then by name (case-insensitive)
+ # FIXME: sorting should be done in the frontends
+ model['content'].sort(key=sort_key)
+ validate_model(model, expect_content=content)
+ self._finish_model(model, location=False)
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def patch(self, path=''):
+ """PATCH renames a file or directory without re-uploading content."""
+ cm = self.contents_manager
+ model = self.get_json_body()
+ if model is None:
+ raise web.HTTPError(400, u'JSON body missing')
+ model = yield gen.maybe_future(cm.update(model, path))
+ validate_model(model, expect_content=False)
+ self._finish_model(model)
+
+ @gen.coroutine
+ def _copy(self, copy_from, copy_to=None):
+ """Copy a file, optionally specifying a target directory."""
+ self.log.info(u"Copying {copy_from} to {copy_to}".format(
+ copy_from=copy_from,
+ copy_to=copy_to or '',
+ ))
+ model = yield gen.maybe_future(self.contents_manager.copy(copy_from, copy_to))
+ self.set_status(201)
+ validate_model(model, expect_content=False)
+ self._finish_model(model)
+
+ @gen.coroutine
+ def _upload(self, model, path):
+ """Handle upload of a new file to path"""
+ self.log.info(u"Uploading file to %s", path)
+ model = yield gen.maybe_future(self.contents_manager.new(model, path))
+ self.set_status(201)
+ validate_model(model, expect_content=False)
+ self._finish_model(model)
+
+ @gen.coroutine
+ def _new_untitled(self, path, type='', ext=''):
+ """Create a new, empty untitled entity"""
+ self.log.info(u"Creating new %s in %s", type or 'file', path)
+ model = yield gen.maybe_future(self.contents_manager.new_untitled(path=path, type=type, ext=ext))
+ self.set_status(201)
+ validate_model(model, expect_content=False)
+ self._finish_model(model)
+
+ @gen.coroutine
+ def _save(self, model, path):
+ """Save an existing file."""
+ self.log.info(u"Saving file at %s", path)
+ model = yield gen.maybe_future(self.contents_manager.save(model, path))
+ validate_model(model, expect_content=False)
+ self._finish_model(model)
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def post(self, path=''):
+ """Create a new file in the specified path.
+
+ POST creates new files. The server always decides on the name.
+
+ POST /api/contents/path
+ New untitled, empty file or directory.
+ POST /api/contents/path
+ with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
+ New copy of OtherNotebook in path
+ """
+
+ cm = self.contents_manager
+
+ if cm.file_exists(path):
+ raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
+
+ if not cm.dir_exists(path):
+ raise web.HTTPError(404, "No such directory: %s" % path)
+
+ model = self.get_json_body()
+
+ if model is not None:
+ copy_from = model.get('copy_from')
+ ext = model.get('ext', '')
+ type = model.get('type', '')
+ if copy_from:
+ yield self._copy(copy_from, path)
+ else:
+ yield self._new_untitled(path, type=type, ext=ext)
+ else:
+ yield self._new_untitled(path)
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def put(self, path=''):
+ """Saves the file in the location specified by name and path.
+
+ PUT is very similar to POST, but the requester specifies the name,
+ whereas with POST, the server picks the name.
+
+ PUT /api/contents/path/Name.ipynb
+ Save notebook at ``path/Name.ipynb``. Notebook structure is specified
+ in `content` key of JSON request body. If content is not specified,
+ create a new empty notebook.
+ """
+ model = self.get_json_body()
+ if model:
+ if model.get('copy_from'):
+ raise web.HTTPError(400, "Cannot copy with PUT, only POST")
+ exists = yield gen.maybe_future(self.contents_manager.file_exists(path))
+ if exists:
+ yield gen.maybe_future(self._save(model, path))
+ else:
+ yield gen.maybe_future(self._upload(model, path))
+ else:
+ yield gen.maybe_future(self._new_untitled(path))
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def delete(self, path=''):
+ """delete a file in the given path"""
+ cm = self.contents_manager
+ self.log.warn('delete %s', path)
+ yield gen.maybe_future(cm.delete(path))
+ self.set_status(204)
+ self.finish()
+
+
+class CheckpointsHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def get(self, path=''):
+ """get lists checkpoints for a file"""
+ cm = self.contents_manager
+ checkpoints = yield gen.maybe_future(cm.list_checkpoints(path))
+ data = json.dumps(checkpoints, default=date_default)
+ self.finish(data)
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def post(self, path=''):
+ """post creates a new checkpoint"""
+ cm = self.contents_manager
+ checkpoint = yield gen.maybe_future(cm.create_checkpoint(path))
+ data = json.dumps(checkpoint, default=date_default)
+ location = url_path_join(self.base_url, 'api/contents',
+ url_escape(path), 'checkpoints', url_escape(checkpoint['id']))
+ self.set_header('Location', location)
+ self.set_status(201)
+ self.finish(data)
+
+
+class ModifyCheckpointsHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def post(self, path, checkpoint_id):
+ """post restores a file from a checkpoint"""
+ cm = self.contents_manager
+ yield gen.maybe_future(cm.restore_checkpoint(checkpoint_id, path))
+ self.set_status(204)
+ self.finish()
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def delete(self, path, checkpoint_id):
+ """delete clears a checkpoint for a given file"""
+ cm = self.contents_manager
+ yield gen.maybe_future(cm.delete_checkpoint(checkpoint_id, path))
+ self.set_status(204)
+ self.finish()
+
+
+class NotebooksRedirectHandler(IPythonHandler):
+ """Redirect /api/notebooks to /api/contents"""
+ SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
+
+ def get(self, path):
+ self.log.warn("/api/notebooks is deprecated, use /api/contents")
+ self.redirect(url_path_join(
+ self.base_url,
+ 'api/contents',
+ path
+ ))
+
+ put = patch = post = delete = get
+
+
+#-----------------------------------------------------------------------------
+# URL to handler mappings
+#-----------------------------------------------------------------------------
+
+
+_checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
+
+default_handlers = [
+ (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
+ (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
+ ModifyCheckpointsHandler),
+ (r"/api/contents%s" % path_regex, ContentsHandler),
+ (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
+]
diff --git a/notebook/services/contents/manager.py b/notebook/services/contents/manager.py
new file mode 100644
index 0000000..76db41c
--- /dev/null
+++ b/notebook/services/contents/manager.py
@@ -0,0 +1,471 @@
+"""A base class for contents managers."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from fnmatch import fnmatch
+import itertools
+import json
+import os
+import re
+
+from tornado.web import HTTPError
+
+from .checkpoints import Checkpoints
+from traitlets.config.configurable import LoggingConfigurable
+from nbformat import sign, validate, ValidationError
+from nbformat.v4 import new_notebook
+from ipython_genutils.importstring import import_item
+from traitlets import (
+ Any,
+ Dict,
+ Instance,
+ List,
+ TraitError,
+ Type,
+ Unicode,
+)
+from ipython_genutils.py3compat import string_types
+
+copy_pat = re.compile(r'\-Copy\d*\.')
+
+
+class ContentsManager(LoggingConfigurable):
+ """Base class for serving files and directories.
+
+ This serves any text or binary file,
+ as well as directories,
+ with special handling for JSON notebook documents.
+
+ Most APIs take a path argument,
+ which is always an API-style unicode path,
+ and always refers to a directory.
+
+ - unicode, not url-escaped
+ - '/'-separated
+ - leading and trailing '/' will be stripped
+ - if unspecified, path defaults to '',
+ indicating the root path.
+
+ """
+
+ notary = Instance(sign.NotebookNotary)
+ def _notary_default(self):
+ return sign.NotebookNotary(parent=self)
+
+ hide_globs = List(Unicode(), [
+ u'__pycache__', '*.pyc', '*.pyo',
+ '.DS_Store', '*.so', '*.dylib', '*~',
+ ], config=True, help="""
+ Glob patterns to hide in file and directory listings.
+ """)
+
+ untitled_notebook = Unicode("Untitled", config=True,
+ help="The base name used when creating untitled notebooks."
+ )
+
+ untitled_file = Unicode("untitled", config=True,
+ help="The base name used when creating untitled files."
+ )
+
+ untitled_directory = Unicode("Untitled Folder", config=True,
+ help="The base name used when creating untitled directories."
+ )
+
+ pre_save_hook = Any(None, config=True,
+ help="""Python callable or importstring thereof
+
+ To be called on a contents model prior to save.
+
+ This can be used to process the structure,
+ such as removing notebook outputs or other side effects that
+ should not be saved.
+
+ It will be called as (all arguments passed by keyword)::
+
+ hook(path=path, model=model, contents_manager=self)
+
+ - model: the model to be saved. Includes file contents.
+ Modifying this dict will affect the file that is stored.
+ - path: the API path of the save destination
+ - contents_manager: this ContentsManager instance
+ """
+ )
+ def _pre_save_hook_changed(self, name, old, new):
+ if new and isinstance(new, string_types):
+ self.pre_save_hook = import_item(self.pre_save_hook)
+ elif new:
+ if not callable(new):
+ raise TraitError("pre_save_hook must be callable")
+
+ def run_pre_save_hook(self, model, path, **kwargs):
+ """Run the pre-save hook if defined, and log errors"""
+ if self.pre_save_hook:
+ try:
+ self.log.debug("Running pre-save hook on %s", path)
+ self.pre_save_hook(model=model, path=path, contents_manager=self, **kwargs)
+ except Exception:
+ self.log.error("Pre-save hook failed on %s", path, exc_info=True)
+
+ checkpoints_class = Type(Checkpoints, config=True)
+ checkpoints = Instance(Checkpoints, config=True)
+ checkpoints_kwargs = Dict(config=True)
+
+ def _checkpoints_default(self):
+ return self.checkpoints_class(**self.checkpoints_kwargs)
+
+ def _checkpoints_kwargs_default(self):
+ return dict(
+ parent=self,
+ log=self.log,
+ )
+
+ # ContentsManager API part 1: methods that must be
+ # implemented in subclasses.
+
+ def dir_exists(self, path):
+ """Does a directory exist at the given path?
+
+ Like os.path.isdir
+
+ Override this method in subclasses.
+
+ Parameters
+ ----------
+ path : string
+ The path to check
+
+ Returns
+ -------
+ exists : bool
+ Whether the path does indeed exist.
+ """
+ raise NotImplementedError
+
+ def is_hidden(self, path):
+ """Is path a hidden directory or file?
+
+ Parameters
+ ----------
+ path : string
+ The path to check. This is an API path (`/` separated,
+ relative to root dir).
+
+ Returns
+ -------
+ hidden : bool
+ Whether the path is hidden.
+
+ """
+ raise NotImplementedError
+
+ def file_exists(self, path=''):
+ """Does a file exist at the given path?
+
+ Like os.path.isfile
+
+ Override this method in subclasses.
+
+ Parameters
+ ----------
+ path : string
+ The API path of a file to check for.
+
+ Returns
+ -------
+ exists : bool
+ Whether the file exists.
+ """
+ raise NotImplementedError('must be implemented in a subclass')
+
+ def exists(self, path):
+ """Does a file or directory exist at the given path?
+
+ Like os.path.exists
+
+ Parameters
+ ----------
+ path : string
+ The API path of a file or directory to check for.
+
+ Returns
+ -------
+ exists : bool
+ Whether the target exists.
+ """
+ return self.file_exists(path) or self.dir_exists(path)
+
+ def get(self, path, content=True, type=None, format=None):
+ """Get a file or directory model."""
+ raise NotImplementedError('must be implemented in a subclass')
+
+ def save(self, model, path):
+ """
+ Save a file or directory model to path.
+
+ Should return the saved model with no content. Save implementations
+ should call self.run_pre_save_hook(model=model, path=path) prior to
+ writing any data.
+ """
+ raise NotImplementedError('must be implemented in a subclass')
+
+ def delete_file(self, path):
+ """Delete the file or directory at path."""
+ raise NotImplementedError('must be implemented in a subclass')
+
+ def rename_file(self, old_path, new_path):
+ """Rename a file or directory."""
+ raise NotImplementedError('must be implemented in a subclass')
+
+ # ContentsManager API part 2: methods that have useable default
+ # implementations, but can be overridden in subclasses.
+
+ def delete(self, path):
+ """Delete a file/directory and any associated checkpoints."""
+ path = path.strip('/')
+ if not path:
+ raise HTTPError(400, "Can't delete root")
+ self.delete_file(path)
+ self.checkpoints.delete_all_checkpoints(path)
+
+ def rename(self, old_path, new_path):
+ """Rename a file and any checkpoints associated with that file."""
+ self.rename_file(old_path, new_path)
+ self.checkpoints.rename_all_checkpoints(old_path, new_path)
+
+ def update(self, model, path):
+ """Update the file's path
+
+ For use in PATCH requests, to enable renaming a file without
+ re-uploading its contents. Only used for renaming at the moment.
+ """
+ path = path.strip('/')
+ new_path = model.get('path', path).strip('/')
+ if path != new_path:
+ self.rename(path, new_path)
+ model = self.get(new_path, content=False)
+ return model
+
+ def info_string(self):
+ return "Serving contents"
+
+ def get_kernel_path(self, path, model=None):
+ """Return the API path for the kernel
+
+ KernelManagers can turn this value into a filesystem path,
+ or ignore it altogether.
+
+ The default value here will start kernels in the directory of the
+ notebook server. FileContentsManager overrides this to use the
+ directory containing the notebook.
+ """
+ return ''
+
+ def increment_filename(self, filename, path='', insert=''):
+ """Increment a filename until it is unique.
+
+ Parameters
+ ----------
+ filename : unicode
+ The name of a file, including extension
+ path : unicode
+ The API path of the target's directory
+
+ Returns
+ -------
+ name : unicode
+ A filename that is unique, based on the input filename.
+ """
+ path = path.strip('/')
+ basename, ext = os.path.splitext(filename)
+ for i in itertools.count():
+ if i:
+ insert_i = '{}{}'.format(insert, i)
+ else:
+ insert_i = ''
+ name = u'{basename}{insert}{ext}'.format(basename=basename,
+ insert=insert_i, ext=ext)
+ if not self.exists(u'{}/{}'.format(path, name)):
+ break
+ return name
+
+ def validate_notebook_model(self, model):
+ """Add failed-validation message to model"""
+ try:
+ validate(model['content'])
+ except ValidationError as e:
+ model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
+ e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
+ )
+ return model
+
+ def new_untitled(self, path='', type='', ext=''):
+ """Create a new untitled file or directory in path
+
+ path must be a directory
+
+ File extension can be specified.
+
+ Use `new` to create files with a fully specified path (including filename).
+ """
+ path = path.strip('/')
+ if not self.dir_exists(path):
+ raise HTTPError(404, 'No such directory: %s' % path)
+
+ model = {}
+ if type:
+ model['type'] = type
+
+ if ext == '.ipynb':
+ model.setdefault('type', 'notebook')
+ else:
+ model.setdefault('type', 'file')
+
+ insert = ''
+ if model['type'] == 'directory':
+ untitled = self.untitled_directory
+ insert = ' '
+ elif model['type'] == 'notebook':
+ untitled = self.untitled_notebook
+ ext = '.ipynb'
+ elif model['type'] == 'file':
+ untitled = self.untitled_file
+ else:
+ raise HTTPError(400, "Unexpected model type: %r" % model['type'])
+
+ name = self.increment_filename(untitled + ext, path, insert=insert)
+ path = u'{0}/{1}'.format(path, name)
+ return self.new(model, path)
+
+ def new(self, model=None, path=''):
+ """Create a new file or directory and return its model with no content.
+
+ To create a new untitled entity in a directory, use `new_untitled`.
+ """
+ path = path.strip('/')
+ if model is None:
+ model = {}
+
+ if path.endswith('.ipynb'):
+ model.setdefault('type', 'notebook')
+ else:
+ model.setdefault('type', 'file')
+
+ # no content, not a directory, so fill out new-file model
+ if 'content' not in model and model['type'] != 'directory':
+ if model['type'] == 'notebook':
+ model['content'] = new_notebook()
+ model['format'] = 'json'
+ else:
+ model['content'] = ''
+ model['type'] = 'file'
+ model['format'] = 'text'
+
+ model = self.save(model, path)
+ return model
+
+ def copy(self, from_path, to_path=None):
+ """Copy an existing file and return its new model.
+
+ If to_path not specified, it will be the parent directory of from_path.
+ If to_path is a directory, filename will increment `from_path-Copy#.ext`.
+
+ from_path must be a full path to a file.
+ """
+ path = from_path.strip('/')
+ if to_path is not None:
+ to_path = to_path.strip('/')
+
+ if '/' in path:
+ from_dir, from_name = path.rsplit('/', 1)
+ else:
+ from_dir = ''
+ from_name = path
+
+ model = self.get(path)
+ model.pop('path', None)
+ model.pop('name', None)
+ if model['type'] == 'directory':
+ raise HTTPError(400, "Can't copy directories")
+
+ if to_path is None:
+ to_path = from_dir
+ if self.dir_exists(to_path):
+ name = copy_pat.sub(u'.', from_name)
+ to_name = self.increment_filename(name, to_path, insert='-Copy')
+ to_path = u'{0}/{1}'.format(to_path, to_name)
+
+ model = self.save(model, to_path)
+ return model
+
+ def log_info(self):
+ self.log.info(self.info_string())
+
+ def trust_notebook(self, path):
+ """Explicitly trust a notebook
+
+ Parameters
+ ----------
+ path : string
+ The path of a notebook
+ """
+ model = self.get(path)
+ nb = model['content']
+ self.log.warn("Trusting notebook %s", path)
+ self.notary.mark_cells(nb, True)
+ self.save(model, path)
+
+ def check_and_sign(self, nb, path=''):
+ """Check for trusted cells, and sign the notebook.
+
+ Called as a part of saving notebooks.
+
+ Parameters
+ ----------
+ nb : dict
+ The notebook dict
+ path : string
+ The notebook's path (for logging)
+ """
+ if self.notary.check_cells(nb):
+ self.notary.sign(nb)
+ else:
+ self.log.warn("Saving untrusted notebook %s", path)
+
+ def mark_trusted_cells(self, nb, path=''):
+ """Mark cells as trusted if the notebook signature matches.
+
+ Called as a part of loading notebooks.
+
+ Parameters
+ ----------
+ nb : dict
+ The notebook object (in current nbformat)
+ path : string
+ The notebook's path (for logging)
+ """
+ trusted = self.notary.check_signature(nb)
+ if not trusted:
+ self.log.warn("Notebook %s is not trusted", path)
+ self.notary.mark_cells(nb, trusted)
+
+ def should_list(self, name):
+ """Should this file/directory name be displayed in a listing?"""
+ return not any(fnmatch(name, glob) for glob in self.hide_globs)
+
+ # Part 3: Checkpoints API
+ def create_checkpoint(self, path):
+ """Create a checkpoint."""
+ return self.checkpoints.create_checkpoint(self, path)
+
+ def restore_checkpoint(self, checkpoint_id, path):
+ """
+ Restore a checkpoint.
+ """
+ self.checkpoints.restore_checkpoint(self, checkpoint_id, path)
+
+ def list_checkpoints(self, path):
+ return self.checkpoints.list_checkpoints(path)
+
+ def delete_checkpoint(self, checkpoint_id, path):
+ return self.checkpoints.delete_checkpoint(checkpoint_id, path)
diff --git a/notebook/services/contents/tests/__init__.py b/notebook/services/contents/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/contents/tests/__init__.py
diff --git a/notebook/services/contents/tests/test_contents_api.py b/notebook/services/contents/tests/test_contents_api.py
new file mode 100644
index 0000000..800a3a2
--- /dev/null
+++ b/notebook/services/contents/tests/test_contents_api.py
@@ -0,0 +1,694 @@
+# coding: utf-8
+"""Test the contents webservice API."""
+
+import base64
+from contextlib import contextmanager
+import io
+import json
+import os
+import shutil
+from unicodedata import normalize
+
+pjoin = os.path.join
+
+import requests
+
+from ..filecheckpoints import GenericFileCheckpoints
+
+from traitlets.config import Config
+from notebook.utils import url_path_join, url_escape, to_os_path
+from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
+from nbformat import read, write, from_dict
+from nbformat.v4 import (
+ new_notebook, new_markdown_cell,
+)
+from nbformat import v2
+from ipython_genutils import py3compat
+from ipython_genutils.tempdir import TemporaryDirectory
+
+def uniq_stable(elems):
+ """uniq_stable(elems) -> list
+
+ Return from an iterable, a list of all the unique elements in the input,
+ maintaining the order in which they first appear.
+ """
+ seen = set()
+ return [x for x in elems if x not in seen and not seen.add(x)]
+
+def notebooks_only(dir_model):
+ return [nb for nb in dir_model['content'] if nb['type']=='notebook']
+
+def dirs_only(dir_model):
+ return [x for x in dir_model['content'] if x['type']=='directory']
+
+
+class API(object):
+ """Wrapper for contents API calls."""
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def _req(self, verb, path, body=None, params=None):
+ response = requests.request(verb,
+ url_path_join(self.base_url, 'api/contents', path),
+ data=body, params=params,
+ )
+ response.raise_for_status()
+ return response
+
+ def list(self, path='/'):
+ return self._req('GET', path)
+
+ def read(self, path, type=None, format=None, content=None):
+ params = {}
+ if type is not None:
+ params['type'] = type
+ if format is not None:
+ params['format'] = format
+ if content == False:
+ params['content'] = '0'
+ return self._req('GET', path, params=params)
+
+ def create_untitled(self, path='/', ext='.ipynb'):
+ body = None
+ if ext:
+ body = json.dumps({'ext': ext})
+ return self._req('POST', path, body)
+
+ def mkdir_untitled(self, path='/'):
+ return self._req('POST', path, json.dumps({'type': 'directory'}))
+
+ def copy(self, copy_from, path='/'):
+ body = json.dumps({'copy_from':copy_from})
+ return self._req('POST', path, body)
+
+ def create(self, path='/'):
+ return self._req('PUT', path)
+
+ def upload(self, path, body):
+ return self._req('PUT', path, body)
+
+ def mkdir(self, path='/'):
+ return self._req('PUT', path, json.dumps({'type': 'directory'}))
+
+ def copy_put(self, copy_from, path='/'):
+ body = json.dumps({'copy_from':copy_from})
+ return self._req('PUT', path, body)
+
+ def save(self, path, body):
+ return self._req('PUT', path, body)
+
+ def delete(self, path='/'):
+ return self._req('DELETE', path)
+
+ def rename(self, path, new_path):
+ body = json.dumps({'path': new_path})
+ return self._req('PATCH', path, body)
+
+ def get_checkpoints(self, path):
+ return self._req('GET', url_path_join(path, 'checkpoints'))
+
+ def new_checkpoint(self, path):
+ return self._req('POST', url_path_join(path, 'checkpoints'))
+
+ def restore_checkpoint(self, path, checkpoint_id):
+ return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
+
+ def delete_checkpoint(self, path, checkpoint_id):
+ return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
+
+class APITest(NotebookTestBase):
+ """Test the kernels web service API"""
+ dirs_nbs = [('', 'inroot'),
+ ('Directory with spaces in', 'inspace'),
+ (u'unicodé', 'innonascii'),
+ ('foo', 'a'),
+ ('foo', 'b'),
+ ('foo', 'name with spaces'),
+ ('foo', u'unicodé'),
+ ('foo/bar', 'baz'),
+ ('ordering', 'A'),
+ ('ordering', 'b'),
+ ('ordering', 'C'),
+ (u'å b', u'ç d'),
+ ]
+ hidden_dirs = ['.hidden', '__pycache__']
+
+ # Don't include root dir.
+ dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs[1:]])
+ top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
+
+ @staticmethod
+ def _blob_for_name(name):
+ return name.encode('utf-8') + b'\xFF'
+
+ @staticmethod
+ def _txt_for_name(name):
+ return u'%s text file' % name
+
+ def to_os_path(self, api_path):
+ return to_os_path(api_path, root=self.notebook_dir.name)
+
+ def make_dir(self, api_path):
+ """Create a directory at api_path"""
+ os_path = self.to_os_path(api_path)
+ try:
+ os.makedirs(os_path)
+ except OSError:
+ print("Directory already exists: %r" % os_path)
+
+ def make_txt(self, api_path, txt):
+ """Make a text file at a given api_path"""
+ os_path = self.to_os_path(api_path)
+ with io.open(os_path, 'w', encoding='utf-8') as f:
+ f.write(txt)
+
+ def make_blob(self, api_path, blob):
+ """Make a binary file at a given api_path"""
+ os_path = self.to_os_path(api_path)
+ with io.open(os_path, 'wb') as f:
+ f.write(blob)
+
+ def make_nb(self, api_path, nb):
+ """Make a notebook file at a given api_path"""
+ os_path = self.to_os_path(api_path)
+
+ with io.open(os_path, 'w', encoding='utf-8') as f:
+ write(nb, f, version=4)
+
+ def delete_dir(self, api_path):
+ """Delete a directory at api_path, removing any contents."""
+ os_path = self.to_os_path(api_path)
+ shutil.rmtree(os_path, ignore_errors=True)
+
+ def delete_file(self, api_path):
+ """Delete a file at the given path if it exists."""
+ if self.isfile(api_path):
+ os.unlink(self.to_os_path(api_path))
+
+ def isfile(self, api_path):
+ return os.path.isfile(self.to_os_path(api_path))
+
+ def isdir(self, api_path):
+ return os.path.isdir(self.to_os_path(api_path))
+
+ def setUp(self):
+
+ for d in (self.dirs + self.hidden_dirs):
+ self.make_dir(d)
+
+ for d, name in self.dirs_nbs:
+ # create a notebook
+ nb = new_notebook()
+ self.make_nb(u'{}/{}.ipynb'.format(d, name), nb)
+
+ # create a text file
+ txt = self._txt_for_name(name)
+ self.make_txt(u'{}/{}.txt'.format(d, name), txt)
+
+ # create a binary file
+ blob = self._blob_for_name(name)
+ self.make_blob(u'{}/{}.blob'.format(d, name), blob)
+
+ self.api = API(self.base_url())
+
+ def tearDown(self):
+ for dname in (list(self.top_level_dirs) + self.hidden_dirs):
+ self.delete_dir(dname)
+ self.delete_file('inroot.ipynb')
+
+ def test_list_notebooks(self):
+ nbs = notebooks_only(self.api.list().json())
+ self.assertEqual(len(nbs), 1)
+ self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
+
+ nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
+ self.assertEqual(len(nbs), 1)
+ self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
+
+ nbs = notebooks_only(self.api.list(u'/unicodé/').json())
+ self.assertEqual(len(nbs), 1)
+ self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
+ self.assertEqual(nbs[0]['path'], u'unicodé/innonascii.ipynb')
+
+ nbs = notebooks_only(self.api.list('/foo/bar/').json())
+ self.assertEqual(len(nbs), 1)
+ self.assertEqual(nbs[0]['name'], 'baz.ipynb')
+ self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
+
+ nbs = notebooks_only(self.api.list('foo').json())
+ self.assertEqual(len(nbs), 4)
+ nbnames = { normalize('NFC', n['name']) for n in nbs }
+ expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb']
+ expected = { normalize('NFC', name) for name in expected }
+ self.assertEqual(nbnames, expected)
+
+ nbs = notebooks_only(self.api.list('ordering').json())
+ nbnames = [n['name'] for n in nbs]
+ expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
+ self.assertEqual(nbnames, expected)
+
+ def test_list_dirs(self):
+ dirs = dirs_only(self.api.list().json())
+ dir_names = {normalize('NFC', d['name']) for d in dirs}
+ self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
+
+ def test_get_dir_no_content(self):
+ for d in self.dirs:
+ model = self.api.read(d, content=False).json()
+ self.assertEqual(model['path'], d)
+ self.assertEqual(model['type'], 'directory')
+ self.assertIn('content', model)
+ self.assertEqual(model['content'], None)
+
+ def test_list_nonexistant_dir(self):
+ with assert_http_error(404):
+ self.api.list('nonexistant')
+
+ def test_get_nb_contents(self):
+ for d, name in self.dirs_nbs:
+ path = url_path_join(d, name + '.ipynb')
+ nb = self.api.read(path).json()
+ self.assertEqual(nb['name'], u'%s.ipynb' % name)
+ self.assertEqual(nb['path'], path)
+ self.assertEqual(nb['type'], 'notebook')
+ self.assertIn('content', nb)
+ self.assertEqual(nb['format'], 'json')
+ self.assertIn('metadata', nb['content'])
+ self.assertIsInstance(nb['content']['metadata'], dict)
+
+ def test_get_nb_no_content(self):
+ for d, name in self.dirs_nbs:
+ path = url_path_join(d, name + '.ipynb')
+ nb = self.api.read(path, content=False).json()
+ self.assertEqual(nb['name'], u'%s.ipynb' % name)
+ self.assertEqual(nb['path'], path)
+ self.assertEqual(nb['type'], 'notebook')
+ self.assertIn('content', nb)
+ self.assertEqual(nb['content'], None)
+
+ def test_get_contents_no_such_file(self):
+ # Name that doesn't exist - should be a 404
+ with assert_http_error(404):
+ self.api.read('foo/q.ipynb')
+
+ def test_get_text_file_contents(self):
+ for d, name in self.dirs_nbs:
+ path = url_path_join(d, name + '.txt')
+ model = self.api.read(path).json()
+ self.assertEqual(model['name'], u'%s.txt' % name)
+ self.assertEqual(model['path'], path)
+ self.assertIn('content', model)
+ self.assertEqual(model['format'], 'text')
+ self.assertEqual(model['type'], 'file')
+ self.assertEqual(model['content'], self._txt_for_name(name))
+
+ # Name that doesn't exist - should be a 404
+ with assert_http_error(404):
+ self.api.read('foo/q.txt')
+
+ # Specifying format=text should fail on a non-UTF-8 file
+ with assert_http_error(400):
+ self.api.read('foo/bar/baz.blob', type='file', format='text')
+
+ def test_get_binary_file_contents(self):
+ for d, name in self.dirs_nbs:
+ path = url_path_join(d, name + '.blob')
+ model = self.api.read(path).json()
+ self.assertEqual(model['name'], u'%s.blob' % name)
+ self.assertEqual(model['path'], path)
+ self.assertIn('content', model)
+ self.assertEqual(model['format'], 'base64')
+ self.assertEqual(model['type'], 'file')
+ self.assertEqual(
+ base64.decodestring(model['content'].encode('ascii')),
+ self._blob_for_name(name),
+ )
+
+ # Name that doesn't exist - should be a 404
+ with assert_http_error(404):
+ self.api.read('foo/q.txt')
+
+ def test_get_bad_type(self):
+ with assert_http_error(400):
+ self.api.read(u'unicodé', type='file') # this is a directory
+
+ with assert_http_error(400):
+ self.api.read(u'unicodé/innonascii.ipynb', type='directory')
+
+ def _check_created(self, resp, path, type='notebook'):
+ self.assertEqual(resp.status_code, 201)
+ location_header = py3compat.str_to_unicode(resp.headers['Location'])
+ self.assertEqual(location_header, url_path_join(self.url_prefix, u'api/contents', url_escape(path)))
+ rjson = resp.json()
+ self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
+ self.assertEqual(rjson['path'], path)
+ self.assertEqual(rjson['type'], type)
+ isright = self.isdir if type == 'directory' else self.isfile
+ assert isright(path)
+
+ def test_create_untitled(self):
+ resp = self.api.create_untitled(path=u'å b')
+ self._check_created(resp, u'å b/Untitled.ipynb')
+
+ # Second time
+ resp = self.api.create_untitled(path=u'å b')
+ self._check_created(resp, u'å b/Untitled1.ipynb')
+
+ # And two directories down
+ resp = self.api.create_untitled(path='foo/bar')
+ self._check_created(resp, 'foo/bar/Untitled.ipynb')
+
+ def test_create_untitled_txt(self):
+ resp = self.api.create_untitled(path='foo/bar', ext='.txt')
+ self._check_created(resp, 'foo/bar/untitled.txt', type='file')
+
+ resp = self.api.read(path='foo/bar/untitled.txt')
+ model = resp.json()
+ self.assertEqual(model['type'], 'file')
+ self.assertEqual(model['format'], 'text')
+ self.assertEqual(model['content'], '')
+
+ def test_upload(self):
+ nb = new_notebook()
+ nbmodel = {'content': nb, 'type': 'notebook'}
+ path = u'å b/Upload tést.ipynb'
+ resp = self.api.upload(path, body=json.dumps(nbmodel))
+ self._check_created(resp, path)
+
+ def test_mkdir_untitled(self):
+ resp = self.api.mkdir_untitled(path=u'å b')
+ self._check_created(resp, u'å b/Untitled Folder', type='directory')
+
+ # Second time
+ resp = self.api.mkdir_untitled(path=u'å b')
+ self._check_created(resp, u'å b/Untitled Folder 1', type='directory')
+
+ # And two directories down
+ resp = self.api.mkdir_untitled(path='foo/bar')
+ self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
+
+ def test_mkdir(self):
+ path = u'å b/New ∂ir'
+ resp = self.api.mkdir(path)
+ self._check_created(resp, path, type='directory')
+
+ def test_mkdir_hidden_400(self):
+ with assert_http_error(400):
+ resp = self.api.mkdir(u'å b/.hidden')
+
+ def test_upload_txt(self):
+ body = u'ünicode téxt'
+ model = {
+ 'content' : body,
+ 'format' : 'text',
+ 'type' : 'file',
+ }
+ path = u'å b/Upload tést.txt'
+ resp = self.api.upload(path, body=json.dumps(model))
+
+ # check roundtrip
+ resp = self.api.read(path)
+ model = resp.json()
+ self.assertEqual(model['type'], 'file')
+ self.assertEqual(model['format'], 'text')
+ self.assertEqual(model['content'], body)
+
+ def test_upload_b64(self):
+ body = b'\xFFblob'
+ b64body = base64.encodestring(body).decode('ascii')
+ model = {
+ 'content' : b64body,
+ 'format' : 'base64',
+ 'type' : 'file',
+ }
+ path = u'å b/Upload tést.blob'
+ resp = self.api.upload(path, body=json.dumps(model))
+
+ # check roundtrip
+ resp = self.api.read(path)
+ model = resp.json()
+ self.assertEqual(model['type'], 'file')
+ self.assertEqual(model['path'], path)
+ self.assertEqual(model['format'], 'base64')
+ decoded = base64.decodestring(model['content'].encode('ascii'))
+ self.assertEqual(decoded, body)
+
+ def test_upload_v2(self):
+ nb = v2.new_notebook()
+ ws = v2.new_worksheet()
+ nb.worksheets.append(ws)
+ ws.cells.append(v2.new_code_cell(input='print("hi")'))
+ nbmodel = {'content': nb, 'type': 'notebook'}
+ path = u'å b/Upload tést.ipynb'
+ resp = self.api.upload(path, body=json.dumps(nbmodel))
+ self._check_created(resp, path)
+ resp = self.api.read(path)
+ data = resp.json()
+ self.assertEqual(data['content']['nbformat'], 4)
+
+ def test_copy(self):
+ resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
+ self._check_created(resp, u'å b/ç d-Copy1.ipynb')
+
+ resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
+ self._check_created(resp, u'å b/ç d-Copy2.ipynb')
+
+ def test_copy_copy(self):
+ resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
+ self._check_created(resp, u'å b/ç d-Copy1.ipynb')
+
+ resp = self.api.copy(u'å b/ç d-Copy1.ipynb', u'å b')
+ self._check_created(resp, u'å b/ç d-Copy2.ipynb')
+
+ def test_copy_path(self):
+ resp = self.api.copy(u'foo/a.ipynb', u'å b')
+ self._check_created(resp, u'å b/a.ipynb')
+
+ resp = self.api.copy(u'foo/a.ipynb', u'å b')
+ self._check_created(resp, u'å b/a-Copy1.ipynb')
+
+ def test_copy_put_400(self):
+ with assert_http_error(400):
+ resp = self.api.copy_put(u'å b/ç d.ipynb', u'å b/cøpy.ipynb')
+
+ def test_copy_dir_400(self):
+ # can't copy directories
+ with assert_http_error(400):
+ resp = self.api.copy(u'å b', u'foo')
+
+ def test_delete(self):
+ for d, name in self.dirs_nbs:
+ print('%r, %r' % (d, name))
+ resp = self.api.delete(url_path_join(d, name + '.ipynb'))
+ self.assertEqual(resp.status_code, 204)
+
+ for d in self.dirs + ['/']:
+ nbs = notebooks_only(self.api.list(d).json())
+ print('------')
+ print(d)
+ print(nbs)
+ self.assertEqual(nbs, [])
+
+ def test_delete_dirs(self):
+ # depth-first delete everything, so we don't try to delete empty directories
+ for name in sorted(self.dirs + ['/'], key=len, reverse=True):
+ listing = self.api.list(name).json()['content']
+ for model in listing:
+ self.api.delete(model['path'])
+ listing = self.api.list('/').json()['content']
+ self.assertEqual(listing, [])
+
+ def test_delete_non_empty_dir(self):
+ """delete non-empty dir raises 400"""
+ with assert_http_error(400):
+ self.api.delete(u'å b')
+
+ def test_rename(self):
+ resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
+ self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
+ self.assertEqual(resp.json()['name'], 'z.ipynb')
+ self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
+ assert self.isfile('foo/z.ipynb')
+
+ nbs = notebooks_only(self.api.list('foo').json())
+ nbnames = set(n['name'] for n in nbs)
+ self.assertIn('z.ipynb', nbnames)
+ self.assertNotIn('a.ipynb', nbnames)
+
+ def test_checkpoints_follow_file(self):
+
+ # Read initial file state
+ orig = self.api.read('foo/a.ipynb')
+
+ # Create a checkpoint of initial state
+ r = self.api.new_checkpoint('foo/a.ipynb')
+ cp1 = r.json()
+
+ # Modify file and save
+ nbcontent = json.loads(orig.text)['content']
+ nb = from_dict(nbcontent)
+ hcell = new_markdown_cell('Created by test')
+ nb.cells.append(hcell)
+ nbmodel = {'content': nb, 'type': 'notebook'}
+ self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
+
+ # Rename the file.
+ self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
+
+ # Looking for checkpoints in the old location should yield no results.
+ self.assertEqual(self.api.get_checkpoints('foo/a.ipynb').json(), [])
+
+ # Looking for checkpoints in the new location should work.
+ cps = self.api.get_checkpoints('foo/z.ipynb').json()
+ self.assertEqual(cps, [cp1])
+
+ # Delete the file. The checkpoint should be deleted as well.
+ self.api.delete('foo/z.ipynb')
+ cps = self.api.get_checkpoints('foo/z.ipynb').json()
+ self.assertEqual(cps, [])
+
+ def test_rename_existing(self):
+ with assert_http_error(409):
+ self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
+
+ def test_save(self):
+ resp = self.api.read('foo/a.ipynb')
+ nbcontent = json.loads(resp.text)['content']
+ nb = from_dict(nbcontent)
+ nb.cells.append(new_markdown_cell(u'Created by test ³'))
+
+ nbmodel = {'content': nb, 'type': 'notebook'}
+ resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
+
+ nbcontent = self.api.read('foo/a.ipynb').json()['content']
+ newnb = from_dict(nbcontent)
+ self.assertEqual(newnb.cells[0].source,
+ u'Created by test ³')
+
+ def test_checkpoints(self):
+ resp = self.api.read('foo/a.ipynb')
+ r = self.api.new_checkpoint('foo/a.ipynb')
+ self.assertEqual(r.status_code, 201)
+ cp1 = r.json()
+ self.assertEqual(set(cp1), {'id', 'last_modified'})
+ self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
+
+ # Modify it
+ nbcontent = json.loads(resp.text)['content']
+ nb = from_dict(nbcontent)
+ hcell = new_markdown_cell('Created by test')
+ nb.cells.append(hcell)
+ # Save
+ nbmodel= {'content': nb, 'type': 'notebook'}
+ resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
+
+ # List checkpoints
+ cps = self.api.get_checkpoints('foo/a.ipynb').json()
+ self.assertEqual(cps, [cp1])
+
+ nbcontent = self.api.read('foo/a.ipynb').json()['content']
+ nb = from_dict(nbcontent)
+ self.assertEqual(nb.cells[0].source, 'Created by test')
+
+ # Restore cp1
+ r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
+ self.assertEqual(r.status_code, 204)
+ nbcontent = self.api.read('foo/a.ipynb').json()['content']
+ nb = from_dict(nbcontent)
+ self.assertEqual(nb.cells, [])
+
+ # Delete cp1
+ r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
+ self.assertEqual(r.status_code, 204)
+ cps = self.api.get_checkpoints('foo/a.ipynb').json()
+ self.assertEqual(cps, [])
+
+ def test_file_checkpoints(self):
+ """
+ Test checkpointing of non-notebook files.
+ """
+ filename = 'foo/a.txt'
+ resp = self.api.read(filename)
+ orig_content = json.loads(resp.text)['content']
+
+ # Create a checkpoint.
+ r = self.api.new_checkpoint(filename)
+ self.assertEqual(r.status_code, 201)
+ cp1 = r.json()
+ self.assertEqual(set(cp1), {'id', 'last_modified'})
+ self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
+
+ # Modify the file and save.
+ new_content = orig_content + '\nsecond line'
+ model = {
+ 'content': new_content,
+ 'type': 'file',
+ 'format': 'text',
+ }
+ resp = self.api.save(filename, body=json.dumps(model))
+
+ # List checkpoints
+ cps = self.api.get_checkpoints(filename).json()
+ self.assertEqual(cps, [cp1])
+
+ content = self.api.read(filename).json()['content']
+ self.assertEqual(content, new_content)
+
+ # Restore cp1
+ r = self.api.restore_checkpoint(filename, cp1['id'])
+ self.assertEqual(r.status_code, 204)
+ restored_content = self.api.read(filename).json()['content']
+ self.assertEqual(restored_content, orig_content)
+
+ # Delete cp1
+ r = self.api.delete_checkpoint(filename, cp1['id'])
+ self.assertEqual(r.status_code, 204)
+ cps = self.api.get_checkpoints(filename).json()
+ self.assertEqual(cps, [])
+
+ @contextmanager
+ def patch_cp_root(self, dirname):
+ """
+ Temporarily patch the root dir of our checkpoint manager.
+ """
+ cpm = self.notebook.contents_manager.checkpoints
+ old_dirname = cpm.root_dir
+ cpm.root_dir = dirname
+ try:
+ yield
+ finally:
+ cpm.root_dir = old_dirname
+
+ def test_checkpoints_separate_root(self):
+ """
+ Test that FileCheckpoints functions correctly even when it's
+ using a different root dir from FileContentsManager. This also keeps
+ the implementation honest for use with ContentsManagers that don't map
+ models to the filesystem
+
+ Override this method to a no-op when testing other managers.
+ """
+ with TemporaryDirectory() as td:
+ with self.patch_cp_root(td):
+ self.test_checkpoints()
+
+ with TemporaryDirectory() as td:
+ with self.patch_cp_root(td):
+ self.test_file_checkpoints()
+
+
+class GenericFileCheckpointsAPITest(APITest):
+ """
+ Run the tests from APITest with GenericFileCheckpoints.
+ """
+ config = Config()
+ config.FileContentsManager.checkpoints_class = GenericFileCheckpoints
+
+ def test_config_did_something(self):
+
+ self.assertIsInstance(
+ self.notebook.contents_manager.checkpoints,
+ GenericFileCheckpoints,
+ )
+
+
diff --git a/notebook/services/contents/tests/test_fileio.py b/notebook/services/contents/tests/test_fileio.py
new file mode 100644
index 0000000..256c664
--- /dev/null
+++ b/notebook/services/contents/tests/test_fileio.py
@@ -0,0 +1,131 @@
+# encoding: utf-8
+"""Tests for file IO"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import io as stdlib_io
+import os.path
+import stat
+
+import nose.tools as nt
+
+from ipython_genutils.testing.decorators import skip_win32
+from ..fileio import atomic_writing
+
+from ipython_genutils.tempdir import TemporaryDirectory
+
+umask = 0
+
+def test_atomic_writing():
+ class CustomExc(Exception): pass
+
+ with TemporaryDirectory() as td:
+ f1 = os.path.join(td, 'penguin')
+ with stdlib_io.open(f1, 'w') as f:
+ f.write(u'Before')
+
+ if os.name != 'nt':
+ os.chmod(f1, 0o701)
+ orig_mode = stat.S_IMODE(os.stat(f1).st_mode)
+
+ f2 = os.path.join(td, 'flamingo')
+ try:
+ os.symlink(f1, f2)
+ have_symlink = True
+ except (AttributeError, NotImplementedError, OSError):
+ # AttributeError: Python doesn't support it
+ # NotImplementedError: The system doesn't support it
+ # OSError: The user lacks the privilege (Windows)
+ have_symlink = False
+
+ with nt.assert_raises(CustomExc):
+ with atomic_writing(f1) as f:
+ f.write(u'Failing write')
+ raise CustomExc
+
+ # Because of the exception, the file should not have been modified
+ with stdlib_io.open(f1, 'r') as f:
+ nt.assert_equal(f.read(), u'Before')
+
+ with atomic_writing(f1) as f:
+ f.write(u'Overwritten')
+
+ with stdlib_io.open(f1, 'r') as f:
+ nt.assert_equal(f.read(), u'Overwritten')
+
+ if os.name != 'nt':
+ mode = stat.S_IMODE(os.stat(f1).st_mode)
+ nt.assert_equal(mode, orig_mode)
+
+ if have_symlink:
+ # Check that writing over a file preserves a symlink
+ with atomic_writing(f2) as f:
+ f.write(u'written from symlink')
+
+ with stdlib_io.open(f1, 'r') as f:
+ nt.assert_equal(f.read(), u'written from symlink')
+
+def _save_umask():
+ global umask
+ umask = os.umask(0)
+ os.umask(umask)
+
+def _restore_umask():
+ os.umask(umask)
+
+@skip_win32
+@nt.with_setup(_save_umask, _restore_umask)
+def test_atomic_writing_umask():
+ with TemporaryDirectory() as td:
+ os.umask(0o022)
+ f1 = os.path.join(td, '1')
+ with atomic_writing(f1) as f:
+ f.write(u'1')
+ mode = stat.S_IMODE(os.stat(f1).st_mode)
+ nt.assert_equal(mode, 0o644, '{:o} != 644'.format(mode))
+
+ os.umask(0o057)
+ f2 = os.path.join(td, '2')
+ with atomic_writing(f2) as f:
+ f.write(u'2')
+ mode = stat.S_IMODE(os.stat(f2).st_mode)
+ nt.assert_equal(mode, 0o620, '{:o} != 620'.format(mode))
+
+
+def test_atomic_writing_newlines():
+ with TemporaryDirectory() as td:
+ path = os.path.join(td, 'testfile')
+
+ lf = u'a\nb\nc\n'
+ plat = lf.replace(u'\n', os.linesep)
+ crlf = lf.replace(u'\n', u'\r\n')
+
+ # test default
+ with stdlib_io.open(path, 'w') as f:
+ f.write(lf)
+ with stdlib_io.open(path, 'r', newline='') as f:
+ read = f.read()
+ nt.assert_equal(read, plat)
+
+ # test newline=LF
+ with stdlib_io.open(path, 'w', newline='\n') as f:
+ f.write(lf)
+ with stdlib_io.open(path, 'r', newline='') as f:
+ read = f.read()
+ nt.assert_equal(read, lf)
+
+ # test newline=CRLF
+ with atomic_writing(path, newline='\r\n') as f:
+ f.write(lf)
+ with stdlib_io.open(path, 'r', newline='') as f:
+ read = f.read()
+ nt.assert_equal(read, crlf)
+
+ # test newline=no convert
+ text = u'crlf\r\ncr\rlf\n'
+ with atomic_writing(path, newline='') as f:
+ f.write(text)
+ with stdlib_io.open(path, 'r', newline='') as f:
+ read = f.read()
+ nt.assert_equal(read, text)
diff --git a/notebook/services/contents/tests/test_manager.py b/notebook/services/contents/tests/test_manager.py
new file mode 100644
index 0000000..c13e60f
--- /dev/null
+++ b/notebook/services/contents/tests/test_manager.py
@@ -0,0 +1,629 @@
+# coding: utf-8
+"""Tests for the notebook manager."""
+from __future__ import print_function
+
+import os
+import sys
+import time
+from contextlib import contextmanager
+from itertools import combinations
+
+from nose import SkipTest
+from tornado.web import HTTPError
+from unittest import TestCase
+from tempfile import NamedTemporaryFile
+
+from nbformat import v4 as nbformat
+
+from ipython_genutils.tempdir import TemporaryDirectory
+from traitlets import TraitError
+from ipython_genutils.testing import decorators as dec
+
+from ..filemanager import FileContentsManager
+
+
+def _make_dir(contents_manager, api_path):
+ """
+ Make a directory.
+ """
+ os_path = contents_manager._get_os_path(api_path)
+ try:
+ os.makedirs(os_path)
+ except OSError:
+ print("Directory already exists: %r" % os_path)
+
+
+class TestFileContentsManager(TestCase):
+
+ @contextmanager
+ def assertRaisesHTTPError(self, status, msg=None):
+ msg = msg or "Should have raised HTTPError(%i)" % status
+ try:
+ yield
+ except HTTPError as e:
+ self.assertEqual(e.status_code, status)
+ else:
+ self.fail(msg)
+
+ def symlink(self, contents_manager, src, dst):
+ """Make a symlink to src from dst
+
+ src and dst are api_paths
+ """
+ src_os_path = contents_manager._get_os_path(src)
+ dst_os_path = contents_manager._get_os_path(dst)
+ print(src_os_path, dst_os_path, os.path.isfile(src_os_path))
+ os.symlink(src_os_path, dst_os_path)
+
+ def test_root_dir(self):
+ with TemporaryDirectory() as td:
+ fm = FileContentsManager(root_dir=td)
+ self.assertEqual(fm.root_dir, td)
+
+ def test_missing_root_dir(self):
+ with TemporaryDirectory() as td:
+ root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
+ self.assertRaises(TraitError, FileContentsManager, root_dir=root)
+
+ def test_invalid_root_dir(self):
+ with NamedTemporaryFile() as tf:
+ self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name)
+
+ def test_get_os_path(self):
+ # full filesystem path should be returned with correct operating system
+ # separators.
+ with TemporaryDirectory() as td:
+ root = td
+ fm = FileContentsManager(root_dir=root)
+ path = fm._get_os_path('/path/to/notebook/test.ipynb')
+ rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
+ fs_path = os.path.join(fm.root_dir, *rel_path_list)
+ self.assertEqual(path, fs_path)
+
+ fm = FileContentsManager(root_dir=root)
+ path = fm._get_os_path('test.ipynb')
+ fs_path = os.path.join(fm.root_dir, 'test.ipynb')
+ self.assertEqual(path, fs_path)
+
+ fm = FileContentsManager(root_dir=root)
+ path = fm._get_os_path('////test.ipynb')
+ fs_path = os.path.join(fm.root_dir, 'test.ipynb')
+ self.assertEqual(path, fs_path)
+
+ def test_checkpoint_subdir(self):
+ subd = u'sub ∂ir'
+ cp_name = 'test-cp.ipynb'
+ with TemporaryDirectory() as td:
+ root = td
+ os.mkdir(os.path.join(td, subd))
+ fm = FileContentsManager(root_dir=root)
+ cpm = fm.checkpoints
+ cp_dir = cpm.checkpoint_path(
+ 'cp', 'test.ipynb'
+ )
+ cp_subdir = cpm.checkpoint_path(
+ 'cp', '/%s/test.ipynb' % subd
+ )
+ self.assertNotEqual(cp_dir, cp_subdir)
+ self.assertEqual(cp_dir, os.path.join(root, cpm.checkpoint_dir, cp_name))
+ self.assertEqual(cp_subdir, os.path.join(root, subd, cpm.checkpoint_dir, cp_name))
+
+ @dec.skip_win32
+ def test_bad_symlink(self):
+ with TemporaryDirectory() as td:
+ cm = FileContentsManager(root_dir=td)
+ path = 'test bad symlink'
+ _make_dir(cm, path)
+
+ file_model = cm.new_untitled(path=path, ext='.txt')
+
+ # create a broken symlink
+ self.symlink(cm, "target", '%s/%s' % (path, 'bad symlink'))
+ model = cm.get(path)
+ self.assertEqual(model['content'], [file_model])
+
+ @dec.skip_win32
+ def test_good_symlink(self):
+ with TemporaryDirectory() as td:
+ cm = FileContentsManager(root_dir=td)
+ parent = 'test good symlink'
+ name = 'good symlink'
+ path = '{0}/{1}'.format(parent, name)
+ _make_dir(cm, parent)
+
+ file_model = cm.new(path=parent + '/zfoo.txt')
+
+ # create a good symlink
+ self.symlink(cm, file_model['path'], path)
+ symlink_model = cm.get(path, content=False)
+ dir_model = cm.get(parent)
+ self.assertEqual(
+ sorted(dir_model['content'], key=lambda x: x['name']),
+ [symlink_model, file_model],
+ )
+
+ def test_403(self):
+ if hasattr(os, 'getuid'):
+ if os.getuid() == 0:
+ raise SkipTest("Can't test permissions as root")
+ if sys.platform.startswith('win'):
+ raise SkipTest("Can't test permissions on Windows")
+
+ with TemporaryDirectory() as td:
+ cm = FileContentsManager(root_dir=td)
+ model = cm.new_untitled(type='file')
+ os_path = cm._get_os_path(model['path'])
+
+ os.chmod(os_path, 0o400)
+ try:
+ with cm.open(os_path, 'w') as f:
+ f.write(u"don't care")
+ except HTTPError as e:
+ self.assertEqual(e.status_code, 403)
+ else:
+ self.fail("Should have raised HTTPError(403)")
+
+ def test_escape_root(self):
+ with TemporaryDirectory() as td:
+ cm = FileContentsManager(root_dir=td)
+ # make foo, bar next to root
+ with open(os.path.join(cm.root_dir, '..', 'foo'), 'w') as f:
+ f.write('foo')
+ with open(os.path.join(cm.root_dir, '..', 'bar'), 'w') as f:
+ f.write('bar')
+
+ with self.assertRaisesHTTPError(404):
+ cm.get('..')
+ with self.assertRaisesHTTPError(404):
+ cm.get('foo/../../../bar')
+ with self.assertRaisesHTTPError(404):
+ cm.delete('../foo')
+ with self.assertRaisesHTTPError(404):
+ cm.rename('../foo', '../bar')
+ with self.assertRaisesHTTPError(404):
+ cm.save(model={
+ 'type': 'file',
+ 'content': u'',
+ 'format': 'text',
+ }, path='../foo')
+
+
+class TestContentsManager(TestCase):
+ @contextmanager
+ def assertRaisesHTTPError(self, status, msg=None):
+ msg = msg or "Should have raised HTTPError(%i)" % status
+ try:
+ yield
+ except HTTPError as e:
+ self.assertEqual(e.status_code, status)
+ else:
+ self.fail(msg)
+
+ def make_populated_dir(self, api_path):
+ cm = self.contents_manager
+
+ self.make_dir(api_path)
+
+ cm.new(path="/".join([api_path, "nb.ipynb"]))
+ cm.new(path="/".join([api_path, "file.txt"]))
+
+ def check_populated_dir_files(self, api_path):
+ dir_model = self.contents_manager.get(api_path)
+
+ self.assertEqual(dir_model['path'], api_path)
+ self.assertEqual(dir_model['type'], "directory")
+
+ for entry in dir_model['content']:
+ if entry['type'] == "directory":
+ continue
+ elif entry['type'] == "file":
+ self.assertEqual(entry['name'], "file.txt")
+ complete_path = "/".join([api_path, "file.txt"])
+ self.assertEqual(entry["path"], complete_path)
+ elif entry['type'] == "notebook":
+ self.assertEqual(entry['name'], "nb.ipynb")
+ complete_path = "/".join([api_path, "nb.ipynb"])
+ self.assertEqual(entry["path"], complete_path)
+
+ def setUp(self):
+ self._temp_dir = TemporaryDirectory()
+ self.td = self._temp_dir.name
+ self.contents_manager = FileContentsManager(
+ root_dir=self.td,
+ )
+
+ def tearDown(self):
+ self._temp_dir.cleanup()
+
+ def make_dir(self, api_path):
+ """make a subdirectory at api_path
+
+ override in subclasses if contents are not on the filesystem.
+ """
+ _make_dir(self.contents_manager, api_path)
+
+ def add_code_cell(self, nb):
+ output = nbformat.new_output("display_data", {'application/javascript': "alert('hi');"})
+ cell = nbformat.new_code_cell("print('hi')", outputs=[output])
+ nb.cells.append(cell)
+
+ def new_notebook(self):
+ cm = self.contents_manager
+ model = cm.new_untitled(type='notebook')
+ name = model['name']
+ path = model['path']
+
+ full_model = cm.get(path)
+ nb = full_model['content']
+ nb['metadata']['counter'] = int(1e6 * time.time())
+ self.add_code_cell(nb)
+
+ cm.save(full_model, path)
+ return nb, name, path
+
+ def test_new_untitled(self):
+ cm = self.contents_manager
+ # Test in root directory
+ model = cm.new_untitled(type='notebook')
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertIn('type', model)
+ self.assertEqual(model['type'], 'notebook')
+ self.assertEqual(model['name'], 'Untitled.ipynb')
+ self.assertEqual(model['path'], 'Untitled.ipynb')
+
+ # Test in sub-directory
+ model = cm.new_untitled(type='directory')
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertIn('type', model)
+ self.assertEqual(model['type'], 'directory')
+ self.assertEqual(model['name'], 'Untitled Folder')
+ self.assertEqual(model['path'], 'Untitled Folder')
+ sub_dir = model['path']
+
+ model = cm.new_untitled(path=sub_dir)
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertIn('type', model)
+ self.assertEqual(model['type'], 'file')
+ self.assertEqual(model['name'], 'untitled')
+ self.assertEqual(model['path'], '%s/untitled' % sub_dir)
+
+ def test_modified_date(self):
+
+ cm = self.contents_manager
+
+ # Create a new notebook.
+ nb, name, path = self.new_notebook()
+ model = cm.get(path)
+
+ # Add a cell and save.
+ self.add_code_cell(model['content'])
+ cm.save(model, path)
+
+ # Reload notebook and verify that last_modified incremented.
+ saved = cm.get(path)
+ self.assertGreaterEqual(saved['last_modified'], model['last_modified'])
+
+ # Move the notebook and verify that last_modified stayed the same.
+ # (The frontend fires a warning if last_modified increases on the
+ # renamed file.)
+ new_path = 'renamed.ipynb'
+ cm.rename(path, new_path)
+ renamed = cm.get(new_path)
+ self.assertGreaterEqual(
+ renamed['last_modified'],
+ saved['last_modified'],
+ )
+
+ def test_get(self):
+ cm = self.contents_manager
+ # Create a notebook
+ model = cm.new_untitled(type='notebook')
+ name = model['name']
+ path = model['path']
+
+ # Check that we 'get' on the notebook we just created
+ model2 = cm.get(path)
+ assert isinstance(model2, dict)
+ self.assertIn('name', model2)
+ self.assertIn('path', model2)
+ self.assertEqual(model['name'], name)
+ self.assertEqual(model['path'], path)
+
+ nb_as_file = cm.get(path, content=True, type='file')
+ self.assertEqual(nb_as_file['path'], path)
+ self.assertEqual(nb_as_file['type'], 'file')
+ self.assertEqual(nb_as_file['format'], 'text')
+ self.assertNotIsInstance(nb_as_file['content'], dict)
+
+ nb_as_bin_file = cm.get(path, content=True, type='file', format='base64')
+ self.assertEqual(nb_as_bin_file['format'], 'base64')
+
+ # Test in sub-directory
+ sub_dir = '/foo/'
+ self.make_dir('foo')
+ model = cm.new_untitled(path=sub_dir, ext='.ipynb')
+ model2 = cm.get(sub_dir + name)
+ assert isinstance(model2, dict)
+ self.assertIn('name', model2)
+ self.assertIn('path', model2)
+ self.assertIn('content', model2)
+ self.assertEqual(model2['name'], 'Untitled.ipynb')
+ self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
+
+ # Test with a regular file.
+ file_model_path = cm.new_untitled(path=sub_dir, ext='.txt')['path']
+ file_model = cm.get(file_model_path)
+ self.assertDictContainsSubset(
+ {
+ 'content': u'',
+ 'format': u'text',
+ 'mimetype': u'text/plain',
+ 'name': u'untitled.txt',
+ 'path': u'foo/untitled.txt',
+ 'type': u'file',
+ 'writable': True,
+ },
+ file_model,
+ )
+ self.assertIn('created', file_model)
+ self.assertIn('last_modified', file_model)
+
+ # Test getting directory model
+
+ # Create a sub-sub directory to test getting directory contents with a
+ # subdir.
+ self.make_dir('foo/bar')
+ dirmodel = cm.get('foo')
+ self.assertEqual(dirmodel['type'], 'directory')
+ self.assertIsInstance(dirmodel['content'], list)
+ self.assertEqual(len(dirmodel['content']), 3)
+ self.assertEqual(dirmodel['path'], 'foo')
+ self.assertEqual(dirmodel['name'], 'foo')
+
+ # Directory contents should match the contents of each individual entry
+ # when requested with content=False.
+ model2_no_content = cm.get(sub_dir + name, content=False)
+ file_model_no_content = cm.get(u'foo/untitled.txt', content=False)
+ sub_sub_dir_no_content = cm.get('foo/bar', content=False)
+ self.assertEqual(sub_sub_dir_no_content['path'], 'foo/bar')
+ self.assertEqual(sub_sub_dir_no_content['name'], 'bar')
+
+ for entry in dirmodel['content']:
+ # Order isn't guaranteed by the spec, so this is a hacky way of
+ # verifying that all entries are matched.
+ if entry['path'] == sub_sub_dir_no_content['path']:
+ self.assertEqual(entry, sub_sub_dir_no_content)
+ elif entry['path'] == model2_no_content['path']:
+ self.assertEqual(entry, model2_no_content)
+ elif entry['path'] == file_model_no_content['path']:
+ self.assertEqual(entry, file_model_no_content)
+ else:
+ self.fail("Unexpected directory entry: %s" % entry())
+
+ with self.assertRaises(HTTPError):
+ cm.get('foo', type='file')
+
+ def test_update(self):
+ cm = self.contents_manager
+ # Create a notebook
+ model = cm.new_untitled(type='notebook')
+ name = model['name']
+ path = model['path']
+
+ # Change the name in the model for rename
+ model['path'] = 'test.ipynb'
+ model = cm.update(model, path)
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertEqual(model['name'], 'test.ipynb')
+
+ # Make sure the old name is gone
+ self.assertRaises(HTTPError, cm.get, path)
+
+ # Test in sub-directory
+ # Create a directory and notebook in that directory
+ sub_dir = '/foo/'
+ self.make_dir('foo')
+ model = cm.new_untitled(path=sub_dir, type='notebook')
+ path = model['path']
+
+ # Change the name in the model for rename
+ d = path.rsplit('/', 1)[0]
+ new_path = model['path'] = d + '/test_in_sub.ipynb'
+ model = cm.update(model, path)
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertEqual(model['name'], 'test_in_sub.ipynb')
+ self.assertEqual(model['path'], new_path)
+
+ # Make sure the old name is gone
+ self.assertRaises(HTTPError, cm.get, path)
+
+ def test_save(self):
+ cm = self.contents_manager
+ # Create a notebook
+ model = cm.new_untitled(type='notebook')
+ name = model['name']
+ path = model['path']
+
+ # Get the model with 'content'
+ full_model = cm.get(path)
+
+ # Save the notebook
+ model = cm.save(full_model, path)
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertEqual(model['name'], name)
+ self.assertEqual(model['path'], path)
+
+ # Test in sub-directory
+ # Create a directory and notebook in that directory
+ sub_dir = '/foo/'
+ self.make_dir('foo')
+ model = cm.new_untitled(path=sub_dir, type='notebook')
+ name = model['name']
+ path = model['path']
+ model = cm.get(path)
+
+ # Change the name in the model for rename
+ model = cm.save(model, path)
+ assert isinstance(model, dict)
+ self.assertIn('name', model)
+ self.assertIn('path', model)
+ self.assertEqual(model['name'], 'Untitled.ipynb')
+ self.assertEqual(model['path'], 'foo/Untitled.ipynb')
+
+ def test_delete(self):
+ cm = self.contents_manager
+ # Create a notebook
+ nb, name, path = self.new_notebook()
+
+ # Delete the notebook
+ cm.delete(path)
+
+ # Check that deleting a non-existent path raises an error.
+ self.assertRaises(HTTPError, cm.delete, path)
+
+ # Check that a 'get' on the deleted notebook raises and error
+ self.assertRaises(HTTPError, cm.get, path)
+
+ def test_rename(self):
+ cm = self.contents_manager
+ # Create a new notebook
+ nb, name, path = self.new_notebook()
+
+ # Rename the notebook
+ cm.rename(path, "changed_path")
+
+ # Attempting to get the notebook under the old name raises an error
+ self.assertRaises(HTTPError, cm.get, path)
+ # Fetching the notebook under the new name is successful
+ assert isinstance(cm.get("changed_path"), dict)
+
+ # Ported tests on nested directory renaming from pgcontents
+ all_dirs = ['foo', 'bar', 'foo/bar', 'foo/bar/foo', 'foo/bar/foo/bar']
+ unchanged_dirs = all_dirs[:2]
+ changed_dirs = all_dirs[2:]
+
+ for _dir in all_dirs:
+ self.make_populated_dir(_dir)
+ self.check_populated_dir_files(_dir)
+
+ # Renaming to an existing directory should fail
+ for src, dest in combinations(all_dirs, 2):
+ with self.assertRaisesHTTPError(409):
+ cm.rename(src, dest)
+
+ # Creating a notebook in a non_existant directory should fail
+ with self.assertRaisesHTTPError(404):
+ cm.new_untitled("foo/bar_diff", ext=".ipynb")
+
+ cm.rename("foo/bar", "foo/bar_diff")
+
+ # Assert that unchanged directories remain so
+ for unchanged in unchanged_dirs:
+ self.check_populated_dir_files(unchanged)
+
+ # Assert changed directories can no longer be accessed under old names
+ for changed_dirname in changed_dirs:
+ with self.assertRaisesHTTPError(404):
+ cm.get(changed_dirname)
+
+ new_dirname = changed_dirname.replace("foo/bar", "foo/bar_diff", 1)
+
+ self.check_populated_dir_files(new_dirname)
+
+ # Created a notebook in the renamed directory should work
+ cm.new_untitled("foo/bar_diff", ext=".ipynb")
+
+ def test_delete_root(self):
+ cm = self.contents_manager
+ with self.assertRaises(HTTPError) as err:
+ cm.delete('')
+ self.assertEqual(err.exception.status_code, 400)
+
+ def test_copy(self):
+ cm = self.contents_manager
+ parent = u'å b'
+ name = u'nb √.ipynb'
+ path = u'{0}/{1}'.format(parent, name)
+ self.make_dir(parent)
+
+ orig = cm.new(path=path)
+ # copy with unspecified name
+ copy = cm.copy(path)
+ self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb'))
+
+ # copy with specified name
+ copy2 = cm.copy(path, u'å b/copy 2.ipynb')
+ self.assertEqual(copy2['name'], u'copy 2.ipynb')
+ self.assertEqual(copy2['path'], u'å b/copy 2.ipynb')
+ # copy with specified path
+ copy2 = cm.copy(path, u'/')
+ self.assertEqual(copy2['name'], name)
+ self.assertEqual(copy2['path'], name)
+
+ def test_trust_notebook(self):
+ cm = self.contents_manager
+ nb, name, path = self.new_notebook()
+
+ untrusted = cm.get(path)['content']
+ assert not cm.notary.check_cells(untrusted)
+
+ # print(untrusted)
+ cm.trust_notebook(path)
+ trusted = cm.get(path)['content']
+ # print(trusted)
+ assert cm.notary.check_cells(trusted)
+
+ def test_mark_trusted_cells(self):
+ cm = self.contents_manager
+ nb, name, path = self.new_notebook()
+
+ cm.mark_trusted_cells(nb, path)
+ for cell in nb.cells:
+ if cell.cell_type == 'code':
+ assert not cell.metadata.trusted
+
+ cm.trust_notebook(path)
+ nb = cm.get(path)['content']
+ for cell in nb.cells:
+ if cell.cell_type == 'code':
+ assert cell.metadata.trusted
+
+ def test_check_and_sign(self):
+ cm = self.contents_manager
+ nb, name, path = self.new_notebook()
+
+ cm.mark_trusted_cells(nb, path)
+ cm.check_and_sign(nb, path)
+ assert not cm.notary.check_signature(nb)
+
+ cm.trust_notebook(path)
+ nb = cm.get(path)['content']
+ cm.mark_trusted_cells(nb, path)
+ cm.check_and_sign(nb, path)
+ assert cm.notary.check_signature(nb)
+
+
+class TestContentsManagerNoAtomic(TestContentsManager):
+ """
+ Make same test in no atomic case than in atomic case, using inheritance
+ """
+
+ def setUp(self):
+ self._temp_dir = TemporaryDirectory()
+ self.td = self._temp_dir.name
+ self.contents_manager = FileContentsManager(
+ root_dir = self.td,
+ )
+ self.contents_manager.use_atomic_writing = False
diff --git a/notebook/services/contents/tz.py b/notebook/services/contents/tz.py
new file mode 100644
index 0000000..b315d53
--- /dev/null
+++ b/notebook/services/contents/tz.py
@@ -0,0 +1,46 @@
+# encoding: utf-8
+"""
+Timezone utilities
+
+Just UTC-awareness right now
+"""
+
+#-----------------------------------------------------------------------------
+# Copyright (C) 2013 The IPython Development Team
+#
+# Distributed under the terms of the BSD License. The full license is in
+# the file COPYING, distributed as part of this software.
+#-----------------------------------------------------------------------------
+
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+from datetime import tzinfo, timedelta, datetime
+
+#-----------------------------------------------------------------------------
+# Code
+#-----------------------------------------------------------------------------
+# constant for zero offset
+ZERO = timedelta(0)
+
+class tzUTC(tzinfo):
+ """tzinfo object for UTC (zero offset)"""
+
+ def utcoffset(self, d):
+ return ZERO
+
+ def dst(self, d):
+ return ZERO
+
+UTC = tzUTC()
+
+def utc_aware(unaware):
+ """decorator for adding UTC tzinfo to datetime's utcfoo methods"""
+ def utc_method(*args, **kwargs):
+ dt = unaware(*args, **kwargs)
+ return dt.replace(tzinfo=UTC)
+ return utc_method
+
+utcfromtimestamp = utc_aware(datetime.utcfromtimestamp)
+utcnow = utc_aware(datetime.utcnow)
diff --git a/notebook/services/kernels/__init__.py b/notebook/services/kernels/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/kernels/__init__.py
diff --git a/notebook/services/kernels/handlers.py b/notebook/services/kernels/handlers.py
new file mode 100644
index 0000000..9536c1b
--- /dev/null
+++ b/notebook/services/kernels/handlers.py
@@ -0,0 +1,435 @@
+"""Tornado handlers for kernels.
+
+Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#kernels-api
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import json
+import logging
+from tornado import gen, web
+from tornado.concurrent import Future
+from tornado.ioloop import IOLoop
+
+from jupyter_client.jsonutil import date_default
+from ipython_genutils.py3compat import cast_unicode
+from notebook.utils import url_path_join, url_escape
+
+from ...base.handlers import APIHandler, json_errors
+from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
+
+from jupyter_client import protocol_version as client_protocol_version
+
+class MainKernelHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def get(self):
+ km = self.kernel_manager
+ kernels = yield gen.maybe_future(km.list_kernels())
+ self.finish(json.dumps(kernels))
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def post(self):
+ km = self.kernel_manager
+ model = self.get_json_body()
+ if model is None:
+ model = {
+ 'name': km.default_kernel_name
+ }
+ else:
+ model.setdefault('name', km.default_kernel_name)
+
+ kernel_id = yield gen.maybe_future(km.start_kernel(kernel_name=model['name']))
+ model = km.kernel_model(kernel_id)
+ location = url_path_join(self.base_url, 'api', 'kernels', url_escape(kernel_id))
+ self.set_header('Location', location)
+ self.set_status(201)
+ self.finish(json.dumps(model))
+
+
+class KernelHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ def get(self, kernel_id):
+ km = self.kernel_manager
+ km._check_kernel_id(kernel_id)
+ model = km.kernel_model(kernel_id)
+ self.finish(json.dumps(model))
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def delete(self, kernel_id):
+ km = self.kernel_manager
+ yield gen.maybe_future(km.shutdown_kernel(kernel_id))
+ self.set_status(204)
+ self.finish()
+
+
+class KernelActionHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def post(self, kernel_id, action):
+ km = self.kernel_manager
+ if action == 'interrupt':
+ km.interrupt_kernel(kernel_id)
+ self.set_status(204)
+ if action == 'restart':
+
+ try:
+ yield gen.maybe_future(km.restart_kernel(kernel_id))
+ except Exception as e:
+ self.log.error("Exception restarting kernel", exc_info=True)
+ self.set_status(500)
+ else:
+ model = km.kernel_model(kernel_id)
+ self.write(json.dumps(model))
+ self.finish()
+
+
+class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
+
+ # class-level registry of open sessions
+ # allows checking for conflict on session-id,
+ # which is used as a zmq identity and must be unique.
+ _open_sessions = {}
+
+ @property
+ def kernel_info_timeout(self):
+ return self.settings.get('kernel_info_timeout', 10)
+
+ @property
+ def iopub_msg_rate_limit(self):
+ return self.settings.get('iopub_msg_rate_limit', None)
+
+ @property
+ def iopub_data_rate_limit(self):
+ return self.settings.get('iopub_data_rate_limit', None)
+
+ @property
+ def rate_limit_window(self):
+ return self.settings.get('rate_limit_window', 1.0)
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
+
+ def create_stream(self):
+ km = self.kernel_manager
+ identity = self.session.bsession
+ for channel in ('shell', 'iopub', 'stdin'):
+ meth = getattr(km, 'connect_' + channel)
+ self.channels[channel] = stream = meth(self.kernel_id, identity=identity)
+ stream.channel = channel
+ km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
+ km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
+
+ def request_kernel_info(self):
+ """send a request for kernel_info"""
+ km = self.kernel_manager
+ kernel = km.get_kernel(self.kernel_id)
+ try:
+ # check for previous request
+ future = kernel._kernel_info_future
+ except AttributeError:
+ self.log.debug("Requesting kernel info from %s", self.kernel_id)
+ # Create a kernel_info channel to query the kernel protocol version.
+ # This channel will be closed after the kernel_info reply is received.
+ if self.kernel_info_channel is None:
+ self.kernel_info_channel = km.connect_shell(self.kernel_id)
+ self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
+ self.session.send(self.kernel_info_channel, "kernel_info_request")
+ # store the future on the kernel, so only one request is sent
+ kernel._kernel_info_future = self._kernel_info_future
+ else:
+ if not future.done():
+ self.log.debug("Waiting for pending kernel_info request")
+ future.add_done_callback(lambda f: self._finish_kernel_info(f.result()))
+ return self._kernel_info_future
+
+ def _handle_kernel_info_reply(self, msg):
+ """process the kernel_info_reply
+
+ enabling msg spec adaptation, if necessary
+ """
+ idents,msg = self.session.feed_identities(msg)
+ try:
+ msg = self.session.deserialize(msg)
+ except:
+ self.log.error("Bad kernel_info reply", exc_info=True)
+ self._kernel_info_future.set_result({})
+ return
+ else:
+ info = msg['content']
+ self.log.debug("Received kernel info: %s", info)
+ if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info:
+ self.log.error("Kernel info request failed, assuming current %s", info)
+ info = {}
+ self._finish_kernel_info(info)
+
+ # close the kernel_info channel, we don't need it anymore
+ if self.kernel_info_channel:
+ self.kernel_info_channel.close()
+ self.kernel_info_channel = None
+
+ def _finish_kernel_info(self, info):
+ """Finish handling kernel_info reply
+
+ Set up protocol adaptation, if needed,
+ and signal that connection can continue.
+ """
+ protocol_version = info.get('protocol_version', client_protocol_version)
+ if protocol_version != client_protocol_version:
+ self.session.adapt_version = int(protocol_version.split('.')[0])
+ self.log.info("Adapting to protocol v%s for kernel %s", protocol_version, self.kernel_id)
+ if not self._kernel_info_future.done():
+ self._kernel_info_future.set_result(info)
+
+ def initialize(self):
+ super(ZMQChannelsHandler, self).initialize()
+ self.zmq_stream = None
+ self.channels = {}
+ self.kernel_id = None
+ self.kernel_info_channel = None
+ self._kernel_info_future = Future()
+ self._close_future = Future()
+ self.session_key = ''
+
+ # Rate limiting code
+ self._iopub_window_msg_count = 0
+ self._iopub_window_byte_count = 0
+ self._iopub_msgs_exceeded = False
+ self._iopub_data_exceeded = False
+ # Queue of (time stamp, byte count)
+ # Allows you to specify that the byte count should be lowered
+ # by a delta amount at some point in the future.
+ self._iopub_window_byte_queue = []
+
+ @gen.coroutine
+ def pre_get(self):
+ # authenticate first
+ super(ZMQChannelsHandler, self).pre_get()
+ # check session collision:
+ yield self._register_session()
+ # then request kernel info, waiting up to a certain time before giving up.
+ # We don't want to wait forever, because browsers don't take it well when
+ # servers never respond to websocket connection requests.
+ kernel = self.kernel_manager.get_kernel(self.kernel_id)
+ self.session.key = kernel.session.key
+ future = self.request_kernel_info()
+
+ def give_up():
+ """Don't wait forever for the kernel to reply"""
+ if future.done():
+ return
+ self.log.warn("Timeout waiting for kernel_info reply from %s", self.kernel_id)
+ future.set_result({})
+ loop = IOLoop.current()
+ loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up)
+ # actually wait for it
+ yield future
+
+ @gen.coroutine
+ def get(self, kernel_id):
+ self.kernel_id = cast_unicode(kernel_id, 'ascii')
+ yield super(ZMQChannelsHandler, self).get(kernel_id=kernel_id)
+
+ @gen.coroutine
+ def _register_session(self):
+ """Ensure we aren't creating a duplicate session.
+
+ If a previous identical session is still open, close it to avoid collisions.
+ This is likely due to a client reconnecting from a lost network connection,
+ where the socket on our side has not been cleaned up yet.
+ """
+ self.session_key = '%s:%s' % (self.kernel_id, self.session.session)
+ stale_handler = self._open_sessions.get(self.session_key)
+ if stale_handler:
+ self.log.warning("Replacing stale connection: %s", self.session_key)
+ yield stale_handler.close()
+ self._open_sessions[self.session_key] = self
+
+ def open(self, kernel_id):
+ super(ZMQChannelsHandler, self).open()
+ try:
+ self.create_stream()
+ except web.HTTPError as e:
+ self.log.error("Error opening stream: %s", e)
+ # WebSockets don't response to traditional error codes so we
+ # close the connection.
+ for channel, stream in self.channels.items():
+ if not stream.closed():
+ stream.close()
+ self.close()
+ else:
+ for channel, stream in self.channels.items():
+ stream.on_recv_stream(self._on_zmq_reply)
+
+ def on_message(self, msg):
+ if not self.channels:
+ # already closed, ignore the message
+ self.log.debug("Received message on closed websocket %r", msg)
+ return
+ if isinstance(msg, bytes):
+ msg = deserialize_binary_message(msg)
+ else:
+ msg = json.loads(msg)
+ channel = msg.pop('channel', None)
+ if channel is None:
+ self.log.warn("No channel specified, assuming shell: %s", msg)
+ channel = 'shell'
+ if channel not in self.channels:
+ self.log.warn("No such channel: %r", channel)
+ return
+ stream = self.channels[channel]
+ self.session.send(stream, msg)
+
+ def _on_zmq_reply(self, stream, msg_list):
+ idents, fed_msg_list = self.session.feed_identities(msg_list)
+ msg = self.session.deserialize(fed_msg_list)
+ parent = msg['parent_header']
+ def write_stderr(error_message):
+ self.log.warn(error_message)
+ msg = self.session.msg("stream",
+ content={"text": error_message, "name": "stderr"},
+ parent=parent
+ )
+ msg['channel'] = 'iopub'
+ self.write_message(json.dumps(msg, default=date_default))
+
+ channel = getattr(stream, 'channel', None)
+ msg_type = msg['header']['msg_type']
+ if channel == 'iopub' and msg_type not in {'status', 'comm_open', 'execute_input'}:
+
+ # Remove the counts queued for removal.
+ now = IOLoop.current().time()
+ while len(self._iopub_window_byte_queue) > 0:
+ queued = self._iopub_window_byte_queue[0]
+ if (now >= queued[0]):
+ self._iopub_window_byte_count -= queued[1]
+ self._iopub_window_msg_count -= 1
+ del self._iopub_window_byte_queue[0]
+ else:
+ # This part of the queue hasn't be reached yet, so we can
+ # abort the loop.
+ break
+
+ # Increment the bytes and message count
+ self._iopub_window_msg_count += 1
+ byte_count = sum([len(x) for x in msg_list])
+ self._iopub_window_byte_count += byte_count
+
+ # Queue a removal of the byte and message count for a time in the
+ # future, when we are no longer interested in it.
+ self._iopub_window_byte_queue.append((now + self.rate_limit_window, byte_count))
+
+ # Check the limits, set the limit flags, and reset the
+ # message and data counts.
+ msg_rate = float(self._iopub_window_msg_count) / self.rate_limit_window
+ data_rate = float(self._iopub_window_byte_count) / self.rate_limit_window
+
+ # Check the msg rate
+ if self.iopub_msg_rate_limit is not None and msg_rate > self.iopub_msg_rate_limit and self.iopub_msg_rate_limit > 0:
+ if not self._iopub_msgs_exceeded:
+ self._iopub_msgs_exceeded = True
+ write_stderr("""iopub message rate exceeded. The
+ notebook server will temporarily stop sending iopub
+ messages to the client in order to avoid crashing it.
+ To change this limit, set the config variable
+ `--NotebookApp.iopub_msg_rate_limit`.""")
+ return
+ else:
+ if self._iopub_msgs_exceeded:
+ self._iopub_msgs_exceeded = False
+ if not self._iopub_data_exceeded:
+ self.log.warn("iopub messages resumed")
+
+ # Check the data rate
+ if self.iopub_data_rate_limit is not None and data_rate > self.iopub_data_rate_limit and self.iopub_data_rate_limit > 0:
+ if not self._iopub_data_exceeded:
+ self._iopub_data_exceeded = True
+ write_stderr("""iopub data rate exceeded. The
+ notebook server will temporarily stop sending iopub
+ messages to the client in order to avoid crashing it.
+ To change this limit, set the config variable
+ `--NotebookApp.iopub_data_rate_limit`.""")
+ return
+ else:
+ if self._iopub_data_exceeded:
+ self._iopub_data_exceeded = False
+ if not self._iopub_msgs_exceeded:
+ self.log.warn("iopub messages resumed")
+
+ # If either of the limit flags are set, do not send the message.
+ if self._iopub_msgs_exceeded or self._iopub_data_exceeded:
+ return
+ super(ZMQChannelsHandler, self)._on_zmq_reply(stream, msg)
+
+ def close(self):
+ super(ZMQChannelsHandler, self).close()
+ return self._close_future
+
+ def on_close(self):
+ self.log.debug("Websocket closed %s", self.session_key)
+ # unregister myself as an open session (only if it's really me)
+ if self._open_sessions.get(self.session_key) is self:
+ self._open_sessions.pop(self.session_key)
+ km = self.kernel_manager
+ if self.kernel_id in km:
+ km.remove_restart_callback(
+ self.kernel_id, self.on_kernel_restarted,
+ )
+ km.remove_restart_callback(
+ self.kernel_id, self.on_restart_failed, 'dead',
+ )
+ # This method can be called twice, once by self.kernel_died and once
+ # from the WebSocket close event. If the WebSocket connection is
+ # closed before the ZMQ streams are setup, they could be None.
+ for channel, stream in self.channels.items():
+ if stream is not None and not stream.closed():
+ stream.on_recv(None)
+ # close the socket directly, don't wait for the stream
+ socket = stream.socket
+ stream.close()
+ socket.close()
+
+ self.channels = {}
+ self._close_future.set_result(None)
+
+ def _send_status_message(self, status):
+ msg = self.session.msg("status",
+ {'execution_state': status}
+ )
+ msg['channel'] = 'iopub'
+ self.write_message(json.dumps(msg, default=date_default))
+
+ def on_kernel_restarted(self):
+ logging.warn("kernel %s restarted", self.kernel_id)
+ self._send_status_message('restarting')
+
+ def on_restart_failed(self):
+ logging.error("kernel %s restarted failed!", self.kernel_id)
+ self._send_status_message('dead')
+
+
+#-----------------------------------------------------------------------------
+# URL to handler mappings
+#-----------------------------------------------------------------------------
+
+
+_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
+_kernel_action_regex = r"(?P<action>restart|interrupt)"
+
+default_handlers = [
+ (r"/api/kernels", MainKernelHandler),
+ (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
+ (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
+ (r"/api/kernels/%s/channels" % _kernel_id_regex, ZMQChannelsHandler),
+]
diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py
new file mode 100644
index 0000000..d80cc3f
--- /dev/null
+++ b/notebook/services/kernels/kernelmanager.py
@@ -0,0 +1,169 @@
+"""A MultiKernelManager for use in the notebook webserver
+
+- raises HTTPErrors
+- creates REST API models
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import os
+
+from tornado import gen, web
+from tornado.concurrent import Future
+from tornado.ioloop import IOLoop
+
+from jupyter_client.multikernelmanager import MultiKernelManager
+from traitlets import List, Unicode, TraitError
+
+from notebook.utils import to_os_path
+from ipython_genutils.py3compat import getcwd
+
+
+class MappingKernelManager(MultiKernelManager):
+ """A KernelManager that handles notebook mapping and HTTP error handling"""
+
+ def _kernel_manager_class_default(self):
+ return "jupyter_client.ioloop.IOLoopKernelManager"
+
+ kernel_argv = List(Unicode())
+
+ root_dir = Unicode(config=True)
+
+ def _root_dir_default(self):
+ try:
+ return self.parent.notebook_dir
+ except AttributeError:
+ return getcwd()
+
+ def _root_dir_changed(self, name, old, new):
+ """Do a bit of validation of the root dir."""
+ if not os.path.isabs(new):
+ # If we receive a non-absolute path, make it absolute.
+ self.root_dir = os.path.abspath(new)
+ return
+ if not os.path.exists(new) or not os.path.isdir(new):
+ raise TraitError("kernel root dir %r is not a directory" % new)
+
+ #-------------------------------------------------------------------------
+ # Methods for managing kernels and sessions
+ #-------------------------------------------------------------------------
+
+ def _handle_kernel_died(self, kernel_id):
+ """notice that a kernel died"""
+ self.log.warn("Kernel %s died, removing from map.", kernel_id)
+ self.remove_kernel(kernel_id)
+
+ def cwd_for_path(self, path):
+ """Turn API path into absolute OS path."""
+ os_path = to_os_path(path, self.root_dir)
+ # in the case of notebooks and kernels not being on the same filesystem,
+ # walk up to root_dir if the paths don't exist
+ while not os.path.isdir(os_path) and os_path != self.root_dir:
+ os_path = os.path.dirname(os_path)
+ return os_path
+
+ @gen.coroutine
+ def start_kernel(self, kernel_id=None, path=None, **kwargs):
+ """Start a kernel for a session and return its kernel_id.
+
+ Parameters
+ ----------
+ kernel_id : uuid
+ The uuid to associate the new kernel with. If this
+ is not None, this kernel will be persistent whenever it is
+ requested.
+ path : API path
+ The API path (unicode, '/' delimited) for the cwd.
+ Will be transformed to an OS path relative to root_dir.
+ kernel_name : str
+ The name identifying which kernel spec to launch. This is ignored if
+ an existing kernel is returned, but it may be checked in the future.
+ """
+ if kernel_id is None:
+ if path is not None:
+ kwargs['cwd'] = self.cwd_for_path(path)
+ kernel_id = yield gen.maybe_future(
+ super(MappingKernelManager, self).start_kernel(**kwargs)
+ )
+ self.log.info("Kernel started: %s" % kernel_id)
+ self.log.debug("Kernel args: %r" % kwargs)
+ # register callback for failed auto-restart
+ self.add_restart_callback(kernel_id,
+ lambda : self._handle_kernel_died(kernel_id),
+ 'dead',
+ )
+ else:
+ self._check_kernel_id(kernel_id)
+ self.log.info("Using existing kernel: %s" % kernel_id)
+ # py2-compat
+ raise gen.Return(kernel_id)
+
+ def shutdown_kernel(self, kernel_id, now=False):
+ """Shutdown a kernel by kernel_id"""
+ self._check_kernel_id(kernel_id)
+ return super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
+
+ def restart_kernel(self, kernel_id):
+ """Restart a kernel by kernel_id"""
+ self._check_kernel_id(kernel_id)
+ super(MappingKernelManager, self).restart_kernel(kernel_id)
+ kernel = self.get_kernel(kernel_id)
+ # return a Future that will resolve when the kernel has successfully restarted
+ channel = kernel.connect_shell()
+ future = Future()
+
+ def finish():
+ """Common cleanup when restart finishes/fails for any reason."""
+ if not channel.closed():
+ channel.close()
+ loop.remove_timeout(timeout)
+ kernel.remove_restart_callback(on_restart_failed, 'dead')
+
+ def on_reply(msg):
+ self.log.debug("Kernel info reply received: %s", kernel_id)
+ finish()
+ if not future.done():
+ future.set_result(msg)
+
+ def on_timeout():
+ self.log.warn("Timeout waiting for kernel_info_reply: %s", kernel_id)
+ finish()
+ if not future.done():
+ future.set_exception(gen.TimeoutError("Timeout waiting for restart"))
+
+ def on_restart_failed():
+ self.log.warn("Restarting kernel failed: %s", kernel_id)
+ finish()
+ if not future.done():
+ future.set_exception(RuntimeError("Restart failed"))
+
+ kernel.add_restart_callback(on_restart_failed, 'dead')
+ kernel.session.send(channel, "kernel_info_request")
+ channel.on_recv(on_reply)
+ loop = IOLoop.current()
+ timeout = loop.add_timeout(loop.time() + 30, on_timeout)
+ return future
+
+ def kernel_model(self, kernel_id):
+ """Return a dictionary of kernel information described in the
+ JSON standard model."""
+ self._check_kernel_id(kernel_id)
+ model = {"id":kernel_id,
+ "name": self._kernels[kernel_id].kernel_name}
+ return model
+
+ def list_kernels(self):
+ """Returns a list of kernel_id's of kernels running."""
+ kernels = []
+ kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
+ for kernel_id in kernel_ids:
+ model = self.kernel_model(kernel_id)
+ kernels.append(model)
+ return kernels
+
+ # override _check_kernel_id to raise 404 instead of KeyError
+ def _check_kernel_id(self, kernel_id):
+ """Check a that a kernel_id exists and raise 404 if not."""
+ if kernel_id not in self:
+ raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
diff --git a/notebook/services/kernels/tests/__init__.py b/notebook/services/kernels/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/kernels/tests/__init__.py
diff --git a/notebook/services/kernels/tests/test_kernels_api.py b/notebook/services/kernels/tests/test_kernels_api.py
new file mode 100644
index 0000000..380228b
--- /dev/null
+++ b/notebook/services/kernels/tests/test_kernels_api.py
@@ -0,0 +1,146 @@
+"""Test the kernels service API."""
+
+import json
+import requests
+
+from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
+
+from notebook.utils import url_path_join
+from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
+
+class KernelAPI(object):
+ """Wrapper for kernel REST API requests"""
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def _req(self, verb, path, body=None):
+ response = requests.request(verb,
+ url_path_join(self.base_url, 'api/kernels', path), data=body)
+
+ if 400 <= response.status_code < 600:
+ try:
+ response.reason = response.json()['message']
+ except:
+ pass
+ response.raise_for_status()
+
+ return response
+
+ def list(self):
+ return self._req('GET', '')
+
+ def get(self, id):
+ return self._req('GET', id)
+
+ def start(self, name=NATIVE_KERNEL_NAME):
+ body = json.dumps({'name': name})
+ return self._req('POST', '', body)
+
+ def shutdown(self, id):
+ return self._req('DELETE', id)
+
+ def interrupt(self, id):
+ return self._req('POST', url_path_join(id, 'interrupt'))
+
+ def restart(self, id):
+ return self._req('POST', url_path_join(id, 'restart'))
+
+class KernelAPITest(NotebookTestBase):
+ """Test the kernels web service API"""
+ def setUp(self):
+ self.kern_api = KernelAPI(self.base_url())
+
+ def tearDown(self):
+ for k in self.kern_api.list().json():
+ self.kern_api.shutdown(k['id'])
+
+ def test_no_kernels(self):
+ """Make sure there are no kernels running at the start"""
+ kernels = self.kern_api.list().json()
+ self.assertEqual(kernels, [])
+
+ def test_default_kernel(self):
+ # POST request
+ r = self.kern_api._req('POST', '')
+ kern1 = r.json()
+ self.assertEqual(r.headers['location'], url_path_join(self.url_prefix, 'api/kernels', kern1['id']))
+ self.assertEqual(r.status_code, 201)
+ self.assertIsInstance(kern1, dict)
+
+ report_uri = url_path_join(self.url_prefix, 'api/security/csp-report')
+ expected_csp = '; '.join([
+ "frame-ancestors 'self'",
+ 'report-uri ' + report_uri,
+ "default-src 'none'"
+ ])
+ self.assertEqual(r.headers['Content-Security-Policy'], expected_csp)
+
+ def test_main_kernel_handler(self):
+ # POST request
+ r = self.kern_api.start()
+ kern1 = r.json()
+ self.assertEqual(r.headers['location'], url_path_join(self.url_prefix, 'api/kernels', kern1['id']))
+ self.assertEqual(r.status_code, 201)
+ self.assertIsInstance(kern1, dict)
+
+ report_uri = url_path_join(self.url_prefix, 'api/security/csp-report')
+ expected_csp = '; '.join([
+ "frame-ancestors 'self'",
+ 'report-uri ' + report_uri,
+ "default-src 'none'"
+ ])
+ self.assertEqual(r.headers['Content-Security-Policy'], expected_csp)
+
+ # GET request
+ r = self.kern_api.list()
+ self.assertEqual(r.status_code, 200)
+ assert isinstance(r.json(), list)
+ self.assertEqual(r.json()[0]['id'], kern1['id'])
+ self.assertEqual(r.json()[0]['name'], kern1['name'])
+
+ # create another kernel and check that they both are added to the
+ # list of kernels from a GET request
+ kern2 = self.kern_api.start().json()
+ assert isinstance(kern2, dict)
+ r = self.kern_api.list()
+ kernels = r.json()
+ self.assertEqual(r.status_code, 200)
+ assert isinstance(kernels, list)
+ self.assertEqual(len(kernels), 2)
+
+ # Interrupt a kernel
+ r = self.kern_api.interrupt(kern2['id'])
+ self.assertEqual(r.status_code, 204)
+
+ # Restart a kernel
+ r = self.kern_api.restart(kern2['id'])
+ rekern = r.json()
+ self.assertEqual(rekern['id'], kern2['id'])
+ self.assertEqual(rekern['name'], kern2['name'])
+
+ def test_kernel_handler(self):
+ # GET kernel with given id
+ kid = self.kern_api.start().json()['id']
+ r = self.kern_api.get(kid)
+ kern1 = r.json()
+ self.assertEqual(r.status_code, 200)
+ assert isinstance(kern1, dict)
+ self.assertIn('id', kern1)
+ self.assertEqual(kern1['id'], kid)
+
+ # Request a bad kernel id and check that a JSON
+ # message is returned!
+ bad_id = '111-111-111-111-111'
+ with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
+ self.kern_api.get(bad_id)
+
+ # DELETE kernel with id
+ r = self.kern_api.shutdown(kid)
+ self.assertEqual(r.status_code, 204)
+ kernels = self.kern_api.list().json()
+ self.assertEqual(kernels, [])
+
+ # Request to delete a non-existent kernel id
+ bad_id = '111-111-111-111-111'
+ with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
+ self.kern_api.shutdown(bad_id)
diff --git a/notebook/services/kernelspecs/__init__.py b/notebook/services/kernelspecs/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/kernelspecs/__init__.py
diff --git a/notebook/services/kernelspecs/handlers.py b/notebook/services/kernelspecs/handlers.py
new file mode 100644
index 0000000..279731f
--- /dev/null
+++ b/notebook/services/kernelspecs/handlers.py
@@ -0,0 +1,87 @@
+"""Tornado handlers for kernel specifications.
+
+Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-25%3A-Registry-of-installed-kernels#rest-api
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import glob
+import json
+import os
+pjoin = os.path.join
+
+from tornado import web
+
+from ...base.handlers import APIHandler, json_errors
+from ...utils import url_path_join
+
+def kernelspec_model(handler, name):
+ """Load a KernelSpec by name and return the REST API model"""
+ ksm = handler.kernel_spec_manager
+ spec = ksm.get_kernel_spec(name)
+ d = {'name': name}
+ d['spec'] = spec.to_dict()
+ d['resources'] = resources = {}
+ resource_dir = spec.resource_dir
+ for resource in ['kernel.js', 'kernel.css']:
+ if os.path.exists(pjoin(resource_dir, resource)):
+ resources[resource] = url_path_join(
+ handler.base_url,
+ 'kernelspecs',
+ name,
+ resource
+ )
+ for logo_file in glob.glob(pjoin(resource_dir, 'logo-*')):
+ fname = os.path.basename(logo_file)
+ no_ext, _ = os.path.splitext(fname)
+ resources[no_ext] = url_path_join(
+ handler.base_url,
+ 'kernelspecs',
+ name,
+ fname
+ )
+ return d
+
+class MainKernelSpecHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ def get(self):
+ ksm = self.kernel_spec_manager
+ km = self.kernel_manager
+ model = {}
+ model['default'] = km.default_kernel_name
+ model['kernelspecs'] = specs = {}
+ for kernel_name in ksm.find_kernel_specs():
+ try:
+ d = kernelspec_model(self, kernel_name)
+ except Exception:
+ self.log.error("Failed to load kernel spec: '%s'", kernel_name, exc_info=True)
+ continue
+ specs[kernel_name] = d
+ self.set_header("Content-Type", 'application/json')
+ self.finish(json.dumps(model))
+
+
+class KernelSpecHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ def get(self, kernel_name):
+ try:
+ model = kernelspec_model(self, kernel_name)
+ except KeyError:
+ raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name)
+ self.set_header("Content-Type", 'application/json')
+ self.finish(json.dumps(model))
+
+
+# URL to handler mappings
+
+kernel_name_regex = r"(?P<kernel_name>[\w\.\-]+)"
+
+default_handlers = [
+ (r"/api/kernelspecs", MainKernelSpecHandler),
+ (r"/api/kernelspecs/%s" % kernel_name_regex, KernelSpecHandler),
+]
diff --git a/notebook/services/kernelspecs/tests/__init__.py b/notebook/services/kernelspecs/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/kernelspecs/tests/__init__.py
diff --git a/notebook/services/kernelspecs/tests/test_kernelspecs_api.py b/notebook/services/kernelspecs/tests/test_kernelspecs_api.py
new file mode 100644
index 0000000..7a93fdf
--- /dev/null
+++ b/notebook/services/kernelspecs/tests/test_kernelspecs_api.py
@@ -0,0 +1,129 @@
+# coding: utf-8
+"""Test the kernel specs webservice API."""
+
+import errno
+import io
+import json
+import os
+import shutil
+
+pjoin = os.path.join
+
+import requests
+
+from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
+from notebook.utils import url_path_join
+from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
+
+# Copied from jupyter_client.tests.test_kernelspec so updating that doesn't
+# break these tests
+sample_kernel_json = {'argv':['cat', '{connection_file}'],
+ 'display_name':'Test kernel',
+ }
+
+some_resource = u"The very model of a modern major general"
+
+
+class KernelSpecAPI(object):
+ """Wrapper for notebook API calls."""
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def _req(self, verb, path, body=None):
+ response = requests.request(verb,
+ url_path_join(self.base_url, path),
+ data=body,
+ )
+ response.raise_for_status()
+ return response
+
+ def list(self):
+ return self._req('GET', 'api/kernelspecs')
+
+ def kernel_spec_info(self, name):
+ return self._req('GET', url_path_join('api/kernelspecs', name))
+
+ def kernel_resource(self, name, path):
+ return self._req('GET', url_path_join('kernelspecs', name, path))
+
+class APITest(NotebookTestBase):
+ """Test the kernelspec web service API"""
+ def setUp(self):
+ sample_kernel_dir = pjoin(self.data_dir.name, 'kernels', 'sample')
+ try:
+ os.makedirs(sample_kernel_dir)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise
+
+ with open(pjoin(sample_kernel_dir, 'kernel.json'), 'w') as f:
+ json.dump(sample_kernel_json, f)
+
+ with io.open(pjoin(sample_kernel_dir, 'resource.txt'), 'w',
+ encoding='utf-8') as f:
+ f.write(some_resource)
+
+ self.ks_api = KernelSpecAPI(self.base_url())
+
+ def test_list_kernelspecs_bad(self):
+ """Can list kernelspecs when one is invalid"""
+ bad_kernel_dir = pjoin(self.data_dir.name, 'kernels', 'bad')
+ try:
+ os.makedirs(bad_kernel_dir)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise
+
+ with open(pjoin(bad_kernel_dir, 'kernel.json'), 'w') as f:
+ f.write("garbage")
+
+ model = self.ks_api.list().json()
+ assert isinstance(model, dict)
+ self.assertEqual(model['default'], NATIVE_KERNEL_NAME)
+ specs = model['kernelspecs']
+ assert isinstance(specs, dict)
+ # 2: the sample kernelspec created in setUp, and the native Python kernel
+ self.assertGreaterEqual(len(specs), 2)
+
+ shutil.rmtree(bad_kernel_dir)
+
+ def test_list_kernelspecs(self):
+ model = self.ks_api.list().json()
+ assert isinstance(model, dict)
+ self.assertEqual(model['default'], NATIVE_KERNEL_NAME)
+ specs = model['kernelspecs']
+ assert isinstance(specs, dict)
+
+ # 2: the sample kernelspec created in setUp, and the native Python kernel
+ self.assertGreaterEqual(len(specs), 2)
+
+ def is_sample_kernelspec(s):
+ return s['name'] == 'sample' and s['spec']['display_name'] == 'Test kernel'
+
+ def is_default_kernelspec(s):
+ return s['name'] == NATIVE_KERNEL_NAME and s['spec']['display_name'].startswith("Python")
+
+ assert any(is_sample_kernelspec(s) for s in specs.values()), specs
+ assert any(is_default_kernelspec(s) for s in specs.values()), specs
+
+ def test_get_kernelspec(self):
+ model = self.ks_api.kernel_spec_info('Sample').json() # Case insensitive
+ self.assertEqual(model['name'].lower(), 'sample')
+ self.assertIsInstance(model['spec'], dict)
+ self.assertEqual(model['spec']['display_name'], 'Test kernel')
+ self.assertIsInstance(model['resources'], dict)
+
+ def test_get_nonexistant_kernelspec(self):
+ with assert_http_error(404):
+ self.ks_api.kernel_spec_info('nonexistant')
+
+ def test_get_kernel_resource_file(self):
+ res = self.ks_api.kernel_resource('sAmple', 'resource.txt')
+ self.assertEqual(res.text, some_resource)
+
+ def test_get_nonexistant_resource(self):
+ with assert_http_error(404):
+ self.ks_api.kernel_resource('nonexistant', 'resource.txt')
+
+ with assert_http_error(404):
+ self.ks_api.kernel_resource('sample', 'nonexistant.txt')
diff --git a/notebook/services/nbconvert/__init__.py b/notebook/services/nbconvert/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/nbconvert/__init__.py
diff --git a/notebook/services/nbconvert/handlers.py b/notebook/services/nbconvert/handlers.py
new file mode 100644
index 0000000..c8da3cf
--- /dev/null
+++ b/notebook/services/nbconvert/handlers.py
@@ -0,0 +1,25 @@
+import json
+
+from tornado import web
+
+from ...base.handlers import APIHandler, json_errors
+
+class NbconvertRootHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ def get(self):
+ try:
+ from nbconvert.exporters.export import exporter_map
+ except ImportError as e:
+ raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
+ res = {}
+ for format, exporter in exporter_map.items():
+ res[format] = info = {}
+ info['output_mimetype'] = exporter.output_mimetype
+
+ self.finish(json.dumps(res))
+
+default_handlers = [
+ (r"/api/nbconvert", NbconvertRootHandler),
+]
diff --git a/notebook/services/nbconvert/tests/__init__.py b/notebook/services/nbconvert/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/nbconvert/tests/__init__.py
diff --git a/notebook/services/nbconvert/tests/test_nbconvert_api.py b/notebook/services/nbconvert/tests/test_nbconvert_api.py
new file mode 100644
index 0000000..d33c537
--- /dev/null
+++ b/notebook/services/nbconvert/tests/test_nbconvert_api.py
@@ -0,0 +1,31 @@
+import requests
+
+from notebook.utils import url_path_join
+from notebook.tests.launchnotebook import NotebookTestBase
+
+class NbconvertAPI(object):
+ """Wrapper for nbconvert API calls."""
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def _req(self, verb, path, body=None, params=None):
+ response = requests.request(verb,
+ url_path_join(self.base_url, 'api/nbconvert', path),
+ data=body, params=params,
+ )
+ response.raise_for_status()
+ return response
+
+ def list_formats(self):
+ return self._req('GET', '')
+
+class APITest(NotebookTestBase):
+ def setUp(self):
+ self.nbconvert_api = NbconvertAPI(self.base_url())
+
+ def test_list_formats(self):
+ formats = self.nbconvert_api.list_formats().json()
+ self.assertIsInstance(formats, dict)
+ self.assertIn('python', formats)
+ self.assertIn('html', formats)
+ self.assertEqual(formats['python']['output_mimetype'], 'text/x-python') \ No newline at end of file
diff --git a/notebook/services/security/__init__.py b/notebook/services/security/__init__.py
new file mode 100644
index 0000000..9cf0d47
--- /dev/null
+++ b/notebook/services/security/__init__.py
@@ -0,0 +1,4 @@
+# URI for the CSP Report. Included here to prevent a cyclic dependency.
+# csp_report_uri is needed both by the BaseHandler (for setting the report-uri)
+# and by the CSPReportHandler (which depends on the BaseHandler).
+csp_report_uri = r"/api/security/csp-report"
diff --git a/notebook/services/security/handlers.py b/notebook/services/security/handlers.py
new file mode 100644
index 0000000..c34ddf9
--- /dev/null
+++ b/notebook/services/security/handlers.py
@@ -0,0 +1,23 @@
+"""Tornado handlers for security logging."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from tornado import gen, web
+
+from ...base.handlers import APIHandler, json_errors
+from . import csp_report_uri
+
+class CSPReportHandler(APIHandler):
+ '''Accepts a content security policy violation report'''
+ @web.authenticated
+ @json_errors
+ def post(self):
+ '''Log a content security policy violation report'''
+ csp_report = self.get_json_body()
+ self.log.warn("Content security violation: %s",
+ self.request.body.decode('utf8', 'replace'))
+
+default_handlers = [
+ (csp_report_uri, CSPReportHandler)
+]
diff --git a/notebook/services/sessions/__init__.py b/notebook/services/sessions/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/sessions/__init__.py
diff --git a/notebook/services/sessions/handlers.py b/notebook/services/sessions/handlers.py
new file mode 100644
index 0000000..a382241
--- /dev/null
+++ b/notebook/services/sessions/handlers.py
@@ -0,0 +1,161 @@
+"""Tornado handlers for the sessions web service.
+
+Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#sessions-api
+"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import json
+
+from tornado import gen, web
+
+from ...base.handlers import APIHandler, json_errors
+from jupyter_client.jsonutil import date_default
+from notebook.utils import url_path_join
+from jupyter_client.kernelspec import NoSuchKernel
+
+
+class SessionRootHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def get(self):
+ # Return a list of running sessions
+ sm = self.session_manager
+ sessions = yield gen.maybe_future(sm.list_sessions())
+ self.finish(json.dumps(sessions, default=date_default))
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def post(self):
+ # Creates a new session
+ #(unless a session already exists for the named nb)
+ sm = self.session_manager
+
+ model = self.get_json_body()
+ if model is None:
+ raise web.HTTPError(400, "No JSON data provided")
+ try:
+ path = model['notebook']['path']
+ except KeyError:
+ raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
+
+ kernel = model.get('kernel', {})
+ kernel_name = kernel.get('name') or None
+ kernel_id = kernel.get('id') or None
+
+ if not kernel_id and not kernel_name:
+ self.log.debug("No kernel specified, using default kernel")
+ kernel_name = None
+
+ # Check to see if session exists
+ exists = yield gen.maybe_future(sm.session_exists(path=path))
+ if exists:
+ model = yield gen.maybe_future(sm.get_session(path=path))
+ else:
+ try:
+ model = yield gen.maybe_future(
+ sm.create_session(path=path, kernel_name=kernel_name,
+ kernel_id=kernel_id))
+ except NoSuchKernel:
+ msg = ("The '%s' kernel is not available. Please pick another "
+ "suitable kernel instead, or install that kernel." % kernel_name)
+ status_msg = '%s not found' % kernel_name
+ self.log.warn('Kernel not found: %s' % kernel_name)
+ self.set_status(501)
+ self.finish(json.dumps(dict(message=msg, short_message=status_msg)))
+ return
+
+ location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
+ self.set_header('Location', location)
+ self.set_status(201)
+ self.finish(json.dumps(model, default=date_default))
+
+
+class SessionHandler(APIHandler):
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def get(self, session_id):
+ # Returns the JSON model for a single session
+ sm = self.session_manager
+ model = yield gen.maybe_future(sm.get_session(session_id=session_id))
+ self.finish(json.dumps(model, default=date_default))
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def patch(self, session_id):
+ """Patch updates sessions:
+
+ - notebook.path updates session to track renamed notebooks
+ - kernel.name starts a new kernel with a given kernelspec
+ """
+ sm = self.session_manager
+ km = self.kernel_manager
+ model = self.get_json_body()
+ if model is None:
+ raise web.HTTPError(400, "No JSON data provided")
+
+ # get the previous session model
+ before = yield gen.maybe_future(sm.get_session(session_id=session_id))
+
+ changes = {}
+ if 'notebook' in model:
+ notebook = model['notebook']
+ if notebook.get('path') is not None:
+ changes['path'] = notebook['path']
+ if 'kernel' in model:
+ # Kernel id takes precedence over name.
+ if model['kernel'].get('id') is not None:
+ kernel_id = model['kernel']['id']
+ if kernel_id not in km:
+ raise web.HTTPError(400, "No such kernel: %s" % kernel_id)
+ changes['kernel_id'] = kernel_id
+ elif model['kernel'].get('name') is not None:
+ kernel_name = model['kernel']['name']
+ kernel_id = yield sm.start_kernel_for_session(
+ session_id, kernel_name=kernel_name, path=before['notebook']['path'])
+ changes['kernel_id'] = kernel_id
+
+ yield gen.maybe_future(sm.update_session(session_id, **changes))
+ model = yield gen.maybe_future(sm.get_session(session_id=session_id))
+
+ if model['kernel']['id'] != before['kernel']['id']:
+ # kernel_id changed because we got a new kernel
+ # shutdown the old one
+ yield gen.maybe_future(
+ km.shutdown_kernel(before['kernel']['id'])
+ )
+ self.finish(json.dumps(model, default=date_default))
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def delete(self, session_id):
+ # Deletes the session with given session_id
+ sm = self.session_manager
+ try:
+ yield gen.maybe_future(sm.delete_session(session_id))
+ except KeyError:
+ # the kernel was deleted but the session wasn't!
+ raise web.HTTPError(410, "Kernel deleted before session")
+ self.set_status(204)
+ self.finish()
+
+
+#-----------------------------------------------------------------------------
+# URL to handler mappings
+#-----------------------------------------------------------------------------
+
+_session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
+
+default_handlers = [
+ (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
+ (r"/api/sessions", SessionRootHandler)
+]
+
diff --git a/notebook/services/sessions/sessionmanager.py b/notebook/services/sessions/sessionmanager.py
new file mode 100644
index 0000000..f0554bf
--- /dev/null
+++ b/notebook/services/sessions/sessionmanager.py
@@ -0,0 +1,235 @@
+"""A base class session manager."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import uuid
+
+try:
+ import sqlite3
+except ImportError:
+ # fallback on pysqlite2 if Python was build without sqlite
+ from pysqlite2 import dbapi2 as sqlite3
+
+from tornado import gen, web
+
+from traitlets.config.configurable import LoggingConfigurable
+from ipython_genutils.py3compat import unicode_type
+from traitlets import Instance
+
+
+class SessionManager(LoggingConfigurable):
+
+ kernel_manager = Instance('notebook.services.kernels.kernelmanager.MappingKernelManager')
+ contents_manager = Instance('notebook.services.contents.manager.ContentsManager')
+
+ # Session database initialized below
+ _cursor = None
+ _connection = None
+ _columns = {'session_id', 'path', 'kernel_id'}
+
+ @property
+ def cursor(self):
+ """Start a cursor and create a database called 'session'"""
+ if self._cursor is None:
+ self._cursor = self.connection.cursor()
+ self._cursor.execute("""CREATE TABLE session
+ (session_id, path, kernel_id)""")
+ return self._cursor
+
+ @property
+ def connection(self):
+ """Start a database connection"""
+ if self._connection is None:
+ self._connection = sqlite3.connect(':memory:')
+ self._connection.row_factory = sqlite3.Row
+ return self._connection
+
+ def close(self):
+ """Close the sqlite connection"""
+ if self._cursor is not None:
+ self._cursor.close()
+ self._cursor = None
+
+ def __del__(self):
+ """Close connection once SessionManager closes"""
+ self.close()
+
+ def session_exists(self, path):
+ """Check to see if the session for a given notebook exists"""
+ self.cursor.execute("SELECT * FROM session WHERE path=?", (path,))
+ reply = self.cursor.fetchone()
+ if reply is None:
+ return False
+ else:
+ return True
+
+ def new_session_id(self):
+ "Create a uuid for a new session"
+ return unicode_type(uuid.uuid4())
+
+ @gen.coroutine
+ def create_session(self, path=None, kernel_name=None, kernel_id=None):
+ """Creates a session and returns its model"""
+ session_id = self.new_session_id()
+ if kernel_id is not None and kernel_id in self.kernel_manager:
+ pass
+ else:
+ kernel_id = yield self.start_kernel_for_session(session_id, path,
+ kernel_name)
+ result = yield gen.maybe_future(
+ self.save_session(session_id, path=path, kernel_id=kernel_id)
+ )
+ # py2-compat
+ raise gen.Return(result)
+
+ @gen.coroutine
+ def start_kernel_for_session(self, session_id, path, kernel_name):
+ """Start a new kernel for a given session."""
+ # allow contents manager to specify kernels cwd
+ kernel_path = self.contents_manager.get_kernel_path(path=path)
+ kernel_id = yield gen.maybe_future(
+ self.kernel_manager.start_kernel(path=kernel_path, kernel_name=kernel_name)
+ )
+ # py2-compat
+ raise gen.Return(kernel_id)
+
+ def save_session(self, session_id, path=None, kernel_id=None):
+ """Saves the items for the session with the given session_id
+
+ Given a session_id (and any other of the arguments), this method
+ creates a row in the sqlite session database that holds the information
+ for a session.
+
+ Parameters
+ ----------
+ session_id : str
+ uuid for the session; this method must be given a session_id
+ path : str
+ the path for the given notebook
+ kernel_id : str
+ a uuid for the kernel associated with this session
+
+ Returns
+ -------
+ model : dict
+ a dictionary of the session model
+ """
+ self.cursor.execute("INSERT INTO session VALUES (?,?,?)",
+ (session_id, path, kernel_id)
+ )
+ return self.get_session(session_id=session_id)
+
+ def get_session(self, **kwargs):
+ """Returns the model for a particular session.
+
+ Takes a keyword argument and searches for the value in the session
+ database, then returns the rest of the session's info.
+
+ Parameters
+ ----------
+ **kwargs : keyword argument
+ must be given one of the keywords and values from the session database
+ (i.e. session_id, path, kernel_id)
+
+ Returns
+ -------
+ model : dict
+ returns a dictionary that includes all the information from the
+ session described by the kwarg.
+ """
+ if not kwargs:
+ raise TypeError("must specify a column to query")
+
+ conditions = []
+ for column in kwargs.keys():
+ if column not in self._columns:
+ raise TypeError("No such column: %r", column)
+ conditions.append("%s=?" % column)
+
+ query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions))
+
+ self.cursor.execute(query, list(kwargs.values()))
+ try:
+ row = self.cursor.fetchone()
+ except KeyError:
+ # The kernel is missing, so the session just got deleted.
+ row = None
+
+ if row is None:
+ q = []
+ for key, value in kwargs.items():
+ q.append("%s=%r" % (key, value))
+
+ raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q)))
+
+ return self.row_to_model(row)
+
+ def update_session(self, session_id, **kwargs):
+ """Updates the values in the session database.
+
+ Changes the values of the session with the given session_id
+ with the values from the keyword arguments.
+
+ Parameters
+ ----------
+ session_id : str
+ a uuid that identifies a session in the sqlite3 database
+ **kwargs : str
+ the key must correspond to a column title in session database,
+ and the value replaces the current value in the session
+ with session_id.
+ """
+ self.get_session(session_id=session_id)
+
+ if not kwargs:
+ # no changes
+ return
+
+ sets = []
+ for column in kwargs.keys():
+ if column not in self._columns:
+ raise TypeError("No such column: %r" % column)
+ sets.append("%s=?" % column)
+ query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
+ self.cursor.execute(query, list(kwargs.values()) + [session_id])
+
+ def row_to_model(self, row):
+ """Takes sqlite database session row and turns it into a dictionary"""
+ if row['kernel_id'] not in self.kernel_manager:
+ # The kernel was killed or died without deleting the session.
+ # We can't use delete_session here because that tries to find
+ # and shut down the kernel.
+ self.cursor.execute("DELETE FROM session WHERE session_id=?",
+ (row['session_id'],))
+ raise KeyError
+
+ model = {
+ 'id': row['session_id'],
+ 'notebook': {
+ 'path': row['path']
+ },
+ 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
+ }
+ return model
+
+ def list_sessions(self):
+ """Returns a list of dictionaries containing all the information from
+ the session database"""
+ c = self.cursor.execute("SELECT * FROM session")
+ result = []
+ # We need to use fetchall() here, because row_to_model can delete rows,
+ # which messes up the cursor if we're iterating over rows.
+ for row in c.fetchall():
+ try:
+ result.append(self.row_to_model(row))
+ except KeyError:
+ pass
+ return result
+
+ @gen.coroutine
+ def delete_session(self, session_id):
+ """Deletes the row in the session database with given session_id"""
+ session = self.get_session(session_id=session_id)
+ yield gen.maybe_future(self.kernel_manager.shutdown_kernel(session['kernel']['id']))
+ self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))
diff --git a/notebook/services/sessions/tests/__init__.py b/notebook/services/sessions/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/services/sessions/tests/__init__.py
diff --git a/notebook/services/sessions/tests/test_sessionmanager.py b/notebook/services/sessions/tests/test_sessionmanager.py
new file mode 100644
index 0000000..14bda02
--- /dev/null
+++ b/notebook/services/sessions/tests/test_sessionmanager.py
@@ -0,0 +1,182 @@
+"""Tests for the session manager."""
+
+from unittest import TestCase
+
+from tornado import gen, web
+from tornado.ioloop import IOLoop
+
+from ..sessionmanager import SessionManager
+from notebook.services.kernels.kernelmanager import MappingKernelManager
+from notebook.services.contents.manager import ContentsManager
+
+class DummyKernel(object):
+ def __init__(self, kernel_name='python'):
+ self.kernel_name = kernel_name
+
+class DummyMKM(MappingKernelManager):
+ """MappingKernelManager interface that doesn't start kernels, for testing"""
+ def __init__(self, *args, **kwargs):
+ super(DummyMKM, self).__init__(*args, **kwargs)
+ self.id_letters = iter(u'ABCDEFGHIJK')
+
+ def _new_id(self):
+ return next(self.id_letters)
+
+ def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
+ kernel_id = kernel_id or self._new_id()
+ self._kernels[kernel_id] = DummyKernel(kernel_name=kernel_name)
+ return kernel_id
+
+ def shutdown_kernel(self, kernel_id, now=False):
+ del self._kernels[kernel_id]
+
+
+class TestSessionManager(TestCase):
+
+ def setUp(self):
+ self.sm = SessionManager(
+ kernel_manager=DummyMKM(),
+ contents_manager=ContentsManager(),
+ )
+ self.loop = IOLoop()
+
+ def tearDown(self):
+ self.loop.close(all_fds=True)
+
+ def create_sessions(self, *kwarg_list):
+ @gen.coroutine
+ def co_add():
+ sessions = []
+ for kwargs in kwarg_list:
+ session = yield self.sm.create_session(**kwargs)
+ sessions.append(session)
+ raise gen.Return(sessions)
+ return self.loop.run_sync(co_add)
+
+ def create_session(self, **kwargs):
+ return self.create_sessions(kwargs)[0]
+
+ def test_get_session(self):
+ sm = self.sm
+ session_id = self.create_session(path='/path/to/test.ipynb', kernel_name='bar')['id']
+ model = sm.get_session(session_id=session_id)
+ expected = {'id':session_id,
+ 'notebook':{'path': u'/path/to/test.ipynb'},
+ 'kernel': {'id':u'A', 'name': 'bar'}}
+ self.assertEqual(model, expected)
+
+ def test_bad_get_session(self):
+ # Should raise error if a bad key is passed to the database.
+ sm = self.sm
+ session_id = self.create_session(path='/path/to/test.ipynb',
+ kernel_name='foo')['id']
+ self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
+
+ def test_get_session_dead_kernel(self):
+ sm = self.sm
+ session = self.create_session(path='/path/to/1/test1.ipynb', kernel_name='python')
+ # kill the kernel
+ sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
+ with self.assertRaises(KeyError):
+ sm.get_session(session_id=session['id'])
+ # no sessions left
+ listed = sm.list_sessions()
+ self.assertEqual(listed, [])
+
+ def test_list_sessions(self):
+ sm = self.sm
+ sessions = self.create_sessions(
+ dict(path='/path/to/1/test1.ipynb', kernel_name='python'),
+ dict(path='/path/to/2/test2.ipynb', kernel_name='python'),
+ dict(path='/path/to/3/test3.ipynb', kernel_name='python'),
+ )
+
+ sessions = sm.list_sessions()
+ expected = [
+ {
+ 'id':sessions[0]['id'],
+ 'notebook':{'path': u'/path/to/1/test1.ipynb'},
+ 'kernel':{'id':u'A', 'name':'python'}
+ }, {
+ 'id':sessions[1]['id'],
+ 'notebook': {'path': u'/path/to/2/test2.ipynb'},
+ 'kernel':{'id':u'B', 'name':'python'}
+ }, {
+ 'id':sessions[2]['id'],
+ 'notebook':{'path': u'/path/to/3/test3.ipynb'},
+ 'kernel':{'id':u'C', 'name':'python'}
+ }
+ ]
+ self.assertEqual(sessions, expected)
+
+ def test_list_sessions_dead_kernel(self):
+ sm = self.sm
+ sessions = self.create_sessions(
+ dict(path='/path/to/1/test1.ipynb', kernel_name='python'),
+ dict(path='/path/to/2/test2.ipynb', kernel_name='python'),
+ )
+ # kill one of the kernels
+ sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
+ listed = sm.list_sessions()
+ expected = [
+ {
+ 'id': sessions[1]['id'],
+ 'notebook': {
+ 'path': u'/path/to/2/test2.ipynb',
+ },
+ 'kernel': {
+ 'id': u'B',
+ 'name':'python',
+ }
+ }
+ ]
+ self.assertEqual(listed, expected)
+
+ def test_update_session(self):
+ sm = self.sm
+ session_id = self.create_session(path='/path/to/test.ipynb',
+ kernel_name='julia')['id']
+ sm.update_session(session_id, path='/path/to/new_name.ipynb')
+ model = sm.get_session(session_id=session_id)
+ expected = {'id':session_id,
+ 'notebook':{'path': u'/path/to/new_name.ipynb'},
+ 'kernel':{'id':u'A', 'name':'julia'}}
+ self.assertEqual(model, expected)
+
+ def test_bad_update_session(self):
+ # try to update a session with a bad keyword ~ raise error
+ sm = self.sm
+ session_id = self.create_session(path='/path/to/test.ipynb',
+ kernel_name='ir')['id']
+ self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
+
+ def test_delete_session(self):
+ sm = self.sm
+ sessions = self.create_sessions(
+ dict(path='/path/to/1/test1.ipynb', kernel_name='python'),
+ dict(path='/path/to/2/test2.ipynb', kernel_name='python'),
+ dict(path='/path/to/3/test3.ipynb', kernel_name='python'),
+ )
+ sm.delete_session(sessions[1]['id'])
+ new_sessions = sm.list_sessions()
+ expected = [{
+ 'id': sessions[0]['id'],
+ 'notebook': {'path': u'/path/to/1/test1.ipynb'},
+ 'kernel': {'id':u'A', 'name':'python'}
+ }, {
+ 'id': sessions[2]['id'],
+ 'notebook': {'path': u'/path/to/3/test3.ipynb'},
+ 'kernel': {'id':u'C', 'name':'python'}
+ }
+ ]
+ self.assertEqual(new_sessions, expected)
+
+ def test_bad_delete_session(self):
+ # try to delete a session that doesn't exist ~ raise error
+ sm = self.sm
+ self.create_session(path='/path/to/test.ipynb', kernel_name='python')
+ with self.assertRaises(TypeError):
+ self.loop.run_sync(lambda : sm.delete_session(bad_kwarg='23424')) # Bad keyword
+ with self.assertRaises(web.HTTPError):
+ self.loop.run_sync(lambda : sm.delete_session(session_id='23424')) # nonexistent
+
diff --git a/notebook/services/sessions/tests/test_sessions_api.py b/notebook/services/sessions/tests/test_sessions_api.py
new file mode 100644
index 0000000..08cb381
--- /dev/null
+++ b/notebook/services/sessions/tests/test_sessions_api.py
@@ -0,0 +1,193 @@
+"""Test the sessions web service API."""
+
+import errno
+import io
+import os
+import json
+import requests
+import shutil
+import time
+
+pjoin = os.path.join
+
+from notebook.utils import url_path_join
+from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
+from nbformat.v4 import new_notebook
+from nbformat import write
+
+class SessionAPI(object):
+ """Wrapper for notebook API calls."""
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def _req(self, verb, path, body=None):
+ response = requests.request(verb,
+ url_path_join(self.base_url, 'api/sessions', path), data=body)
+
+ if 400 <= response.status_code < 600:
+ try:
+ response.reason = response.json()['message']
+ except:
+ pass
+ response.raise_for_status()
+
+ return response
+
+ def list(self):
+ return self._req('GET', '')
+
+ def get(self, id):
+ return self._req('GET', id)
+
+ def create(self, path, kernel_name='python', kernel_id=None):
+ body = json.dumps({'notebook': {'path':path},
+ 'kernel': {'name': kernel_name,
+ 'id': kernel_id}})
+ return self._req('POST', '', body)
+
+ def modify_path(self, id, path):
+ body = json.dumps({'notebook': {'path':path}})
+ return self._req('PATCH', id, body)
+
+ def modify_kernel_name(self, id, kernel_name):
+ body = json.dumps({'kernel': {'name': kernel_name}})
+ return self._req('PATCH', id, body)
+
+ def modify_kernel_id(self, id, kernel_id):
+ # Also send a dummy name to show that id takes precedence.
+ body = json.dumps({'kernel': {'id': kernel_id, 'name': 'foo'}})
+ return self._req('PATCH', id, body)
+
+ def delete(self, id):
+ return self._req('DELETE', id)
+
+class SessionAPITest(NotebookTestBase):
+ """Test the sessions web service API"""
+ def setUp(self):
+ nbdir = self.notebook_dir.name
+ try:
+ os.mkdir(pjoin(nbdir, 'foo'))
+ except OSError as e:
+ # Deleting the folder in an earlier test may have failed
+ if e.errno != errno.EEXIST:
+ raise
+
+ with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
+ encoding='utf-8') as f:
+ nb = new_notebook()
+ write(nb, f, version=4)
+
+ self.sess_api = SessionAPI(self.base_url())
+
+ def tearDown(self):
+ for session in self.sess_api.list().json():
+ self.sess_api.delete(session['id'])
+ # This is necessary in some situations on Windows: without it, it
+ # fails to delete the directory because something is still using it. I
+ # think there is a brief period after the kernel terminates where
+ # Windows still treats its working directory as in use. On my Windows
+ # VM, 0.01s is not long enough, but 0.1s appears to work reliably.
+ # -- TK, 15 December 2014
+ time.sleep(0.1)
+
+ shutil.rmtree(pjoin(self.notebook_dir.name, 'foo'),
+ ignore_errors=True)
+
+ def test_create(self):
+ sessions = self.sess_api.list().json()
+ self.assertEqual(len(sessions), 0)
+
+ resp = self.sess_api.create('foo/nb1.ipynb')
+ self.assertEqual(resp.status_code, 201)
+ newsession = resp.json()
+ self.assertIn('id', newsession)
+ self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb')
+ self.assertEqual(resp.headers['Location'], self.url_prefix + 'api/sessions/{0}'.format(newsession['id']))
+
+ sessions = self.sess_api.list().json()
+ self.assertEqual(sessions, [newsession])
+
+ # Retrieve it
+ sid = newsession['id']
+ got = self.sess_api.get(sid).json()
+ self.assertEqual(got, newsession)
+
+ def test_create_with_kernel_id(self):
+ # create a new kernel
+ r = requests.post(url_path_join(self.base_url(), 'api/kernels'))
+ r.raise_for_status()
+ kernel = r.json()
+
+ resp = self.sess_api.create('foo/nb1.ipynb', kernel_id=kernel['id'])
+ self.assertEqual(resp.status_code, 201)
+ newsession = resp.json()
+ self.assertIn('id', newsession)
+ self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb')
+ self.assertEqual(newsession['kernel']['id'], kernel['id'])
+ self.assertEqual(resp.headers['Location'], self.url_prefix + 'api/sessions/{0}'.format(newsession['id']))
+
+ sessions = self.sess_api.list().json()
+ self.assertEqual(sessions, [newsession])
+
+ # Retrieve it
+ sid = newsession['id']
+ got = self.sess_api.get(sid).json()
+ self.assertEqual(got, newsession)
+
+ def test_delete(self):
+ newsession = self.sess_api.create('foo/nb1.ipynb').json()
+ sid = newsession['id']
+
+ resp = self.sess_api.delete(sid)
+ self.assertEqual(resp.status_code, 204)
+
+ sessions = self.sess_api.list().json()
+ self.assertEqual(sessions, [])
+
+ with assert_http_error(404):
+ self.sess_api.get(sid)
+
+ def test_modify_path(self):
+ newsession = self.sess_api.create('foo/nb1.ipynb').json()
+ sid = newsession['id']
+
+ changed = self.sess_api.modify_path(sid, 'nb2.ipynb').json()
+ self.assertEqual(changed['id'], sid)
+ self.assertEqual(changed['notebook']['path'], 'nb2.ipynb')
+
+ def test_modify_kernel_name(self):
+ before = self.sess_api.create('foo/nb1.ipynb').json()
+ sid = before['id']
+
+ after = self.sess_api.modify_kernel_name(sid, before['kernel']['name']).json()
+ self.assertEqual(after['id'], sid)
+ self.assertEqual(after['notebook'], before['notebook'])
+ self.assertNotEqual(after['kernel']['id'], before['kernel']['id'])
+
+ # check kernel list, to be sure previous kernel was cleaned up
+ r = requests.get(url_path_join(self.base_url(), 'api/kernels'))
+ r.raise_for_status()
+ kernel_list = r.json()
+ self.assertEqual(kernel_list, [after['kernel']])
+
+ def test_modify_kernel_id(self):
+ before = self.sess_api.create('foo/nb1.ipynb').json()
+ sid = before['id']
+
+ # create a new kernel
+ r = requests.post(url_path_join(self.base_url(), 'api/kernels'))
+ r.raise_for_status()
+ kernel = r.json()
+
+ # Attach our session to the existing kernel
+ after = self.sess_api.modify_kernel_id(sid, kernel['id']).json()
+ self.assertEqual(after['id'], sid)
+ self.assertEqual(after['notebook'], before['notebook'])
+ self.assertNotEqual(after['kernel']['id'], before['kernel']['id'])
+ self.assertEqual(after['kernel']['id'], kernel['id'])
+
+ # check kernel list, to be sure previous kernel was cleaned up
+ r = requests.get(url_path_join(self.base_url(), 'api/kernels'))
+ r.raise_for_status()
+ kernel_list = r.json()
+ self.assertEqual(kernel_list, [kernel])
diff --git a/notebook/static/auth/css/override.css b/notebook/static/auth/css/override.css
new file mode 100644
index 0000000..cda1549
--- /dev/null
+++ b/notebook/static/auth/css/override.css
@@ -0,0 +1,8 @@
+/*This file contains any manual css for this page that needs to override the global styles.
+This is only required when different pages style the same element differently. This is just
+a hack to deal with our current css styles and no new styling should be added in this file.*/
+
+#ipython-main-app {
+ padding-top: 50px;
+ text-align: center;
+} \ No newline at end of file
diff --git a/notebook/static/auth/js/loginmain.js b/notebook/static/auth/js/loginmain.js
new file mode 100644
index 0000000..1e312ee
--- /dev/null
+++ b/notebook/static/auth/js/loginmain.js
@@ -0,0 +1,14 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define(['base/js/namespace', 'base/js/page'], function(IPython, page) {
+ function login_main() {
+ var page_instance = new page.Page();
+ $('button#login_submit').addClass("btn btn-default");
+ page_instance.show();
+ $('input#password_input').focus();
+
+ IPython.page = page_instance;
+ }
+ return login_main;
+});
diff --git a/notebook/static/auth/js/loginwidget.js b/notebook/static/auth/js/loginwidget.js
new file mode 100644
index 0000000..1ae4dba
--- /dev/null
+++ b/notebook/static/auth/js/loginwidget.js
@@ -0,0 +1,38 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'base/js/utils',
+ 'jquery',
+], function(utils, $){
+ "use strict";
+
+ var LoginWidget = function (selector, options) {
+ options = options || {};
+ this.base_url = options.base_url || utils.get_body_data("baseUrl");
+ this.selector = selector;
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.bind_events();
+ }
+ };
+
+
+ LoginWidget.prototype.bind_events = function () {
+ var that = this;
+ this.element.find("button#logout").click(function () {
+ window.location = utils.url_path_join(
+ that.base_url,
+ "logout"
+ );
+ });
+ this.element.find("button#login").click(function () {
+ window.location = utils.url_path_join(
+ that.base_url,
+ "login"
+ );
+ });
+ };
+
+ return {'LoginWidget': LoginWidget};
+});
diff --git a/notebook/static/auth/js/logoutmain.js b/notebook/static/auth/js/logoutmain.js
new file mode 100644
index 0000000..7b3f6b4
--- /dev/null
+++ b/notebook/static/auth/js/logoutmain.js
@@ -0,0 +1,12 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define(['base/js/namespace', 'base/js/page'], function(IPython, page) {
+ function logout_main() {
+ var page_instance = new page.Page();
+ page_instance.show();
+
+ IPython.page = page_instance;
+ }
+ return logout_main;
+});
diff --git a/notebook/static/auth/js/main.js b/notebook/static/auth/js/main.js
new file mode 100644
index 0000000..7be8238
--- /dev/null
+++ b/notebook/static/auth/js/main.js
@@ -0,0 +1,9 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define(['./loginmain', './logoutmain'], function (login_main, logout_main) {
+ return {
+ login_main: login_main,
+ logout_main: logout_main
+ };
+});
diff --git a/notebook/static/auth/less/login.less b/notebook/static/auth/less/login.less
new file mode 100644
index 0000000..d21a3a1
--- /dev/null
+++ b/notebook/static/auth/less/login.less
@@ -0,0 +1,6 @@
+// Custom styles for login.html
+.center-nav {
+ display: inline-block;
+ // pull the lower margin back
+ margin-bottom: -4px;
+} \ No newline at end of file
diff --git a/notebook/static/auth/less/logout.less b/notebook/static/auth/less/logout.less
new file mode 100644
index 0000000..63cd701
--- /dev/null
+++ b/notebook/static/auth/less/logout.less
@@ -0,0 +1,2 @@
+// Custom styles for logout.html
+
diff --git a/notebook/static/auth/less/style.less b/notebook/static/auth/less/style.less
new file mode 100644
index 0000000..4d1919c
--- /dev/null
+++ b/notebook/static/auth/less/style.less
@@ -0,0 +1,7 @@
+/*!
+*
+* IPython auth
+*
+*/
+@import "login.less";
+@import "logout.less"; \ No newline at end of file
diff --git a/notebook/static/base/images/favicon.ico b/notebook/static/base/images/favicon.ico
new file mode 100644
index 0000000..6c31690
--- /dev/null
+++ b/notebook/static/base/images/favicon.ico
Binary files differ
diff --git a/notebook/static/base/images/logo.png b/notebook/static/base/images/logo.png
new file mode 100644
index 0000000..54cc416
--- /dev/null
+++ b/notebook/static/base/images/logo.png
Binary files differ
diff --git a/notebook/static/base/js/dialog.js b/notebook/static/base/js/dialog.js
new file mode 100644
index 0000000..7698174
--- /dev/null
+++ b/notebook/static/base/js/dialog.js
@@ -0,0 +1,220 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define(function(require) {
+ "use strict";
+
+ var CodeMirror = require('codemirror/lib/codemirror');
+ var $ = require('jquery');
+ // bootstrap is required for calling .modal(...) on elements
+ require('bootstrap');
+
+ /**
+ * A wrapper around bootstrap modal for easier use
+ * Pass it an option dictionary with the following properties:
+ *
+ * - body : <string> or <DOM node>, main content of the dialog
+ * if pass a <string> it will be wrapped in a p tag and
+ * html element escaped, unless you specify sanitize=false
+ * option.
+ * - title : Dialog title, default to empty string.
+ * - buttons : dict of btn_options who keys are button label.
+ * see btn_options below for description
+ * - open : callback to trigger on dialog open.
+ * - destroy:
+ * - notebook : notebook instance
+ * - keyboard_manager: keyboard manager instance.
+ *
+ * Unlike bootstrap modals, the backdrop options is set by default
+ * to 'static'.
+ *
+ * The rest of the options are passed as is to bootstrap modals.
+ *
+ * btn_options: dict with the following property:
+ *
+ * - click : callback to trigger on click
+ * - class : css classes to add to button.
+ *
+ *
+ *
+ **/
+ var modal = function (options) {
+
+ var modal = $("<div/>")
+ .addClass("modal")
+ .addClass("fade")
+ .attr("role", "dialog");
+ var dialog = $("<div/>")
+ .addClass("modal-dialog")
+ .appendTo(modal);
+ var dialog_content = $("<div/>")
+ .addClass("modal-content")
+ .appendTo(dialog);
+ if(typeof(options.body) === 'string' && options.sanitize !== false){
+ options.body = $("<p/>").text(options.body);
+ }
+ dialog_content.append(
+ $("<div/>")
+ .addClass("modal-header")
+ .append($("<button>")
+ .attr("type", "button")
+ .addClass("close")
+ .attr("data-dismiss", "modal")
+ .attr("aria-hidden", "true")
+ .html("&times;")
+ ).append(
+ $("<h4/>")
+ .addClass('modal-title')
+ .text(options.title || "")
+ )
+ ).append(
+ $("<div/>").addClass("modal-body").append(
+ options.body || $("<p/>")
+ )
+ );
+
+ var footer = $("<div/>").addClass("modal-footer");
+
+ var default_button;
+
+ for (var label in options.buttons) {
+ var btn_opts = options.buttons[label];
+ var button = $("<button/>")
+ .addClass("btn btn-default btn-sm")
+ .attr("data-dismiss", "modal")
+ .text(label);
+ if (btn_opts.click) {
+ button.click($.proxy(btn_opts.click, dialog_content));
+ }
+ if (btn_opts.class) {
+ button.addClass(btn_opts.class);
+ }
+ footer.append(button);
+ if (options.default_button && label === options.default_button) {
+ default_button = button;
+ }
+ }
+ if (!options.default_button) {
+ default_button = footer.find("button").last();
+ }
+ dialog_content.append(footer);
+ // hook up on-open event
+ modal.on("shown.bs.modal", function () {
+ setTimeout(function () {
+ default_button.focus();
+ if (options.open) {
+ $.proxy(options.open, modal)();
+ }
+ }, 0);
+ });
+
+ // destroy modal on hide, unless explicitly asked not to
+ if (options.destroy === undefined || options.destroy) {
+ modal.on("hidden.bs.modal", function () {
+ modal.remove();
+ });
+ }
+ modal.on("hidden.bs.modal", function () {
+ if (options.notebook) {
+ var cell = options.notebook.get_selected_cell();
+ if (cell) cell.select();
+ }
+ if (options.keyboard_manager) {
+ options.keyboard_manager.enable();
+ options.keyboard_manager.command_mode();
+ }
+ });
+
+ if (options.keyboard_manager) {
+ options.keyboard_manager.disable();
+ }
+
+ if(options.backdrop === undefined){
+ options.backdrop = 'static';
+ }
+
+ return modal.modal(options);
+ };
+
+ var kernel_modal = function (options) {
+ /**
+ * only one kernel dialog should be open at a time -- but
+ * other modal dialogs can still be open
+ */
+ $('.kernel-modal').modal('hide');
+ var dialog = modal(options);
+ dialog.addClass('kernel-modal');
+ return dialog;
+ };
+
+ var edit_metadata = function (options) {
+ options.name = options.name || "Cell";
+ var error_div = $('<div/>').css('color', 'red');
+ var message =
+ "Manually edit the JSON below to manipulate the metadata for this " + options.name + "." +
+ " We recommend putting custom metadata attributes in an appropriately named sub-structure," +
+ " so they don't conflict with those of others.";
+
+ var textarea = $('<textarea/>')
+ .attr('rows', '13')
+ .attr('cols', '80')
+ .attr('name', 'metadata')
+ .text(JSON.stringify(options.md || {}, null, 2));
+
+ var dialogform = $('<div/>').attr('title', 'Edit the metadata')
+ .append(
+ $('<form/>').append(
+ $('<fieldset/>').append(
+ $('<label/>')
+ .attr('for','metadata')
+ .text(message)
+ )
+ .append(error_div)
+ .append($('<br/>'))
+ .append(textarea)
+ )
+ );
+ var editor = CodeMirror.fromTextArea(textarea[0], {
+ lineNumbers: true,
+ matchBrackets: true,
+ indentUnit: 2,
+ autoIndent: true,
+ mode: 'application/json',
+ });
+ var modal_obj = modal({
+ title: "Edit " + options.name + " Metadata",
+ body: dialogform,
+ buttons: {
+ OK: { class : "btn-primary",
+ click: function() {
+ /**
+ * validate json and set it
+ */
+ var new_md;
+ try {
+ new_md = JSON.parse(editor.getValue());
+ } catch(e) {
+ console.log(e);
+ error_div.text('WARNING: Could not save invalid JSON.');
+ return false;
+ }
+ options.callback(new_md);
+ }
+ },
+ Cancel: {}
+ },
+ notebook: options.notebook,
+ keyboard_manager: options.keyboard_manager,
+ });
+
+ modal_obj.on('shown.bs.modal', function(){ editor.refresh(); });
+ };
+
+ var dialog = {
+ modal : modal,
+ kernel_modal : kernel_modal,
+ edit_metadata : edit_metadata,
+ };
+
+ return dialog;
+});
diff --git a/notebook/static/base/js/events.js b/notebook/static/base/js/events.js
new file mode 100644
index 0000000..4cab05d
--- /dev/null
+++ b/notebook/static/base/js/events.js
@@ -0,0 +1,24 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+// Give us an object to bind all events to. This object should be created
+// before all other objects so it exists when others register event handlers.
+// To register an event handler:
+//
+// require(['base/js/events'], function (events) {
+// events.on("event.Namespace", function () { do_stuff(); });
+// });
+
+define(['base/js/namespace', 'jquery'], function(IPython, $) {
+ "use strict";
+
+ var Events = function () {};
+
+ var events = new Events();
+
+ // Backwards compatability.
+ IPython.Events = Events;
+ IPython.events = events;
+
+ return $([events]);
+});
diff --git a/notebook/static/base/js/keyboard.js b/notebook/static/base/js/keyboard.js
new file mode 100644
index 0000000..ee8f253
--- /dev/null
+++ b/notebook/static/base/js/keyboard.js
@@ -0,0 +1,475 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+/**
+ *
+ *
+ * @module keyboard
+ * @namespace keyboard
+ * @class ShortcutManager
+ */
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'underscore',
+], function($, utils, _) {
+ "use strict";
+
+
+ /**
+ * Setup global keycodes and inverse keycodes.
+ *
+ * See http://unixpapa.com/js/key.html for a complete description. The short of
+ * it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
+ * and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
+ * but have minor differences.
+ **/
+
+ // These apply to Firefox, (Webkit and IE)
+ // This does work **only** on US keyboard.
+ var _keycodes = {
+ 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
+ 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
+ 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90,
+ '1 !': 49, '2 @': 50, '3 #': 51, '4 $': 52, '5 %': 53, '6 ^': 54,
+ '7 &': 55, '8 *': 56, '9 (': 57, '0 )': 48,
+ '[ {': 219, '] }': 221, '` ~': 192, ', <': 188, '. >': 190, '/ ?': 191,
+ '\\ |': 220, '\' "': 222,
+ 'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100,
+ 'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105,
+ 'multiply': 106, 'add': 107, 'subtract': 109, 'decimal': 110, 'divide': 111,
+ 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118,
+ 'f8': 119, 'f9': 120, 'f10': 121, 'f11': 122, 'f12': 123, 'f13': 124, 'f14': 125, 'f15': 126,
+ 'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18,
+ 'meta': 91, 'capslock': 20, 'esc': 27, 'space': 32, 'pageup': 33, 'pagedown': 34,
+ 'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40,
+ 'insert': 45, 'delete': 46, 'numlock': 144,
+ };
+
+ // These apply to Firefox and Opera
+ var _mozilla_keycodes = {
+ '; :': 59, '= +': 61, '- _': 173, 'meta': 224, 'minus':173
+ };
+
+ // This apply to Webkit and IE
+ var _ie_keycodes = {
+ '; :': 186, '= +': 187, '- _': 189, 'minus':189
+ };
+
+ var browser = utils.browser[0];
+ var platform = utils.platform;
+
+ if (browser === 'Firefox' || browser === 'Opera' || browser === 'Netscape') {
+ $.extend(_keycodes, _mozilla_keycodes);
+ } else if (browser === 'Safari' || browser === 'Chrome' || browser === 'MSIE') {
+ $.extend(_keycodes, _ie_keycodes);
+ }
+
+ var keycodes = {};
+ var inv_keycodes = {};
+ for (var name in _keycodes) {
+ var names = name.split(' ');
+ if (names.length === 1) {
+ var n = names[0];
+ keycodes[n] = _keycodes[n];
+ inv_keycodes[_keycodes[n]] = n;
+ } else {
+ var primary = names[0];
+ var secondary = names[1];
+ keycodes[primary] = _keycodes[name];
+ keycodes[secondary] = _keycodes[name];
+ inv_keycodes[_keycodes[name]] = primary;
+ }
+ }
+
+ var normalize_key = function (key) {
+ return inv_keycodes[keycodes[key]];
+ };
+
+ var normalize_shortcut = function (shortcut) {
+ /**
+ * @function _normalize_shortcut
+ * @private
+ * return a dict containing the normalized shortcut and the number of time it should be pressed:
+ *
+ * Put a shortcut into normalized form:
+ * 1. Make lowercase
+ * 2. Replace cmd by meta
+ * 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift
+ * 4. Normalize keys
+ **/
+ if (platform === 'MacOS') {
+ shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'cmd-');
+ } else {
+ shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'ctrl-');
+ }
+
+ shortcut = shortcut.toLowerCase().replace('cmd', 'meta');
+ shortcut = shortcut.replace(/-$/, 'minus'); // catch shortcuts using '-' key
+ shortcut = shortcut.replace(/,$/, 'comma'); // catch shortcuts using '-' key
+ if(shortcut.indexOf(',') !== -1){
+ var sht = shortcut.split(',');
+ sht = _.map(sht, normalize_shortcut);
+ return shortcut;
+ }
+ shortcut = shortcut.replace(/comma/g, ','); // catch shortcuts using '-' key
+ var values = shortcut.split("-");
+ if (values.length === 1) {
+ return normalize_key(values[0]);
+ } else {
+ var modifiers = values.slice(0,-1);
+ var key = normalize_key(values[values.length-1]);
+ modifiers.sort();
+ return modifiers.join('-') + '-' + key;
+ }
+ };
+
+ var shortcut_to_event = function (shortcut, type) {
+ /**
+ * Convert a shortcut (shift-r) to a jQuery Event object
+ **/
+ type = type || 'keydown';
+ shortcut = normalize_shortcut(shortcut);
+ shortcut = shortcut.replace(/-$/, 'minus'); // catch shortcuts using '-' key
+ var values = shortcut.split("-");
+ var modifiers = values.slice(0,-1);
+ var key = values[values.length-1];
+ var opts = {which: keycodes[key]};
+ if (modifiers.indexOf('alt') !== -1) {opts.altKey = true;}
+ if (modifiers.indexOf('ctrl') !== -1) {opts.ctrlKey = true;}
+ if (modifiers.indexOf('meta') !== -1) {opts.metaKey = true;}
+ if (modifiers.indexOf('shift') !== -1) {opts.shiftKey = true;}
+ return $.Event(type, opts);
+ };
+
+ var only_modifier_event = function(event){
+ /**
+ * Return `true` if the event only contains modifiers keys.
+ * false otherwise
+ **/
+ var key = inv_keycodes[event.which];
+ return ((event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) &&
+ (key === 'alt'|| key === 'ctrl'|| key === 'meta'|| key === 'shift'));
+
+ };
+
+ var event_to_shortcut = function (event) {
+ /**
+ * Convert a jQuery Event object to a normalized shortcut string (shift-r)
+ **/
+ var shortcut = '';
+ var key = inv_keycodes[event.which];
+ if (event.altKey && key !== 'alt') {shortcut += 'alt-';}
+ if (event.ctrlKey && key !== 'ctrl') {shortcut += 'ctrl-';}
+ if (event.metaKey && key !== 'meta') {shortcut += 'meta-';}
+ if (event.shiftKey && key !== 'shift') {shortcut += 'shift-';}
+ shortcut += key;
+ return shortcut;
+ };
+
+ // Shortcut manager class
+
+ var ShortcutManager = function (delay, events, actions, env) {
+ /**
+ * A class to deal with keyboard event and shortcut
+ *
+ * @class ShortcutManager
+ * @constructor
+ */
+ this._shortcuts = {};
+ this.delay = delay || 800; // delay in milliseconds
+ this.events = events;
+ this.actions = actions;
+ this.actions.extend_env(env);
+ this._queue = [];
+ this._cleartimeout = null;
+ Object.seal(this);
+ };
+
+ ShortcutManager.prototype.clearsoon = function(){
+ /**
+ * Clear the pending shortcut soon, and cancel previous clearing
+ * that might be registered.
+ **/
+ var that = this;
+ clearTimeout(this._cleartimeout);
+ this._cleartimeout = setTimeout(function(){that.clearqueue();}, this.delay);
+ };
+
+
+ ShortcutManager.prototype.clearqueue = function(){
+ /**
+ * clear the pending shortcut sequence now.
+ **/
+ this._queue = [];
+ clearTimeout(this._cleartimeout);
+ };
+
+
+ var flatten_shorttree = function(tree){
+ /**
+ * Flatten a tree of shortcut sequences.
+ * use full to iterate over all the key/values of available shortcuts.
+ **/
+ var dct = {};
+ for(var key in tree){
+ var value = tree[key];
+ if(typeof(value) === 'string'){
+ dct[key] = value;
+ } else {
+ var ftree=flatten_shorttree(value);
+ for(var subkey in ftree){
+ dct[key+','+subkey] = ftree[subkey];
+ }
+ }
+ }
+ return dct;
+ };
+
+ ShortcutManager.prototype.get_action_shortcut = function(name){
+ var ftree = flatten_shorttree(this._shortcuts);
+ var res = {};
+ for (var sht in ftree ){
+ if(ftree[sht] === name){
+ return sht;
+ }
+ }
+ return undefined;
+ };
+
+ ShortcutManager.prototype.help = function () {
+ var help = [];
+ var ftree = flatten_shorttree(this._shortcuts);
+ for (var shortcut in ftree) {
+ var action = this.actions.get(ftree[shortcut]);
+ var help_string = action.help||'== no help ==';
+ var help_index = action.help_index;
+ if (help_string) {
+ var shortstring = (action.shortstring||shortcut);
+ help.push({
+ shortcut: shortstring,
+ help: help_string,
+ help_index: help_index}
+ );
+ }
+ }
+ help.sort(function (a, b) {
+ if (a.help_index === b.help_index) {
+ if (a.shortcut === b.shortcut) {
+ return 0;
+ }
+ if (a.shortcut > b.shortcut) {
+ return 1;
+ }
+ return -1;
+ }
+ if (a.help_index === undefined || a.help_index > b.help_index){
+ return 1;
+ }
+ return -1;
+ });
+ return help;
+ };
+
+ ShortcutManager.prototype.clear_shortcuts = function () {
+ this._shortcuts = {};
+ };
+
+ ShortcutManager.prototype.get_shortcut = function (shortcut){
+ /**
+ * return a node of the shortcut tree which an action name (string) if leaf,
+ * and an object with `object.subtree===true`
+ **/
+ if(typeof(shortcut) === 'string'){
+ shortcut = shortcut.split(',');
+ }
+
+ return this._get_leaf(shortcut, this._shortcuts);
+ };
+
+
+ ShortcutManager.prototype._get_leaf = function(shortcut_array, tree){
+ /**
+ * @private
+ * find a leaf/node in a subtree of the keyboard shortcut
+ *
+ **/
+ if(shortcut_array.length === 1){
+ return tree[shortcut_array[0]];
+ } else if( typeof(tree[shortcut_array[0]]) !== 'string'){
+ return this._get_leaf(shortcut_array.slice(1), tree[shortcut_array[0]]);
+ }
+ return null;
+ };
+
+ ShortcutManager.prototype.set_shortcut = function( shortcut, action_name){
+ if( typeof(action_name) !== 'string'){throw new Error('action is not a string', action_name);}
+ if( typeof(shortcut) === 'string'){
+ shortcut = shortcut.split(',');
+ }
+ return this._set_leaf(shortcut, action_name, this._shortcuts);
+ };
+
+ ShortcutManager.prototype._is_leaf = function(shortcut_array, tree){
+ if(shortcut_array.length === 1){
+ return(typeof(tree[shortcut_array[0]]) === 'string');
+ } else {
+ var subtree = tree[shortcut_array[0]];
+ return this._is_leaf(shortcut_array.slice(1), subtree );
+ }
+ };
+
+ ShortcutManager.prototype._remove_leaf = function(shortcut_array, tree, allow_node){
+ if(shortcut_array.length === 1){
+ var current_node = tree[shortcut_array[0]];
+ if(typeof(current_node) === 'string'){
+ delete tree[shortcut_array[0]];
+ } else {
+ throw('try to delete non-leaf');
+ }
+ } else {
+ this._remove_leaf(shortcut_array.slice(1), tree[shortcut_array[0]], allow_node);
+ if(_.keys(tree[shortcut_array[0]]).length === 0){
+ delete tree[shortcut_array[0]];
+ }
+ }
+ };
+
+ ShortcutManager.prototype._set_leaf = function(shortcut_array, action_name, tree){
+ var current_node = tree[shortcut_array[0]];
+ if(shortcut_array.length === 1){
+ if(current_node !== undefined && typeof(current_node) !== 'string'){
+ console.warn('[warning], you are overriting a long shortcut with a shorter one');
+ }
+ tree[shortcut_array[0]] = action_name;
+ return true;
+ } else {
+ if(typeof(current_node) === 'string'){
+ console.warn('you are trying to set a shortcut that will be shadowed'+
+ 'by a more specific one. Aborting for :', action_name, 'the follwing '+
+ 'will take precedence', current_node);
+ return false;
+ } else {
+ tree[shortcut_array[0]] = tree[shortcut_array[0]]||{};
+ }
+ this._set_leaf(shortcut_array.slice(1), action_name, tree[shortcut_array[0]]);
+ return true;
+ }
+ };
+
+ ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) {
+ /**
+ * Add a action to be handled by shortcut manager.
+ *
+ * - `shortcut` should be a `Shortcut Sequence` of the for `Ctrl-Alt-C,Meta-X`...
+ * - `data` could be an `action name`, an `action` or a `function`.
+ * if a `function` is passed it will be converted to an anonymous `action`.
+ *
+ **/
+ var action_name = this.actions.get_name(data);
+ if (! action_name){
+ throw new Error('does not know how to deal with : ' + data);
+ }
+ shortcut = normalize_shortcut(shortcut);
+ this.set_shortcut(shortcut, action_name);
+
+ if (!suppress_help_update) {
+ // update the keyboard shortcuts notebook help
+ this.events.trigger('rebuild.QuickHelp');
+ }
+ };
+
+ ShortcutManager.prototype.add_shortcuts = function (data) {
+ /**
+ * Convenient methods to call `add_shortcut(key, value)` on several items
+ *
+ * data : Dict of the form {key:value, ...}
+ **/
+ for (var shortcut in data) {
+ this.add_shortcut(shortcut, data[shortcut], true);
+ }
+ // update the keyboard shortcuts notebook help
+ this.events.trigger('rebuild.QuickHelp');
+ };
+
+ ShortcutManager.prototype.remove_shortcut = function (shortcut, suppress_help_update) {
+ /**
+ * Remove the binding of shortcut `sortcut` with its action.
+ * throw an error if trying to remove a non-exiting shortcut
+ **/
+ shortcut = normalize_shortcut(shortcut);
+ if( typeof(shortcut) === 'string'){
+ shortcut = shortcut.split(',');
+ }
+ /*
+ * The shortcut error should be explicit here, because it will be
+ * seen by users.
+ */
+ try
+ {
+ this._remove_leaf(shortcut, this._shortcuts);
+ if (!suppress_help_update) {
+ // update the keyboard shortcuts notebook help
+ this.events.trigger('rebuild.QuickHelp');
+ }
+ } catch (ex) {
+ throw new Error('trying to remove a non-existent shortcut', shortcut);
+ }
+ };
+
+
+
+ ShortcutManager.prototype.call_handler = function (event) {
+ /**
+ * Call the corresponding shortcut handler for a keyboard event
+ * @method call_handler
+ * @return {Boolean} `true|false`, `false` if no handler was found, otherwise the value return by the handler.
+ * @param event {event}
+ *
+ * given an event, call the corresponding shortcut.
+ * return false is event wan handled, true otherwise
+ * in any case returning false stop event propagation
+ **/
+
+
+ this.clearsoon();
+ if(only_modifier_event(event)){
+ return true;
+ }
+ var shortcut = event_to_shortcut(event);
+ this._queue.push(shortcut);
+ var action_name = this.get_shortcut(this._queue);
+
+ if (typeof(action_name) === 'undefined'|| action_name === null){
+ this.clearqueue();
+ return true;
+ }
+
+ if (this.actions.exists(action_name)) {
+ event.preventDefault();
+ this.clearqueue();
+ return this.actions.call(action_name, event);
+ }
+
+ return false;
+ };
+
+
+ ShortcutManager.prototype.handles = function (event) {
+ var shortcut = event_to_shortcut(event);
+ var action_name = this.get_shortcut(this._queue.concat(shortcut));
+ return (typeof(action_name) !== 'undefined');
+ };
+
+ var keyboard = {
+ keycodes : keycodes,
+ inv_keycodes : inv_keycodes,
+ ShortcutManager : ShortcutManager,
+ normalize_key : normalize_key,
+ normalize_shortcut : normalize_shortcut,
+ shortcut_to_event : shortcut_to_event,
+ event_to_shortcut : event_to_shortcut,
+ };
+
+ return keyboard;
+});
diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js
new file mode 100644
index 0000000..6783577
--- /dev/null
+++ b/notebook/static/base/js/namespace.js
@@ -0,0 +1,82 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+
+var Jupyter = Jupyter || {};
+
+var jprop = function(name, module_path){
+ Object.defineProperty(Jupyter, name, {
+ get: function() {
+ console.warn('accessing `'+name+'` is deprecated. Use `require("'+module_path+'")`');
+ return require(module_path);
+ },
+ enumerable: true,
+ configurable: false
+ });
+}
+
+var jglobal = function(name, module_path){
+ Object.defineProperty(Jupyter, name, {
+ get: function() {
+ console.warn('accessing `'+name+'` is deprecated. Use `require("'+module_path+'").'+name+'`');
+ return require(module_path)[name];
+ },
+ enumerable: true,
+ configurable: false
+ });
+}
+
+define(function(){
+ "use strict";
+
+ // expose modules
+
+ jprop('utils','base/js/utils')
+
+ //Jupyter.load_extensions = Jupyter.utils.load_extensions;
+ //
+ jprop('security','base/js/security');
+ jprop('keyboard','base/js/keyboard');
+ jprop('dialog','base/js/dialog');
+ jprop('mathjaxutils','notebook/js/mathjaxutils');
+
+
+ //// exposed constructors
+ jglobal('CommManager','services/kernels/comm')
+ jglobal('Comm','services/kernels/comm')
+
+ jglobal('NotificationWidget','base/js/notificationwidget');
+ jglobal('Kernel','services/kernels/kernel');
+ jglobal('Session','services/sessions/session');
+ jglobal('LoginWidget','auth/js/loginwidget');
+ jglobal('Page','base/js/page');
+
+ // notebook
+ jglobal('TextCell','notebook/js/textcell');
+ jglobal('OutputArea','notebook/js/outputarea');
+ jglobal('KeyboardManager','notebook/js/keyboardmanager');
+ jglobal('Completer','notebook/js/completer');
+ jglobal('Notebook','notebook/js/notebook');
+ jglobal('Tooltip','notebook/js/tooltip');
+ jglobal('Toolbar','notebook/js/toolbar');
+ jglobal('SaveWidget','notebook/js/savewidget');
+ jglobal('Pager','notebook/js/pager');
+ jglobal('QuickHelp','notebook/js/quickhelp');
+ jglobal('MarkdownCell','notebook/js/textcell');
+ jglobal('RawCell','notebook/js/textcell');
+ jglobal('Cell','notebook/js/cell');
+ jglobal('MainToolBar','notebook/js/maintoolbar');
+ jglobal('NotebookNotificationArea','notebook/js/notificationarea');
+ jglobal('NotebookTour', 'notebook/js/tour');
+ jglobal('MenuBar', 'notebook/js/menubar');
+
+ // tree
+ jglobal('SessionList','tree/js/sessionlist');
+
+ Jupyter.version = "4.2.3";
+ Jupyter._target = '_blank';
+ return Jupyter;
+});
+
+// deprecated since 4.0, remove in 5+
+var IPython = Jupyter
diff --git a/notebook/static/base/js/notificationarea.js b/notebook/static/base/js/notificationarea.js
new file mode 100644
index 0000000..94b7649
--- /dev/null
+++ b/notebook/static/base/js/notificationarea.js
@@ -0,0 +1,83 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/notificationwidget',
+], function($, notificationwidget) {
+ "use strict";
+
+ // store reference to the NotificationWidget class
+ var NotificationWidget = notificationwidget.NotificationWidget;
+
+ /**
+ * Construct the NotificationArea object. Options are:
+ * events: $(Events) instance
+ * save_widget: SaveWidget instance
+ * notebook: Notebook instance
+ * keyboard_manager: KeyboardManager instance
+ *
+ * @constructor
+ * @param {string} selector - a jQuery selector string for the
+ * notification area element
+ * @param {Object} [options] - a dictionary of keyword arguments.
+ */
+ var NotificationArea = function (selector, options) {
+ this.selector = selector;
+ this.events = options.events;
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ }
+ this.widget_dict = {};
+ };
+
+ /**
+ * Get a widget by name, creating it if it doesn't exist.
+ *
+ * @method widget
+ * @param {string} name - the widget name
+ */
+ NotificationArea.prototype.widget = function (name) {
+ if (this.widget_dict[name] === undefined) {
+ return this.new_notification_widget(name);
+ }
+ return this.get_widget(name);
+ };
+
+ /**
+ * Get a widget by name, throwing an error if it doesn't exist.
+ *
+ * @method get_widget
+ * @param {string} name - the widget name
+ */
+ NotificationArea.prototype.get_widget = function (name) {
+ if(this.widget_dict[name] === undefined) {
+ throw('no widgets with this name');
+ }
+ return this.widget_dict[name];
+ };
+
+ /**
+ * Create a new notification widget with the given name. The
+ * widget must not already exist.
+ *
+ * @method new_notification_widget
+ * @param {string} name - the widget name
+ */
+ NotificationArea.prototype.new_notification_widget = function (name) {
+ if (this.widget_dict[name] !== undefined) {
+ throw('widget with that name already exists!');
+ }
+
+ // create the element for the notification widget and add it
+ // to the notification aread element
+ var div = $('<div/>').attr('id', 'notification_' + name);
+ $(this.selector).append(div);
+
+ // create the widget object and return it
+ this.widget_dict[name] = new NotificationWidget('#notification_' + name);
+ return this.widget_dict[name];
+ };
+
+ return {'NotificationArea': NotificationArea};
+});
diff --git a/notebook/static/base/js/notificationwidget.js b/notebook/static/base/js/notificationwidget.js
new file mode 100644
index 0000000..6601437
--- /dev/null
+++ b/notebook/static/base/js/notificationwidget.js
@@ -0,0 +1,170 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+], function($) {
+ "use strict";
+
+ /**
+ * Construct a NotificationWidget object.
+ *
+ * @constructor
+ * @param {string} selector - a jQuery selector string for the
+ * notification widget element
+ */
+ var NotificationWidget = function (selector) {
+ this.selector = selector;
+ this.timeout = null;
+ this.busy = false;
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.style();
+ }
+ this.element.hide();
+ this.inner = $('<span/>');
+ this.element.append(this.inner);
+ };
+
+ /**
+ * Add the 'notification_widget' CSS class to the widget element.
+ *
+ * @method style
+ */
+ NotificationWidget.prototype.style = function () {
+ // use explicit bootstrap classes here,
+ // because multiple inheritance in LESS doesn't work
+ // for this particular combination
+ this.element.addClass('notification_widget btn btn-xs navbar-btn');
+ };
+
+ /**
+ * hide the widget and empty the text
+ **/
+ NotificationWidget.prototype.hide = function () {
+ var that = this;
+ this.element.fadeOut(100, function(){that.inner.text('');});
+ };
+
+ /**
+ * Set the notification widget message to display for a certain
+ * amount of time (timeout). The widget will be shown forever if
+ * timeout is <= 0 or undefined. If the widget is clicked while it
+ * is still displayed, execute an optional callback
+ * (click_callback). If the callback returns false, it will
+ * prevent the notification from being dismissed.
+ *
+ * Options:
+ * class - CSS class name for styling
+ * icon - CSS class name for the widget icon
+ * title - HTML title attribute for the widget
+ *
+ * @method set_message
+ * @param {string} msg - The notification to display
+ * @param {integer} [timeout] - The amount of time in milliseconds to display the widget
+ * @param {function} [click_callback] - The function to run when the widget is clicked
+ * @param {Object} [options] - Additional options
+ */
+ NotificationWidget.prototype.set_message = function (msg, timeout, click_callback, options) {
+ options = options || {};
+
+ // unbind potential previous callback
+ this.element.unbind('click');
+ this.inner.attr('class', options.icon);
+ this.inner.attr('title', options.title);
+ this.inner.text(msg);
+ this.element.fadeIn(100);
+
+ // reset previous set style
+ this.element.removeClass();
+ this.style();
+ if (options.class) {
+ this.element.addClass(options.class);
+ }
+
+ // clear previous timer
+ if (this.timeout !== null) {
+ clearTimeout(this.timeout);
+ this.timeout = null;
+ }
+
+ // set the timer if a timeout is given
+ var that = this;
+ if (timeout !== undefined && timeout >= 0) {
+ this.timeout = setTimeout(function () {
+ that.element.fadeOut(100, function () {that.inner.text('');});
+ that.element.unbind('click');
+ that.timeout = null;
+ }, timeout);
+ }
+
+ // if no click callback assume we will just dismiss the notification
+ if (click_callback === undefined) {
+ click_callback = function(){return true};
+ }
+ // on click, remove widget if click callback say so
+ // and unbind click event.
+ this.element.click(function () {
+ if (click_callback() !== false) {
+ that.element.fadeOut(100, function () {that.inner.text('');});
+ that.element.unbind('click');
+ }
+ if (that.timeout !== null) {
+ clearTimeout(that.timeout);
+ that.timeout = null;
+ }
+ });
+ };
+
+ /**
+ * Display an information message (styled with the 'info'
+ * class). Arguments are the same as in set_message. Default
+ * timeout is 3500 milliseconds.
+ *
+ * @method info
+ */
+ NotificationWidget.prototype.info = function (msg, timeout, click_callback, options) {
+ options = options || {};
+ options.class = options.class + ' info';
+ timeout = timeout || 3500;
+ this.set_message(msg, timeout, click_callback, options);
+ };
+
+ /**
+ * Display a warning message (styled with the 'warning'
+ * class). Arguments are the same as in set_message. Messages are
+ * sticky by default.
+ *
+ * @method warning
+ */
+ NotificationWidget.prototype.warning = function (msg, timeout, click_callback, options) {
+ options = options || {};
+ options.class = options.class + ' warning';
+ this.set_message(msg, timeout, click_callback, options);
+ };
+
+ /**
+ * Display a danger message (styled with the 'danger'
+ * class). Arguments are the same as in set_message. Messages are
+ * sticky by default.
+ *
+ * @method danger
+ */
+ NotificationWidget.prototype.danger = function (msg, timeout, click_callback, options) {
+ options = options || {};
+ options.class = options.class + ' danger';
+ this.set_message(msg, timeout, click_callback, options);
+ };
+
+ /**
+ * Get the text of the widget message.
+ *
+ * @method get_message
+ * @return {string} - the message text
+ */
+ NotificationWidget.prototype.get_message = function () {
+ return this.inner.html();
+ };
+
+ return {'NotificationWidget': NotificationWidget};
+});
diff --git a/notebook/static/base/js/page.js b/notebook/static/base/js/page.js
new file mode 100644
index 0000000..9ec7278
--- /dev/null
+++ b/notebook/static/base/js/page.js
@@ -0,0 +1,62 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/events',
+], function($, events){
+ "use strict";
+
+ var Page = function () {
+ this.bind_events();
+ };
+
+ Page.prototype.bind_events = function () {
+ // resize site on:
+ // - window resize
+ // - header change
+ // - page load
+ var _handle_resize = $.proxy(this._resize_site, this);
+
+ $(window).resize(_handle_resize);
+
+ // On document ready, resize codemirror.
+ $(document).ready(_handle_resize);
+ events.on('resize-header.Page', _handle_resize);
+ };
+
+ Page.prototype.show = function () {
+ /**
+ * The header and site divs start out hidden to prevent FLOUC.
+ * Main scripts should call this method after styling everything.
+ */
+ this.show_header();
+ this.show_site();
+ };
+
+ Page.prototype.show_header = function () {
+ /**
+ * The header and site divs start out hidden to prevent FLOUC.
+ * Main scripts should call this method after styling everything.
+ * TODO: selector are hardcoded, pass as constructor argument
+ */
+ $('div#header').css('display','block');
+ };
+
+ Page.prototype.show_site = function () {
+ /**
+ * The header and site divs start out hidden to prevent FLOUC.
+ * Main scripts should call this method after styling everything.
+ * TODO: selector are hardcoded, pass as constructor argument
+ */
+ $('div#site').css('display', 'block');
+ this._resize_site();
+ };
+
+ Page.prototype._resize_site = function() {
+ // Update the site's size.
+ $('div#site').height($(window).height() - $('#header').height());
+ };
+
+ return {'Page': Page};
+});
diff --git a/notebook/static/base/js/security.js b/notebook/static/base/js/security.js
new file mode 100644
index 0000000..71130fc
--- /dev/null
+++ b/notebook/static/base/js/security.js
@@ -0,0 +1,126 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'components/google-caja/html-css-sanitizer-minified',
+], function($, sanitize) {
+ "use strict";
+
+ var noop = function (x) { return x; };
+
+ var caja;
+ if (window && window.html) {
+ caja = window.html;
+ caja.html4 = window.html4;
+ caja.sanitizeStylesheet = window.sanitizeStylesheet;
+ }
+
+ var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
+ /**
+ * add trusting data-attributes to the default sanitizeAttribs from caja
+ * this function is mostly copied from the caja source
+ */
+ var ATTRIBS = caja.html4.ATTRIBS;
+ for (var i = 0; i < attribs.length; i += 2) {
+ var attribName = attribs[i];
+ if (attribName.substr(0,5) == 'data-') {
+ var attribKey = '*::' + attribName;
+ if (!ATTRIBS.hasOwnProperty(attribKey)) {
+ ATTRIBS[attribKey] = 0;
+ }
+ }
+ }
+ return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
+ };
+
+ var sanitize_css = function (css, tagPolicy) {
+ /**
+ * sanitize CSS
+ * like sanitize_html, but for CSS
+ * called by sanitize_stylesheets
+ */
+ return caja.sanitizeStylesheet(
+ window.location.pathname,
+ css,
+ {
+ containerClass: null,
+ idSuffix: '',
+ tagPolicy: tagPolicy,
+ virtualizeAttrName: noop
+ },
+ noop
+ );
+ };
+
+ var sanitize_stylesheets = function (html, tagPolicy) {
+ /**
+ * sanitize just the css in style tags in a block of html
+ * called by sanitize_html, if allow_css is true
+ */
+ var h = $("<div/>").append(html);
+ var style_tags = h.find("style");
+ if (!style_tags.length) {
+ // no style tags to sanitize
+ return html;
+ }
+ style_tags.each(function(i, style) {
+ style.innerHTML = sanitize_css(style.innerHTML, tagPolicy);
+ });
+ return h.html();
+ };
+
+ var sanitize_html = function (html, allow_css) {
+ /**
+ * sanitize HTML
+ * if allow_css is true (default: false), CSS is sanitized as well.
+ * otherwise, CSS elements and attributes are simply removed.
+ */
+ var html4 = caja.html4;
+
+ if (allow_css) {
+ // allow sanitization of style tags,
+ // not just scrubbing
+ html4.ELEMENTS.style &= ~html4.eflags.UNSAFE;
+ html4.ATTRIBS.style = html4.atype.STYLE;
+ } else {
+ // scrub all CSS
+ html4.ELEMENTS.style |= html4.eflags.UNSAFE;
+ html4.ATTRIBS.style = html4.atype.SCRIPT;
+ }
+
+ var record_messages = function (msg, opts) {
+ console.log("HTML Sanitizer", msg, opts);
+ };
+
+ var policy = function (tagName, attribs) {
+ if (!(html4.ELEMENTS[tagName] & html4.eflags.UNSAFE)) {
+ return {
+ 'attribs': sanitizeAttribs(tagName, attribs,
+ noop, noop, record_messages)
+ };
+ } else {
+ record_messages(tagName + " removed", {
+ change: "removed",
+ tagName: tagName
+ });
+ }
+ };
+
+ var sanitized = caja.sanitizeWithPolicy(html, policy);
+
+ if (allow_css) {
+ // sanitize style tags as stylesheets
+ sanitized = sanitize_stylesheets(result.sanitized, policy);
+ }
+
+ return sanitized;
+ };
+
+ var security = {
+ caja: caja,
+ sanitize_html: sanitize_html
+ };
+
+ return security;
+});
diff --git a/notebook/static/base/js/utils.js b/notebook/static/base/js/utils.js
new file mode 100644
index 0000000..e64d71a
--- /dev/null
+++ b/notebook/static/base/js/utils.js
@@ -0,0 +1,898 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'codemirror/lib/codemirror',
+ 'moment',
+ // silently upgrades CodeMirror
+ 'codemirror/mode/meta',
+], function($, CodeMirror, moment){
+ "use strict";
+
+ // keep track of which extensions have been loaded already
+ var extensions_loaded = [];
+
+ /**
+ * Whether or not an extension has been loaded
+ * @param {string} extension - name of the extension
+ * @return {boolean} true if loaded already
+ */
+ var is_loaded = function(extension) {
+ var ext_path = "nbextensions/" + extension;
+ return extensions_loaded.indexOf(ext_path) >= 0;
+ };
+
+ /**
+ * Load a single extension.
+ * @param {string} extension - extension path.
+ * @return {Promise} that resolves to an extension module handle
+ */
+ var load_extension = function (extension) {
+ return new Promise(function(resolve, reject) {
+ var ext_path = "nbextensions/" + extension;
+ requirejs([ext_path], function(module) {
+ if (!is_loaded(extension)) {
+ console.log("Loading extension: " + extension);
+ if (module.load_ipython_extension) {
+ Promise.resolve(module.load_ipython_extension()).then(function() {
+ resolve(module);
+ }).catch(reject);
+ }
+ extensions_loaded.push(ext_path);
+ } else {
+ console.log("Loaded extension already: " + extension);
+ resolve(module);
+ }
+ }, function(err) {
+ reject(err);
+ });
+ });
+ };
+
+ /**
+ * Load multiple extensions.
+ * Takes n-args, where each arg is a string path to the extension.
+ * @return {Promise} that resolves to a list of loaded module handles.
+ */
+ var load_extensions = function () {
+ console.log("load_extensions", arguments);
+ return Promise.all(Array.prototype.map.call(arguments, load_extension)).catch(function(err) {
+ console.error("Failed to load extension" + (err.requireModules.length>1?'s':'') + ":", err.requireModules, err);
+ });
+ };
+
+ /**
+ * Return a list of extensions that should be active
+ * The config for nbextensions comes in as a dict where keys are
+ * nbextensions paths and the values are a bool indicating if it
+ * should be active. This returns a list of nbextension paths
+ * where the value is true
+ */
+ function filter_extensions(nbext_config) {
+ var active = [];
+ Object.keys(nbext_config).forEach(function (nbext) {
+ if (nbext_config[nbext]) {active.push(nbext);}
+ });
+ return active;
+ }
+
+ /**
+ * Wait for a config section to load, and then load the extensions specified
+ * in a 'load_extensions' key inside it.
+ */
+ function load_extensions_from_config(section) {
+ return section.loaded.then(function() {
+ if (section.data.load_extensions) {
+ var active = filter_extensions(section.data.load_extensions);
+ return load_extensions.apply(this, active);
+ }
+ }).catch(utils.reject('Could not load nbextensions from ' + section.section_name + ' config file'));
+ }
+
+ //============================================================================
+ // Cross-browser RegEx Split
+ //============================================================================
+
+ // This code has been MODIFIED from the code licensed below to not replace the
+ // default browser split. The license is reproduced here.
+
+ // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
+ /*!
+ * Cross-Browser Split 1.1.1
+ * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
+ * Available under the MIT License
+ * ECMAScript compliant, uniform cross-browser split method
+ */
+
+ /**
+ * Splits a string into an array of strings using a regex or string
+ * separator. Matches of the separator are not included in the result array.
+ * However, if `separator` is a regex that contains capturing groups,
+ * backreferences are spliced into the result each time `separator` is
+ * matched. Fixes browser bugs compared to the native
+ * `String.prototype.split` and can be used reliably cross-browser.
+ * @param {String} str String to split.
+ * @param {RegExp} separator Regex to use for separating
+ * the string.
+ * @param {Number} [limit] Maximum number of items to include in the result
+ * array.
+ * @returns {Array} Array of substrings.
+ * @example
+ *
+ * // Basic use
+ * regex_split('a b c d', ' ');
+ * // -> ['a', 'b', 'c', 'd']
+ *
+ * // With limit
+ * regex_split('a b c d', ' ', 2);
+ * // -> ['a', 'b']
+ *
+ * // Backreferences in result array
+ * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
+ * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
+ */
+ var regex_split = function (str, separator, limit) {
+ var output = [],
+ flags = (separator.ignoreCase ? "i" : "") +
+ (separator.multiline ? "m" : "") +
+ (separator.extended ? "x" : "") + // Proposed for ES6
+ (separator.sticky ? "y" : ""), // Firefox 3+
+ lastLastIndex = 0,
+ separator2, match, lastIndex, lastLength;
+ // Make `global` and avoid `lastIndex` issues by working with a copy
+ separator = new RegExp(separator.source, flags + "g");
+
+ var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
+ if (!compliantExecNpcg) {
+ // Doesn't need flags gy, but they don't hurt
+ separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
+ }
+ /* Values for `limit`, per the spec:
+ * If undefined: 4294967295 // Math.pow(2, 32) - 1
+ * If 0, Infinity, or NaN: 0
+ * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
+ * If negative number: 4294967296 - Math.floor(Math.abs(limit))
+ * If other: Type-convert, then use the above rules
+ */
+ limit = typeof(limit) === "undefined" ?
+ -1 >>> 0 : // Math.pow(2, 32) - 1
+ limit >>> 0; // ToUint32(limit)
+ for (match = separator.exec(str); match; match = separator.exec(str)) {
+ // `separator.lastIndex` is not reliable cross-browser
+ lastIndex = match.index + match[0].length;
+ if (lastIndex > lastLastIndex) {
+ output.push(str.slice(lastLastIndex, match.index));
+ // Fix browsers whose `exec` methods don't consistently return `undefined` for
+ // nonparticipating capturing groups
+ if (!compliantExecNpcg && match.length > 1) {
+ match[0].replace(separator2, function () {
+ for (var i = 1; i < arguments.length - 2; i++) {
+ if (typeof(arguments[i]) === "undefined") {
+ match[i] = undefined;
+ }
+ }
+ });
+ }
+ if (match.length > 1 && match.index < str.length) {
+ Array.prototype.push.apply(output, match.slice(1));
+ }
+ lastLength = match[0].length;
+ lastLastIndex = lastIndex;
+ if (output.length >= limit) {
+ break;
+ }
+ }
+ if (separator.lastIndex === match.index) {
+ separator.lastIndex++; // Avoid an infinite loop
+ }
+ }
+ if (lastLastIndex === str.length) {
+ if (lastLength || !separator.test("")) {
+ output.push("");
+ }
+ } else {
+ output.push(str.slice(lastLastIndex));
+ }
+ return output.length > limit ? output.slice(0, limit) : output;
+ };
+
+ //============================================================================
+ // End contributed Cross-browser RegEx Split
+ //============================================================================
+
+
+ var uuid = function () {
+ /**
+ * http://www.ietf.org/rfc/rfc4122.txt
+ */
+ var s = [];
+ var hexDigits = "0123456789ABCDEF";
+ for (var i = 0; i < 32; i++) {
+ s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
+ }
+ s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
+ s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
+
+ var uuid = s.join("");
+ return uuid;
+ };
+
+
+ //Fix raw text to parse correctly in crazy XML
+ function xmlencode(string) {
+ return string.replace(/\&/g,'&'+'amp;')
+ .replace(/</g,'&'+'lt;')
+ .replace(/>/g,'&'+'gt;')
+ .replace(/\'/g,'&'+'apos;')
+ .replace(/\"/g,'&'+'quot;')
+ .replace(/`/g,'&'+'#96;');
+ }
+
+
+ //Map from terminal commands to CSS classes
+ var ansi_colormap = {
+ "01":"ansibold",
+
+ "30":"ansiblack",
+ "31":"ansired",
+ "32":"ansigreen",
+ "33":"ansiyellow",
+ "34":"ansiblue",
+ "35":"ansipurple",
+ "36":"ansicyan",
+ "37":"ansigray",
+
+ "40":"ansibgblack",
+ "41":"ansibgred",
+ "42":"ansibggreen",
+ "43":"ansibgyellow",
+ "44":"ansibgblue",
+ "45":"ansibgpurple",
+ "46":"ansibgcyan",
+ "47":"ansibggray"
+ };
+
+ function _process_numbers(attrs, numbers) {
+ // process ansi escapes
+ var n = numbers.shift();
+ if (ansi_colormap[n]) {
+ if ( ! attrs["class"] ) {
+ attrs["class"] = ansi_colormap[n];
+ } else {
+ attrs["class"] += " " + ansi_colormap[n];
+ }
+ } else if (n == "38" || n == "48") {
+ // VT100 256 color or 24 bit RGB
+ if (numbers.length < 2) {
+ console.log("Not enough fields for VT100 color", numbers);
+ return;
+ }
+
+ var index_or_rgb = numbers.shift();
+ var r,g,b;
+ if (index_or_rgb == "5") {
+ // 256 color
+ var idx = parseInt(numbers.shift(), 10);
+ if (idx < 16) {
+ // indexed ANSI
+ // ignore bright / non-bright distinction
+ idx = idx % 8;
+ var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
+ if ( ! attrs["class"] ) {
+ attrs["class"] = ansiclass;
+ } else {
+ attrs["class"] += " " + ansiclass;
+ }
+ return;
+ } else if (idx < 232) {
+ // 216 color 6x6x6 RGB
+ idx = idx - 16;
+ b = idx % 6;
+ g = Math.floor(idx / 6) % 6;
+ r = Math.floor(idx / 36) % 6;
+ // convert to rgb
+ r = (r * 51);
+ g = (g * 51);
+ b = (b * 51);
+ } else {
+ // grayscale
+ idx = idx - 231;
+ // it's 1-24 and should *not* include black or white,
+ // so a 26 point scale
+ r = g = b = Math.floor(idx * 256 / 26);
+ }
+ } else if (index_or_rgb == "2") {
+ // Simple 24 bit RGB
+ if (numbers.length > 3) {
+ console.log("Not enough fields for RGB", numbers);
+ return;
+ }
+ r = numbers.shift();
+ g = numbers.shift();
+ b = numbers.shift();
+ } else {
+ console.log("unrecognized control", numbers);
+ return;
+ }
+ if (r !== undefined) {
+ // apply the rgb color
+ var line;
+ if (n == "38") {
+ line = "color: ";
+ } else {
+ line = "background-color: ";
+ }
+ line = line + "rgb(" + r + "," + g + "," + b + ");";
+ if ( !attrs.style ) {
+ attrs.style = line;
+ } else {
+ attrs.style += " " + line;
+ }
+ }
+ }
+ }
+
+ function _ansispan(str) {
+ // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
+ // regular ansi escapes (using the table above)
+ var is_open = false;
+ return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
+ if (!pattern || prefix === '39') {
+ // [(01|22|39|)m close spans
+ if (is_open) {
+ is_open = false;
+ return "</span>";
+ } else {
+ return "";
+ }
+ } else {
+ is_open = true;
+
+ // consume sequence of color escapes
+ var numbers = pattern.match(/\d+/g);
+ var attrs = {};
+ while (numbers.length > 0) {
+ _process_numbers(attrs, numbers);
+ }
+
+ var span = "<span ";
+ Object.keys(attrs).map(function (attr) {
+ span = span + " " + attr + '="' + attrs[attr] + '"';
+ });
+ return span + ">";
+ }
+ });
+ }
+
+ // Transform ANSI color escape codes into HTML <span> tags with css
+ // classes listed in the above ansi_colormap object. The actual color used
+ // are set in the css file.
+ function fixConsole(txt) {
+ txt = xmlencode(txt);
+
+ // Strip all ANSI codes that are not color related. Matches
+ // all ANSI codes that do not end with "m".
+ var ignored_re = /(?=(\033\[[?\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
+ txt = txt.replace(ignored_re, "");
+
+ // color ansi codes
+ txt = _ansispan(txt);
+ return txt;
+ }
+
+ // Remove chunks that should be overridden by the effect of
+ // carriage return characters
+ function fixCarriageReturn(txt) {
+ var tmp = txt;
+ do {
+ txt = tmp;
+ tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
+ tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
+ } while (tmp.length < txt.length);
+ return txt;
+ }
+
+ // Locate any URLs and convert them to a anchor tag
+ function autoLinkUrls(txt) {
+ return txt.replace(/(^|\s)(https?|ftp)(:[^'"<>\s]+)/gi,
+ "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
+ }
+
+ var points_to_pixels = function (points) {
+ /**
+ * A reasonably good way of converting between points and pixels.
+ */
+ var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
+ $('body').append(test);
+ var pixel_per_point = test.width()/10000;
+ test.remove();
+ return Math.floor(points*pixel_per_point);
+ };
+
+ var always_new = function (constructor) {
+ /**
+ * wrapper around contructor to avoid requiring `var a = new constructor()`
+ * useful for passing constructors as callbacks,
+ * not for programmer laziness.
+ * from http://programmers.stackexchange.com/questions/118798
+ */
+ return function () {
+ var obj = Object.create(constructor.prototype);
+ constructor.apply(obj, arguments);
+ return obj;
+ };
+ };
+
+ var url_path_join = function () {
+ /**
+ * join a sequence of url components with '/'
+ */
+ var url = '';
+ for (var i = 0; i < arguments.length; i++) {
+ if (arguments[i] === '') {
+ continue;
+ }
+ if (url.length > 0 && url[url.length-1] != '/') {
+ url = url + '/' + arguments[i];
+ } else {
+ url = url + arguments[i];
+ }
+ }
+ url = url.replace(/\/\/+/, '/');
+ return url;
+ };
+
+ var url_path_split = function (path) {
+ /**
+ * Like os.path.split for URLs.
+ * Always returns two strings, the directory path and the base filename
+ */
+
+ var idx = path.lastIndexOf('/');
+ if (idx === -1) {
+ return ['', path];
+ } else {
+ return [ path.slice(0, idx), path.slice(idx + 1) ];
+ }
+ };
+
+ var parse_url = function (url) {
+ /**
+ * an `a` element with an href allows attr-access to the parsed segments of a URL
+ * a = parse_url("http://localhost:8888/path/name#hash")
+ * a.protocol = "http:"
+ * a.host = "localhost:8888"
+ * a.hostname = "localhost"
+ * a.port = 8888
+ * a.pathname = "/path/name"
+ * a.hash = "#hash"
+ */
+ var a = document.createElement("a");
+ a.href = url;
+ return a;
+ };
+
+ var encode_uri_components = function (uri) {
+ /**
+ * encode just the components of a multi-segment uri,
+ * leaving '/' separators
+ */
+ return uri.split('/').map(encodeURIComponent).join('/');
+ };
+
+ var url_join_encode = function () {
+ /**
+ * join a sequence of url components with '/',
+ * encoding each component with encodeURIComponent
+ */
+ return encode_uri_components(url_path_join.apply(null, arguments));
+ };
+
+
+ var splitext = function (filename) {
+ /**
+ * mimic Python os.path.splitext
+ * Returns ['base', '.ext']
+ */
+ var idx = filename.lastIndexOf('.');
+ if (idx > 0) {
+ return [filename.slice(0, idx), filename.slice(idx)];
+ } else {
+ return [filename, ''];
+ }
+ };
+
+
+ var escape_html = function (text) {
+ /**
+ * escape text to HTML
+ */
+ return $("<div/>").text(text).html();
+ };
+
+
+ var get_body_data = function(key) {
+ /**
+ * get a url-encoded item from body.data and decode it
+ * we should never have any encoded URLs anywhere else in code
+ * until we are building an actual request
+ */
+ var val = $('body').data(key);
+ if (!val)
+ return val;
+ return decodeURIComponent(val);
+ };
+
+ var to_absolute_cursor_pos = function (cm, cursor) {
+ console.warn('`utils.to_absolute_cursor_pos(cm, pos)` is deprecated. Use `cm.indexFromPos(cursor)`');
+ return cm.indexFromPos(cusrsor);
+ };
+
+ var from_absolute_cursor_pos = function (cm, cursor_pos) {
+ console.warn('`utils.from_absolute_cursor_pos(cm, pos)` is deprecated. Use `cm.posFromIndex(index)`');
+ return cm.posFromIndex(cursor_pos);
+ };
+
+ // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
+ var browser = (function() {
+ if (typeof navigator === 'undefined') {
+ // navigator undefined in node
+ return 'None';
+ }
+ var N= navigator.appName, ua= navigator.userAgent, tem;
+ var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
+ if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
+ M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
+ return M;
+ })();
+
+ // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
+ var platform = (function () {
+ if (typeof navigator === 'undefined') {
+ // navigator undefined in node
+ return 'None';
+ }
+ var OSName="None";
+ if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
+ if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
+ if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
+ if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
+ return OSName;
+ })();
+
+ var get_url_param = function (name) {
+ // get a URL parameter. I cannot believe we actually need this.
+ // Based on http://stackoverflow.com/a/25359264/938949
+ var match = new RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
+ if (match){
+ return decodeURIComponent(match[1] || '');
+ }
+ };
+
+ var is_or_has = function (a, b) {
+ /**
+ * Is b a child of a or a itself?
+ */
+ return a.has(b).length !==0 || a.is(b);
+ };
+
+ var is_focused = function (e) {
+ /**
+ * Is element e, or one of its children focused?
+ */
+ e = $(e);
+ var target = $(document.activeElement);
+ if (target.length > 0) {
+ if (is_or_has(e, target)) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ };
+
+ var mergeopt = function(_class, options, overwrite){
+ options = options || {};
+ overwrite = overwrite || {};
+ return $.extend(true, {}, _class.options_default, options, overwrite);
+ };
+
+ var ajax_error_msg = function (jqXHR) {
+ /**
+ * Return a JSON error message if there is one,
+ * otherwise the basic HTTP status text.
+ */
+ if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
+ return jqXHR.responseJSON.traceback;
+ } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
+ return jqXHR.responseJSON.message;
+ } else {
+ return jqXHR.statusText;
+ }
+ };
+ var log_ajax_error = function (jqXHR, status, error) {
+ /**
+ * log ajax failures with informative messages
+ */
+ var msg = "API request failed (" + jqXHR.status + "): ";
+ console.log(jqXHR);
+ msg += ajax_error_msg(jqXHR);
+ console.log(msg);
+ };
+
+ var requireCodeMirrorMode = function (mode, callback, errback) {
+ /**
+ * find a predefined mode or detect from CM metadata then
+ * require and callback with the resolveable mode string: mime or
+ * custom name
+ */
+
+ var modename = (typeof mode == "string") ? mode :
+ mode.mode || mode.name;
+
+ // simplest, cheapest check by mode name: mode may also have config
+ if (CodeMirror.modes.hasOwnProperty(modename)) {
+ // return the full mode object, if it has a name
+ callback(mode.name ? mode : modename);
+ return;
+ }
+
+ // *somehow* get back a CM.modeInfo-like object that has .mode and
+ // .mime
+ var info = (mode && mode.mode && mode.mime && mode) ||
+ CodeMirror.findModeByName(modename) ||
+ CodeMirror.findModeByExtension(modename.split(".").slice(-1)) ||
+ CodeMirror.findModeByMIME(modename) ||
+ {mode: modename, mime: modename};
+
+ require([
+ // might want to use CodeMirror.modeURL here
+ ['codemirror/mode', info.mode, info.mode].join('/'),
+ ], function() {
+ // return the original mode, as from a kernelspec on first load
+ // or the mimetype, as for most highlighting
+ callback(mode.name ? mode : info.mime);
+ }, errback
+ );
+ };
+
+ /** Error type for wrapped XHR errors. */
+ var XHR_ERROR = 'XhrError';
+
+ /**
+ * Wraps an AJAX error as an Error object.
+ */
+ var wrap_ajax_error = function (jqXHR, status, error) {
+ var wrapped_error = new Error(ajax_error_msg(jqXHR));
+ wrapped_error.name = XHR_ERROR;
+ // provide xhr response
+ wrapped_error.xhr = jqXHR;
+ wrapped_error.xhr_status = status;
+ wrapped_error.xhr_error = error;
+ return wrapped_error;
+ };
+
+ var promising_ajax = function(url, settings) {
+ /**
+ * Like $.ajax, but returning an ES6 promise. success and error settings
+ * will be ignored.
+ */
+ settings = settings || {};
+ return new Promise(function(resolve, reject) {
+ settings.success = function(data, status, jqXHR) {
+ resolve(data);
+ };
+ settings.error = function(jqXHR, status, error) {
+ log_ajax_error(jqXHR, status, error);
+ reject(wrap_ajax_error(jqXHR, status, error));
+ };
+ $.ajax(url, settings);
+ });
+ };
+
+ var WrappedError = function(message, error){
+ /**
+ * Wrappable Error class
+ *
+ * The Error class doesn't actually act on `this`. Instead it always
+ * returns a new instance of Error. Here we capture that instance so we
+ * can apply it's properties to `this`.
+ */
+ var tmp = Error.apply(this, [message]);
+
+ // Copy the properties of the error over to this.
+ var properties = Object.getOwnPropertyNames(tmp);
+ for (var i = 0; i < properties.length; i++) {
+ this[properties[i]] = tmp[properties[i]];
+ }
+
+ // Keep a stack of the original error messages.
+ if (error instanceof WrappedError) {
+ this.error_stack = error.error_stack;
+ } else {
+ this.error_stack = [error];
+ }
+ this.error_stack.push(tmp);
+
+ return this;
+ };
+
+ WrappedError.prototype = Object.create(Error.prototype, {});
+
+
+ var load_class = function(class_name, module_name, registry) {
+ /**
+ * Tries to load a class
+ *
+ * Tries to load a class from a module using require.js, if a module
+ * is specified, otherwise tries to load a class from the global
+ * registry, if the global registry is provided.
+ */
+ return new Promise(function(resolve, reject) {
+
+ // Try loading the view module using require.js
+ if (module_name) {
+ require([module_name], function(module) {
+ if (module[class_name] === undefined) {
+ reject(new Error('Class '+class_name+' not found in module '+module_name));
+ } else {
+ resolve(module[class_name]);
+ }
+ }, reject);
+ } else {
+ if (registry && registry[class_name]) {
+ resolve(registry[class_name]);
+ } else {
+ reject(new Error('Class '+class_name+' not found in registry '));
+ }
+ }
+ });
+ };
+
+ var resolve_promises_dict = function(d) {
+ /**
+ * Resolve a promiseful dictionary.
+ * Returns a single Promise.
+ */
+ var keys = Object.keys(d);
+ var values = [];
+ keys.forEach(function(key) {
+ values.push(d[key]);
+ });
+ return Promise.all(values).then(function(v) {
+ d = {};
+ for(var i=0; i<keys.length; i++) {
+ d[keys[i]] = v[i];
+ }
+ return d;
+ });
+ };
+
+ var reject = function(message, log) {
+ /**
+ * Creates a wrappable Promise rejection function.
+ *
+ * Creates a function that returns a Promise.reject with a new WrappedError
+ * that has the provided message and wraps the original error that
+ * caused the promise to reject.
+ */
+ return function(error) {
+ var wrapped_error = new WrappedError(message, error);
+ if (log) {
+ console.error(message, " -- ", error);
+ }
+ return Promise.reject(wrapped_error);
+ };
+ };
+
+ var typeset = function(element, text) {
+ /**
+ * Apply MathJax rendering to an element, and optionally set its text
+ *
+ * If MathJax is not available, make no changes.
+ *
+ * Returns the output any number of typeset elements, or undefined if
+ * MathJax was not available.
+ *
+ * Parameters
+ * ----------
+ * element: Node, NodeList, or jQuery selection
+ * text: option string
+ */
+ var $el = element.jquery ? element : $(element);
+ if(arguments.length > 1){
+ $el.text(text);
+ }
+ if(!window.MathJax){
+ return;
+ }
+ return $el.map(function(){
+ // MathJax takes a DOM node: $.map makes `this` the context
+ return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
+ });
+ };
+
+ var time = {};
+ time.milliseconds = {};
+ time.milliseconds.s = 1000;
+ time.milliseconds.m = 60 * time.milliseconds.s;
+ time.milliseconds.h = 60 * time.milliseconds.m;
+ time.milliseconds.d = 24 * time.milliseconds.h;
+
+ time.thresholds = {
+ // moment.js thresholds in milliseconds
+ s: moment.relativeTimeThreshold('s') * time.milliseconds.s,
+ m: moment.relativeTimeThreshold('m') * time.milliseconds.m,
+ h: moment.relativeTimeThreshold('h') * time.milliseconds.h,
+ d: moment.relativeTimeThreshold('d') * time.milliseconds.d,
+ };
+
+ time.timeout_from_dt = function (dt) {
+ /** compute a timeout based on dt
+
+ input and output both in milliseconds
+
+ use moment's relative time thresholds:
+
+ - 10 seconds if in 'seconds ago' territory
+ - 1 minute if in 'minutes ago'
+ - 1 hour otherwise
+ */
+ if (dt < time.thresholds.s) {
+ return 10 * time.milliseconds.s;
+ } else if (dt < time.thresholds.m) {
+ return time.milliseconds.m;
+ } else {
+ return time.milliseconds.h;
+ }
+ };
+
+ var utils = {
+ is_loaded: is_loaded,
+ load_extension: load_extension,
+ load_extensions: load_extensions,
+ filter_extensions: filter_extensions,
+ load_extensions_from_config: load_extensions_from_config,
+ regex_split : regex_split,
+ uuid : uuid,
+ fixConsole : fixConsole,
+ fixCarriageReturn : fixCarriageReturn,
+ autoLinkUrls : autoLinkUrls,
+ points_to_pixels : points_to_pixels,
+ get_body_data : get_body_data,
+ parse_url : parse_url,
+ url_path_split : url_path_split,
+ url_path_join : url_path_join,
+ url_join_encode : url_join_encode,
+ encode_uri_components : encode_uri_components,
+ splitext : splitext,
+ escape_html : escape_html,
+ always_new : always_new,
+ to_absolute_cursor_pos : to_absolute_cursor_pos,
+ from_absolute_cursor_pos : from_absolute_cursor_pos,
+ browser : browser,
+ platform: platform,
+ get_url_param: get_url_param,
+ is_or_has : is_or_has,
+ is_focused : is_focused,
+ mergeopt: mergeopt,
+ ajax_error_msg : ajax_error_msg,
+ log_ajax_error : log_ajax_error,
+ requireCodeMirrorMode : requireCodeMirrorMode,
+ XHR_ERROR : XHR_ERROR,
+ wrap_ajax_error : wrap_ajax_error,
+ promising_ajax : promising_ajax,
+ WrappedError: WrappedError,
+ load_class: load_class,
+ resolve_promises_dict: resolve_promises_dict,
+ reject: reject,
+ typeset: typeset,
+ time: time,
+ _ansispan:_ansispan
+ };
+
+ return utils;
+});
diff --git a/notebook/static/base/less/error.less b/notebook/static/base/less/error.less
new file mode 100644
index 0000000..0a1eadb
--- /dev/null
+++ b/notebook/static/base/less/error.less
@@ -0,0 +1,20 @@
+div.error {
+ margin: 2em;
+ text-align: center;
+}
+
+div.error > h1 {
+ font-size: 500%;
+ line-height: normal;
+}
+
+div.error > p {
+ font-size: 200%;
+ line-height: normal;
+}
+
+div.traceback-wrapper {
+ text-align: left;
+ max-width: 800px;
+ margin: auto;
+}
diff --git a/notebook/static/base/less/flexbox.less b/notebook/static/base/less/flexbox.less
new file mode 100644
index 0000000..62eaad2
--- /dev/null
+++ b/notebook/static/base/less/flexbox.less
@@ -0,0 +1,269 @@
+
+/* Flexible box model classes */
+/* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
+
+/* This file is a compatability layer. It allows the usage of flexible box
+model layouts accross multiple browsers, including older browsers. The newest,
+universal implementation of the flexible box model is used when available (see
+`Modern browsers` comments below). Browsers that are known to implement this
+new spec completely include:
+
+ Firefox 28.0+
+ Chrome 29.0+
+ Internet Explorer 11+
+ Opera 17.0+
+
+Browsers not listed, including Safari, are supported via the styling under the
+`Old browsers` comments below.
+*/
+
+
+.hbox {
+ /* Old browsers */
+ display: -webkit-box;
+ -webkit-box-orient: horizontal;
+ -webkit-box-align: stretch;
+
+ display: -moz-box;
+ -moz-box-orient: horizontal;
+ -moz-box-align: stretch;
+
+ display: box;
+ box-orient: horizontal;
+ box-align: stretch;
+
+ /* Modern browsers */
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+}
+
+.hbox > * {
+ /* Old browsers */
+ -webkit-box-flex: 0;
+ -moz-box-flex: 0;
+ box-flex: 0;
+
+ /* Modern browsers */
+ flex: none;
+}
+
+.vbox {
+ /* Old browsers */
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-box-align: stretch;
+
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ -moz-box-align: stretch;
+
+ display: box;
+ box-orient: vertical;
+ box-align: stretch;
+
+ /* Modern browsers */
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.vbox > * {
+ /* Old browsers */
+ -webkit-box-flex: 0;
+ -moz-box-flex: 0;
+ box-flex: 0;
+
+ /* Modern browsers */
+ flex: none;
+}
+
+.hbox.reverse,
+.vbox.reverse,
+.reverse {
+ /* Old browsers */
+ -webkit-box-direction: reverse;
+ -moz-box-direction: reverse;
+ box-direction: reverse;
+
+ /* Modern browsers */
+ flex-direction: row-reverse;
+}
+
+.hbox.box-flex0,
+.vbox.box-flex0,
+.box-flex0 {
+ /* Old browsers */
+ -webkit-box-flex: 0;
+ -moz-box-flex: 0;
+ box-flex: 0;
+
+ /* Modern browsers */
+ flex: none;
+ width: auto;
+}
+
+.hbox.box-flex1,
+.vbox.box-flex1,
+.box-flex1 {
+ /* Old browsers */
+ -webkit-box-flex: 1;
+ -moz-box-flex: 1;
+ box-flex: 1;
+
+ /* Modern browsers */
+ flex: 1;
+}
+
+.hbox.box-flex,
+.vbox.box-flex,
+.box-flex {
+ /* Old browsers */
+ .box-flex1();
+}
+
+.hbox.box-flex2,
+.vbox.box-flex2,
+.box-flex2 {
+ /* Old browsers */
+ -webkit-box-flex: 2;
+ -moz-box-flex: 2;
+ box-flex: 2;
+
+ /* Modern browsers */
+ flex: 2;
+}
+
+.box-group1 {
+ /* Deprecated */
+ -webkit-box-flex-group: 1;
+ -moz-box-flex-group: 1;
+ box-flex-group: 1;
+}
+
+.box-group2 {
+ /* Deprecated */
+ -webkit-box-flex-group: 2;
+ -moz-box-flex-group: 2;
+ box-flex-group: 2;
+}
+
+.hbox.start,
+.vbox.start,
+.start {
+ /* Old browsers */
+ -webkit-box-pack: start;
+ -moz-box-pack: start;
+ box-pack: start;
+
+ /* Modern browsers */
+ justify-content: flex-start;
+}
+
+.hbox.end,
+.vbox.end,
+.end {
+ /* Old browsers */
+ -webkit-box-pack: end;
+ -moz-box-pack: end;
+ box-pack: end;
+
+ /* Modern browsers */
+ justify-content: flex-end;
+}
+
+.hbox.center,
+.vbox.center,
+.center {
+ /* Old browsers */
+ -webkit-box-pack: center;
+ -moz-box-pack: center;
+ box-pack: center;
+
+ /* Modern browsers */
+ justify-content: center;
+}
+
+.hbox.baseline,
+.vbox.baseline,
+.baseline {
+ /* Old browsers */
+ -webkit-box-pack: baseline;
+ -moz-box-pack: baseline;
+ box-pack: baseline;
+
+ /* Modern browsers */
+ justify-content: baseline;
+}
+
+.hbox.stretch,
+.vbox.stretch,
+.stretch {
+ /* Old browsers */
+ -webkit-box-pack: stretch;
+ -moz-box-pack: stretch;
+ box-pack: stretch;
+
+ /* Modern browsers */
+ justify-content: stretch;
+}
+
+.hbox.align-start,
+.vbox.align-start,
+.align-start {
+ /* Old browsers */
+ -webkit-box-align: start;
+ -moz-box-align: start;
+ box-align: start;
+
+ /* Modern browsers */
+ align-items: flex-start;
+}
+
+.hbox.align-end,
+.vbox.align-end,
+.align-end {
+ /* Old browsers */
+ -webkit-box-align: end;
+ -moz-box-align: end;
+ box-align: end;
+
+ /* Modern browsers */
+ align-items: flex-end;
+}
+
+.hbox.align-center,
+.vbox.align-center,
+.align-center {
+ /* Old browsers */
+ -webkit-box-align: center;
+ -moz-box-align: center;
+ box-align: center;
+
+ /* Modern browsers */
+ align-items: center;
+}
+
+.hbox.align-baseline,
+.vbox.align-baseline,
+.align-baseline {
+ /* Old browsers */
+ -webkit-box-align: baseline;
+ -moz-box-align: baseline;
+ box-align: baseline;
+
+ /* Modern browsers */
+ align-items: baseline;
+}
+
+.hbox.align-stretch,
+.vbox.align-stretch,
+.align-stretch {
+ /* Old browsers */
+ -webkit-box-align: stretch;
+ -moz-box-align: stretch;
+ box-align: stretch;
+
+ /* Modern browsers */
+ align-items: stretch;
+}
diff --git a/notebook/static/base/less/mixins.less b/notebook/static/base/less/mixins.less
new file mode 100644
index 0000000..85fe434
--- /dev/null
+++ b/notebook/static/base/less/mixins.less
@@ -0,0 +1,19 @@
+// Mixin CSS classes
+
+.border-box-sizing {
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+}
+
+.corner-all {
+ border-radius: @border-radius-base;
+}
+
+.border-radius(@radius) {
+ border-radius: @radius;
+}
+
+.no-padding {
+ padding: 0px;
+}
diff --git a/notebook/static/base/less/page.less b/notebook/static/base/less/page.less
new file mode 100644
index 0000000..9d036d4
--- /dev/null
+++ b/notebook/static/base/less/page.less
@@ -0,0 +1,149 @@
+/**
+ * Primary styles
+ *
+ * Author: Jupyter Development Team
+ */
+
+
+body {
+ background-color: @body-bg;
+ /* This makes sure that the body covers the entire window and needs to
+ be in a different element than the display: box in wrapper below */
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ top: 0px;
+ bottom: 0px;
+ overflow: visible;
+}
+
+body > #header {
+ /* Initially hidden to prevent FLOUC */
+ display: none;
+ background-color: @body-bg;
+
+ /* Display over codemirror */
+ position: relative;
+ z-index: 100;
+
+ #header-container {
+ padding-bottom: 5px;
+ padding-top: 5px;
+ .border-box-sizing();
+ }
+
+ .header-bar {
+ width: 100%;
+ height: 1px;
+ background: @navbar-default-border;
+ margin-bottom: -1px;
+ }
+
+ @media print {
+ display: none !important;
+ }
+}
+
+#header-spacer {
+ width: 100%;
+ visibility: hidden;
+
+ @media print {
+ display: none;
+ }
+}
+
+#ipython_notebook {
+ padding-left: 0px;
+ padding-top: (@navbar-height - @logo_height) / 2;
+ padding-bottom: (@navbar-height - @logo_height) / 2;
+ @media (max-width: @screen-sm-max){
+ margin-left: 10px;
+ }
+}
+
+
+
+#noscript {
+ width: auto;
+ padding-top: 16px;
+ padding-bottom: 16px;
+ text-align: center;
+ font-size: 22px;
+ color: red;
+ font-weight: bold;
+}
+
+#ipython_notebook img {
+ height: @logo_height;
+}
+
+#site {
+ width: 100%;
+ display: none;
+ .border-box-sizing();
+ overflow: auto;
+ @media print {
+ // force auto-height on print (overrides manual resizing in live view)
+ height: auto !important;
+ }
+}
+
+/* Smaller buttons */
+.ui-button .ui-button-text {
+ padding: 0.2em 0.8em;
+ font-size: 77%;
+}
+
+input.ui-button {
+ padding: 0.3em 0.9em;
+}
+
+span#login_widget {
+ float: right;
+}
+
+span#login_widget > .button,
+#logout
+{
+ .btn-default();
+}
+
+.nav-header {
+ text-transform: none;
+}
+
+#header > span {
+ margin-top: 10px;
+}
+
+// class for stretching dialogs to fill the screen
+.modal_stretch .modal-dialog {
+ .vbox();
+ min-height: 80vh;
+ .modal-body {
+ // ~"foo" is to avoid less turning this into a weird value
+ max-height: calc(~"100vh - 200px");
+ overflow: auto;
+ flex: 1;
+ }
+}
+
+@media (min-width: @screen-sm-min) {
+ .modal .modal-dialog {
+ width: 700px;
+ }
+}
+
+// less mixin to be sure to add the right class to get icons with font awesome.
+.icon(@ico){
+ .fa();
+ content: @ico;
+}
+
+@media (min-width: @screen-sm-min) {
+ select.form-control {
+ margin-left: @padding-base-horizontal;
+ margin-right: @padding-base-horizontal;
+ }
+}
diff --git a/notebook/static/base/less/style.less b/notebook/static/base/less/style.less
new file mode 100644
index 0000000..cc77da9
--- /dev/null
+++ b/notebook/static/base/less/style.less
@@ -0,0 +1,9 @@
+/*!
+*
+* IPython base
+*
+*/
+@import "variables.less";
+@import "mixins.less";
+@import "flexbox.less";
+@import "error.less";
diff --git a/notebook/static/base/less/variables.less b/notebook/static/base/less/variables.less
new file mode 100644
index 0000000..4cddd08
--- /dev/null
+++ b/notebook/static/base/less/variables.less
@@ -0,0 +1,62 @@
+// Our customizations to bootstrap go here.
+
+@black: #000;
+@text-color: @black;
+@font-size-base: 13px;
+@font-family-monospace: monospace; // to allow user to customize their fonts
+@navbar-height: 30px;
+@breadcrumb-color: darken(@border_color, 30%);
+@blockquote-font-size: inherit;
+@modal-inner-padding: 15px;
+@grid-float-breakpoint: 541px;
+@screen-xs: 540px;
+@logo_height: 28px;
+@border-radius-small: 1px;
+@border-radius-base: 2px;
+@border-radius-large: 3px;
+@grid-gutter-width: 0px;
+@kbd-color: #888;
+@kbd-bg: transparent;
+
+@icon-font-path: "../components/bootstrap/fonts/";
+
+// Disable modal slide-in from top animation.
+.modal {
+ &.fade .modal-dialog {
+ .translate(0, 0);
+ }
+}
+
+// Set the default code color.
+code {
+ color: @black; // default code color in bootstrap is #d14 (crimson / amaranth)
+}
+
+// Override bootstrap pre element styling.
+pre {
+ // bootstrap has pre defaults that we don't want to inherit.
+ // start pre tag defaults based on the surrounding context instead.
+ font-size: inherit;
+ line-height: inherit;
+}
+
+// Disable bold labels in BS3
+label {
+ font-weight: normal;
+}
+
+// Our own global variables for all pages go here
+@global-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2);
+@global-shadow-dark: 0px 0px 12px 1px rgba(87, 87, 87, 0.4);
+@page-header-padding: 20px;
+/* Make the page background atleast 100% the height of the view port */
+@page-backdrop-height: 100vh;
+/* Make the page itself atleast 70% the height of the view port */
+@page-min-height: 0;
+@page-backdrop-color: #EEE;
+@page-color: @body-bg;
+@page-padding: 15px;
+
+// preven container size to jump from 768px to 720px
+// when window width go from 768 to 769+
+@container-sm : @screen-sm-min;
diff --git a/notebook/static/custom/custom.css b/notebook/static/custom/custom.css
new file mode 100644
index 0000000..9f4abda
--- /dev/null
+++ b/notebook/static/custom/custom.css
@@ -0,0 +1,7 @@
+/*
+Placeholder for custom user CSS
+
+mainly to be overridden in profile/static/custom/custom.css
+
+This will always be an empty file in IPython
+*/ \ No newline at end of file
diff --git a/notebook/static/custom/custom.js b/notebook/static/custom/custom.js
new file mode 100644
index 0000000..05aa9ae
--- /dev/null
+++ b/notebook/static/custom/custom.js
@@ -0,0 +1,82 @@
+// leave at least 2 line with only a star on it below, or doc generation fails
+/**
+ *
+ *
+ * Placeholder for custom user javascript
+ * mainly to be overridden in profile/static/custom/custom.js
+ * This will always be an empty file in IPython
+ *
+ * User could add any javascript in the `profile/static/custom/custom.js` file.
+ * It will be executed by the ipython notebook at load time.
+ *
+ * Same thing with `profile/static/custom/custom.css` to inject custom css into the notebook.
+ *
+ *
+ * The object available at load time depend on the version of IPython in use.
+ * there is no guaranties of API stability.
+ *
+ * The example below explain the principle, and might not be valid.
+ *
+ * Instances are created after the loading of this file and might need to be accessed using events:
+ * define([
+ * 'base/js/namespace',
+ * 'base/js/events'
+ * ], function(IPython, events) {
+ * events.on("app_initialized.NotebookApp", function () {
+ * IPython.keyboard_manager....
+ * });
+ * });
+ *
+ * __Example 1:__
+ *
+ * Create a custom button in toolbar that execute `%qtconsole` in kernel
+ * and hence open a qtconsole attached to the same kernel as the current notebook
+ *
+ * define([
+ * 'base/js/namespace',
+ * 'base/js/events'
+ * ], function(IPython, events) {
+ * events.on('app_initialized.NotebookApp', function(){
+ * IPython.toolbar.add_buttons_group([
+ * {
+ * 'label' : 'run qtconsole',
+ * 'icon' : 'icon-terminal', // select your icon from http://fortawesome.github.io/Font-Awesome/icons
+ * 'callback': function () {
+ * IPython.notebook.kernel.execute('%qtconsole')
+ * }
+ * }
+ * // add more button here if needed.
+ * ]);
+ * });
+ * });
+ *
+ * __Example 2:__
+ *
+ * At the completion of the dashboard loading, load an unofficial javascript extension
+ * that is installed in profile/static/custom/
+ *
+ * define([
+ * 'base/js/events'
+ * ], function(events) {
+ * events.on('app_initialized.DashboardApp', function(){
+ * require(['custom/unofficial_extension.js'])
+ * });
+ * });
+ *
+ * __Example 3:__
+ *
+ * Use `jQuery.getScript(url [, success(script, textStatus, jqXHR)] );`
+ * to load custom script into the notebook.
+ *
+ * // to load the metadata ui extension example.
+ * $.getScript('/static/notebook/js/celltoolbarpresets/example.js');
+ * // or
+ * // to load the metadata ui extension to control slideshow mode / reveal js for nbconvert
+ * $.getScript('/static/notebook/js/celltoolbarpresets/slideshow.js');
+ *
+ *
+ * @module IPython
+ * @namespace IPython
+ * @class customjs
+ * @static
+ */
diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js
new file mode 100644
index 0000000..d27da43
--- /dev/null
+++ b/notebook/static/edit/js/editor.js
@@ -0,0 +1,220 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'codemirror/lib/codemirror',
+ 'codemirror/mode/meta',
+ 'codemirror/addon/comment/comment',
+ 'codemirror/addon/dialog/dialog',
+ 'codemirror/addon/edit/closebrackets',
+ 'codemirror/addon/edit/matchbrackets',
+ 'codemirror/addon/search/searchcursor',
+ 'codemirror/addon/search/search',
+ 'codemirror/keymap/emacs',
+ 'codemirror/keymap/sublime',
+ 'codemirror/keymap/vim',
+ ],
+function($,
+ utils,
+ CodeMirror
+) {
+ "use strict";
+
+ var Editor = function(selector, options) {
+ var that = this;
+ this.selector = selector;
+ this.clean = false;
+ this.contents = options.contents;
+ this.events = options.events;
+ this.base_url = options.base_url;
+ this.file_path = options.file_path;
+ this.config = options.config;
+ this.codemirror = new CodeMirror($(this.selector)[0]);
+ this.codemirror.on('changes', function(cm, changes){
+ that._clean_state();
+ });
+ this.generation = -1;
+
+ // It appears we have to set commands on the CodeMirror class, not the
+ // instance. I'd like to be wrong, but since there should only be one CM
+ // instance on the page, this is good enough for now.
+ CodeMirror.commands.save = $.proxy(this.save, this);
+
+ this.save_enabled = false;
+
+ this.config.loaded.then(function () {
+ // load codemirror config
+ var cfg = that.config.data.Editor || {};
+ var cmopts = $.extend(true, {}, // true = recursive copy
+ Editor.default_codemirror_options,
+ cfg.codemirror_options || {}
+ );
+ that._set_codemirror_options(cmopts);
+ that.events.trigger('config_changed.Editor', {config: that.config});
+ that._clean_state();
+ });
+ this.clean_sel = $('<div/>');
+ $('.last_modified').before(this.clean_sel);
+ this.clean_sel.addClass('dirty-indicator-dirty');
+ };
+
+ // default CodeMirror options
+ Editor.default_codemirror_options = {
+ extraKeys: {
+ "Tab" : "indentMore",
+ },
+ indentUnit: 4,
+ theme: "ipython",
+ lineNumbers: true,
+ lineWrapping: true,
+ };
+
+ Editor.prototype.load = function() {
+ /** load the file */
+ var that = this;
+ var cm = this.codemirror;
+ return this.contents.get(this.file_path, {type: 'file', format: 'text'})
+ .then(function(model) {
+ cm.setValue(model.content);
+
+ // Setting the file's initial value creates a history entry,
+ // which we don't want.
+ cm.clearHistory();
+ that._set_mode_for_model(model);
+ that.save_enabled = true;
+ that.generation = cm.changeGeneration();
+ that.events.trigger("file_loaded.Editor", model);
+ that._clean_state();
+ }).catch(
+ function(error) {
+ that.events.trigger("file_load_failed.Editor", error);
+ console.warn('Error loading: ', error);
+ cm.setValue("Error! " + error.message +
+ "\nSaving disabled.\nSee Console for more details.");
+ cm.setOption('readOnly','nocursor');
+ that.save_enabled = false;
+ }
+ );
+ };
+
+ Editor.prototype._set_mode_for_model = function (model) {
+ /** Set the CodeMirror mode based on the file model */
+
+ // Find and load the highlighting mode,
+ // first by mime-type, then by file extension
+
+ var modeinfo;
+ // mimetype is unset on file rename
+ if (model.mimetype) {
+ modeinfo = CodeMirror.findModeByMIME(model.mimetype);
+ }
+ if (!modeinfo || modeinfo.mode === "null") {
+ // find by mime failed, use find by ext
+ var ext_idx = model.name.lastIndexOf('.');
+
+ if (ext_idx > 0) {
+ // CodeMirror.findModeByExtension wants extension without '.'
+ modeinfo = CodeMirror.findModeByExtension(
+ model.name.slice(ext_idx + 1).toLowerCase());
+ }
+ }
+ if (modeinfo) {
+ this.set_codemirror_mode(modeinfo);
+ }
+ };
+
+ Editor.prototype.set_codemirror_mode = function (modeinfo) {
+ /** set the codemirror mode from a modeinfo struct */
+ var that = this;
+ utils.requireCodeMirrorMode(modeinfo, function () {
+ that.codemirror.setOption('mode', modeinfo.mode);
+ that.events.trigger("mode_changed.Editor", modeinfo);
+ });
+ };
+
+ Editor.prototype.get_filename = function () {
+ return utils.url_path_split(this.file_path)[1];
+ };
+
+ Editor.prototype.rename = function (new_name) {
+ /** rename the file */
+ var that = this;
+ var parent = utils.url_path_split(this.file_path)[0];
+ var new_path = utils.url_path_join(parent, new_name);
+ return this.contents.rename(this.file_path, new_path).then(
+ function (model) {
+ that.file_path = model.path;
+ that.events.trigger('file_renamed.Editor', model);
+ that._set_mode_for_model(model);
+ that._clean_state();
+ }
+ );
+ };
+
+ Editor.prototype.save = function () {
+ /** save the file */
+ if (!this.save_enabled) {
+ console.log("Not saving, save disabled");
+ return;
+ }
+ var model = {
+ path: this.file_path,
+ type: 'file',
+ format: 'text',
+ content: this.codemirror.getValue(),
+ };
+ var that = this;
+ // record change generation for isClean
+ this.generation = this.codemirror.changeGeneration();
+ that.events.trigger("file_saving.Editor");
+ return this.contents.save(this.file_path, model).then(function(data) {
+ that.events.trigger("file_saved.Editor", data);
+ that._clean_state();
+ });
+ };
+
+ Editor.prototype._clean_state = function(){
+ var clean = this.codemirror.isClean(this.generation);
+ if (clean === this.clean){
+ return;
+ } else {
+ this.clean = clean;
+ }
+ if(clean){
+ this.events.trigger("save_status_clean.Editor");
+ this.clean_sel.attr('class','dirty-indicator-clean').attr('title','No changes to save');
+ } else {
+ this.events.trigger("save_status_dirty.Editor");
+ this.clean_sel.attr('class','dirty-indicator-dirty').attr('title','Unsaved changes');
+ }
+ };
+
+ Editor.prototype._set_codemirror_options = function (options) {
+ // update codemirror options from a dict
+ var codemirror = this.codemirror;
+ $.map(options, function (value, opt) {
+ if (value === null) {
+ value = CodeMirror.defaults[opt];
+ }
+ codemirror.setOption(opt, value);
+ });
+ var that = this;
+ };
+
+ Editor.prototype.update_codemirror_options = function (options) {
+ /** update codemirror options locally and save changes in config */
+ var that = this;
+ this._set_codemirror_options(options);
+ return this.config.update({
+ Editor: {
+ codemirror_options: options
+ }
+ }).then(
+ that.events.trigger('config_changed.Editor', {config: that.config})
+ );
+ };
+
+ return {Editor: Editor};
+});
diff --git a/notebook/static/edit/js/main.js b/notebook/static/edit/js/main.js
new file mode 100644
index 0000000..73cd65e
--- /dev/null
+++ b/notebook/static/edit/js/main.js
@@ -0,0 +1,98 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+require([
+ 'jquery',
+ 'base/js/namespace',
+ 'base/js/utils',
+ 'base/js/page',
+ 'base/js/events',
+ 'contents',
+ 'services/config',
+ 'edit/js/editor',
+ 'edit/js/menubar',
+ 'edit/js/savewidget',
+ 'edit/js/notificationarea',
+ 'custom',
+], function(
+ $,
+ IPython,
+ utils,
+ page,
+ events,
+ contents,
+ configmod,
+ editmod,
+ menubar,
+ savewidget,
+ notificationarea
+ ){
+ "use strict";
+
+ page = new page.Page();
+
+ var base_url = utils.get_body_data('baseUrl');
+ var file_path = utils.get_body_data('filePath');
+ var config = new configmod.ConfigSection('edit', {base_url: base_url});
+ config.load();
+ var common_config = new configmod.ConfigSection('common', {base_url: base_url});
+ common_config.load();
+ contents = new contents.Contents({
+ base_url: base_url,
+ common_config: common_config
+ });
+
+ var editor = new editmod.Editor('#texteditor-container', {
+ base_url: base_url,
+ events: events,
+ contents: contents,
+ file_path: file_path,
+ config: config,
+ });
+
+ // Make it available for debugging
+ IPython.editor = editor;
+
+ var save_widget = new savewidget.SaveWidget('span#save_widget', {
+ editor: editor,
+ events: events,
+ });
+
+ var menus = new menubar.MenuBar('#menubar', {
+ base_url: base_url,
+ editor: editor,
+ events: events,
+ save_widget: save_widget,
+ });
+
+ var notification_area = new notificationarea.EditorNotificationArea(
+ '#notification_area', {
+ events: events,
+ });
+ editor.notification_area = notification_area;
+ notification_area.init_notification_widgets();
+
+ utils.load_extensions_from_config(config);
+ utils.load_extensions_from_config(common_config);
+ editor.load();
+ page.show();
+
+ window.onbeforeunload = function () {
+ if (editor.save_enabled && !editor.codemirror.isClean(editor.generation)) {
+ return "Unsaved changes will be lost. Close anyway?";
+ }
+ };
+
+ // Make sure the codemirror editor is sized appropriatley.
+ var _handle_resize = function() {
+ var backdrop = $("#texteditor-backdrop");
+
+ // account for padding on the backdrop wrapper
+ var padding = backdrop.outerHeight(true) - backdrop.height();
+ $('div.CodeMirror').height($("#site").height() - padding);
+ };
+ $(window).resize(_handle_resize);
+
+ // On document ready, resize codemirror.
+ $(document).ready(_handle_resize);
+});
diff --git a/notebook/static/edit/js/menubar.js b/notebook/static/edit/js/menubar.js
new file mode 100644
index 0000000..ddb1ed7
--- /dev/null
+++ b/notebook/static/edit/js/menubar.js
@@ -0,0 +1,166 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/namespace',
+ 'base/js/utils',
+ 'base/js/dialog',
+ 'codemirror/lib/codemirror',
+ 'codemirror/mode/meta',
+ 'bootstrap',
+], function($, IPython, utils, dialog, CodeMirror) {
+ "use strict";
+
+ var MenuBar = function (selector, options) {
+ /**
+ * Constructor
+ *
+ * A MenuBar Class to generate the menubar of IPython notebook
+ *
+ * Parameters:
+ * selector: string
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * codemirror: CodeMirror instance
+ * contents: ContentManager instance
+ * events: $(Events) instance
+ * base_url : string
+ * file_path : string
+ */
+ options = options || {};
+ this.base_url = options.base_url || utils.get_body_data("baseUrl");
+ this.selector = selector;
+ this.editor = options.editor;
+ this.events = options.events;
+ this.save_widget = options.save_widget;
+
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.bind_events();
+ }
+ this._load_mode_menu();
+ Object.seal(this);
+ };
+
+ MenuBar.prototype.bind_events = function () {
+ var that = this;
+ var editor = that.editor;
+
+ // File
+ this.element.find('#new-file').click(function () {
+ var w = window.open(undefined, IPython._target);
+ // Create a new file in the current directory
+ var parent = utils.url_path_split(editor.file_path)[0];
+ editor.contents.new_untitled(parent, {type: "file"}).then(
+ function (data) {
+ w.location = utils.url_path_join(
+ that.base_url, 'edit', utils.encode_uri_components(data.path)
+ );
+ },
+ function(error) {
+ w.close();
+ dialog.modal({
+ title : 'Creating New File Failed',
+ body : "The error was: " + error.message,
+ buttons : {'OK' : {'class' : 'btn-primary'}}
+ });
+ }
+ );
+ });
+ this.element.find('#save-file').click(function () {
+ editor.save();
+ });
+ this.element.find('#rename-file').click(function () {
+ that.save_widget.rename();
+ });
+ this.element.find('#download-file').click(function () {
+ window.open(utils.url_path_join(
+ that.base_url, 'files',
+ utils.encode_uri_components(that.editor.file_path)
+ ) + '?download=1');
+ });
+
+ // Edit
+ this.element.find('#menu-find').click(function () {
+ editor.codemirror.execCommand("find");
+ });
+ this.element.find('#menu-replace').click(function () {
+ editor.codemirror.execCommand("replace");
+ });
+ this.element.find('#menu-keymap-default').click(function () {
+ editor.update_codemirror_options({
+ vimMode: false,
+ keyMap: 'default'
+ });
+ });
+ this.element.find('#menu-keymap-sublime').click(function () {
+ editor.update_codemirror_options({
+ vimMode: false,
+ keyMap: 'sublime'
+ });
+ });
+ this.element.find('#menu-keymap-emacs').click(function () {
+ editor.update_codemirror_options({
+ vimMode: false,
+ keyMap: 'emacs'
+ });
+ });
+ this.element.find('#menu-keymap-vim').click(function () {
+ editor.update_codemirror_options({
+ vimMode: true,
+ keyMap: 'vim'
+ });
+ });
+
+ // View
+
+ this.element.find('#toggle_header').click(function (){
+ $("#header-container").toggle();
+ });
+
+ this.element.find('#menu-line-numbers').click(function () {
+ var current = editor.codemirror.getOption('lineNumbers');
+ var value = Boolean(1-current);
+ editor.update_codemirror_options({lineNumbers: value});
+ });
+
+ this.events.on("config_changed.Editor", function () {
+ var keyMap = editor.codemirror.getOption('keyMap') || "default";
+ that.element.find(".selected-keymap").removeClass("selected-keymap");
+ that.element.find("#menu-keymap-" + keyMap).addClass("selected-keymap");
+ });
+
+ this.events.on("mode_changed.Editor", function (evt, modeinfo) {
+ that.element.find("#current-mode")
+ .text(modeinfo.name)
+ .attr(
+ 'title',
+ "The current language is " + modeinfo.name
+ );
+ });
+ };
+
+ MenuBar.prototype._load_mode_menu = function () {
+ var list = this.element.find("#mode-menu");
+ var editor = this.editor;
+ function make_set_mode(info) {
+ return function () {
+ editor.set_codemirror_mode(info);
+ };
+ }
+ for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
+ var info = CodeMirror.modeInfo[i];
+ list.append($("<li>").append(
+ $("<a>").attr("href", "#")
+ .text(info.name)
+ .click(make_set_mode(info))
+ .attr('title',
+ "Set language to " + info.name
+ )
+ ));
+ }
+ };
+
+ return {'MenuBar': MenuBar};
+});
diff --git a/notebook/static/edit/js/notificationarea.js b/notebook/static/edit/js/notificationarea.js
new file mode 100644
index 0000000..73f7077
--- /dev/null
+++ b/notebook/static/edit/js/notificationarea.js
@@ -0,0 +1,29 @@
+define([
+ 'base/js/notificationarea'
+], function(notificationarea) {
+ "use strict";
+ var NotificationArea = notificationarea.NotificationArea;
+
+ var EditorNotificationArea = function(selector, options) {
+ NotificationArea.apply(this, [selector, options]);
+ }
+
+ EditorNotificationArea.prototype = Object.create(NotificationArea.prototype);
+
+ /**
+ * Initialize the default set of notification widgets.
+ *
+ * @method init_notification_widgets
+ */
+ EditorNotificationArea.prototype.init_notification_widgets = function () {
+ var that = this;
+ var savew = this.new_notification_widget('save');
+
+ this.events.on("file_saved.Editor", function() {
+ savew.set_message("File saved", 2000);
+ });
+ };
+
+
+ return {EditorNotificationArea: EditorNotificationArea};
+});
diff --git a/notebook/static/edit/js/savewidget.js b/notebook/static/edit/js/savewidget.js
new file mode 100644
index 0000000..7d6d38b
--- /dev/null
+++ b/notebook/static/edit/js/savewidget.js
@@ -0,0 +1,184 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/dialog',
+ 'base/js/keyboard',
+ 'moment',
+], function($, utils, dialog, keyboard, moment) {
+ "use strict";
+
+ var SaveWidget = function (selector, options) {
+ this.editor = undefined;
+ this.selector = selector;
+ this.events = options.events;
+ this.editor = options.editor;
+ this._last_modified = undefined;
+ this._filename = undefined;
+ this.keyboard_manager = options.keyboard_manager;
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.bind_events();
+ }
+ };
+
+
+ SaveWidget.prototype.bind_events = function () {
+ var that = this;
+ this.element.find('span.filename').click(function () {
+ that.rename();
+ });
+ this.events.on('save_status_clean.Editor', function (evt) {
+ that.update_document_title();
+ });
+ this.events.on('save_status_dirty.Editor', function (evt) {
+ that.update_document_title(undefined, true);
+ });
+ this.events.on('file_loaded.Editor', function (evt, model) {
+ that.update_filename(model.name);
+ that.update_document_title(model.name);
+ that.update_last_modified(model.last_modified);
+ });
+ this.events.on('file_saved.Editor', function (evt, model) {
+ that.update_filename(model.name);
+ that.update_document_title(model.name);
+ that.update_last_modified(model.last_modified);
+ });
+ this.events.on('file_renamed.Editor', function (evt, model) {
+ that.update_filename(model.name);
+ that.update_document_title(model.name);
+ that.update_address_bar(model.path);
+ });
+ this.events.on('file_save_failed.Editor', function () {
+ that.set_save_status('Save Failed!');
+ });
+ };
+
+
+ SaveWidget.prototype.rename = function (options) {
+ options = options || {};
+ var that = this;
+ var dialog_body = $('<div/>').append(
+ $("<p/>").addClass("rename-message")
+ .text('Enter a new filename:')
+ ).append(
+ $("<br/>")
+ ).append(
+ $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
+ .val(that.editor.get_filename())
+ );
+ var d = dialog.modal({
+ title: "Rename File",
+ body: dialog_body,
+ buttons : {
+ "OK": {
+ class: "btn-primary",
+ click: function () {
+ var new_name = d.find('input').val();
+ d.find('.rename-message').text("Renaming...");
+ d.find('input[type="text"]').prop('disabled', true);
+ that.editor.rename(new_name).then(
+ function () {
+ d.modal('hide');
+ }, function (error) {
+ d.find('.rename-message').text(error.message || 'Unknown error');
+ d.find('input[type="text"]').prop('disabled', false).focus().select();
+ }
+ );
+ return false;
+ }
+ },
+ "Cancel": {}
+ },
+ open : function () {
+ // Upon ENTER, click the OK button.
+ d.find('input[type="text"]').keydown(function (event) {
+ if (event.which === keyboard.keycodes.enter) {
+ d.find('.btn-primary').first().click();
+ return false;
+ }
+ });
+ d.find('input[type="text"]').focus().select();
+ }
+ });
+ };
+
+
+ SaveWidget.prototype.update_filename = function (filename) {
+ this.element.find('span.filename').text(filename);
+ };
+
+ SaveWidget.prototype.update_document_title = function (filename, dirty) {
+ if(filename){
+ this._filename = filename;
+ }
+ document.title = (dirty?'*':'')+this._filename;
+ };
+
+ SaveWidget.prototype.update_address_bar = function (path) {
+ var state = {path : path};
+ window.history.replaceState(state, "", utils.url_path_join(
+ this.editor.base_url,
+ "edit",
+ utils.encode_uri_components(path)
+ ));
+ };
+
+ SaveWidget.prototype.update_last_modified = function (last_modified) {
+ if (last_modified) {
+ this._last_modified = new Date(last_modified);
+ } else {
+ this._last_modified = null;
+ }
+ this._render_last_modified();
+ };
+
+ SaveWidget.prototype._render_last_modified = function () {
+ /** actually set the text in the element, from our _last_modified value
+
+ called directly, and periodically in timeouts.
+ */
+ this._schedule_render_last_modified();
+ var el = this.element.find('span.last_modified');
+ if (!this._last_modified) {
+ el.text('').attr('title', 'never saved');
+ return;
+ }
+ var chkd = moment(this._last_modified);
+ var long_date = chkd.format('llll');
+ var human_date;
+ var tdelta = Math.ceil(new Date() - this._last_modified);
+ if (tdelta < utils.time.milliseconds.d){
+ // less than 24 hours old, use relative date
+ human_date = chkd.fromNow();
+ } else {
+ // otherwise show calendar
+ // <Today | yesterday|...> at hh,mm,ss
+ human_date = chkd.calendar();
+ }
+ el.text(human_date).attr('title', long_date);
+ };
+
+ SaveWidget.prototype._schedule_render_last_modified = function () {
+ /** schedule the next update to relative date
+
+ periodically updated, so short values like 'a few seconds ago' don't get stale.
+ */
+ if (!this._last_modified) {
+ return;
+ }
+ if ((this._last_modified_timeout)) {
+ clearTimeout(this._last_modified_timeout);
+ }
+ var dt = Math.ceil(new Date() - this._last_modified);
+ this._last_modified_timeout = setTimeout(
+ $.proxy(this._render_last_modified, this),
+ utils.time.timeout_from_dt(dt)
+ );
+ };
+
+ return {'SaveWidget': SaveWidget};
+
+});
diff --git a/notebook/static/edit/less/edit.less b/notebook/static/edit/less/edit.less
new file mode 100644
index 0000000..972da2b
--- /dev/null
+++ b/notebook/static/edit/less/edit.less
@@ -0,0 +1,51 @@
+.dirty-indicator{
+ .fa();
+ width:20px;
+}
+.dirty-indicator-dirty{
+ .dirty-indicator();
+}
+
+.dirty-indicator-clean{
+ .dirty-indicator();
+ &:before{
+ .icon(@fa-var-check);
+ }
+}
+
+#filename {
+ font-size: 16pt;
+ display: table;
+ padding: 0px 5px;
+}
+
+#current-mode{
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+#texteditor-backdrop {
+ padding-top: @page-header-padding;
+ padding-bottom: @page-header-padding;
+
+ @media not print{
+ background-color: @page-backdrop-color;
+ }
+
+ #texteditor-container {
+ .CodeMirror-gutter, .CodeMirror-gutters {
+ @media print {
+ background-color: @body-bg;
+ }
+ @media not print {
+ background-color: @page-color;
+ }
+ }
+
+ @media not print{
+ padding: 0px;
+ background-color : @page-color;
+ .box-shadow(@global-shadow);
+ }
+ }
+}
diff --git a/notebook/static/edit/less/menubar.less b/notebook/static/edit/less/menubar.less
new file mode 100644
index 0000000..c314f31
--- /dev/null
+++ b/notebook/static/edit/less/menubar.less
@@ -0,0 +1,26 @@
+.selected-keymap {
+ i.fa {
+ padding: 0px 5px;
+ }
+ i.fa:before {
+ content: @fa-var-check;
+ }
+}
+
+#mode-menu {
+ // truncate mode-menu, so it doesn't get longer than the screen
+ overflow: auto;
+ max-height: 20em;
+}
+
+.edit_app {
+ #header {
+ .box-shadow(@global-shadow);
+ }
+
+ #menubar .navbar {
+ /* Use a negative 1 bottom margin, so the border overlaps the border of the
+ header */
+ margin-bottom: -1px;
+ }
+}
diff --git a/notebook/static/edit/less/style.less b/notebook/static/edit/less/style.less
new file mode 100644
index 0000000..b365b93
--- /dev/null
+++ b/notebook/static/edit/less/style.less
@@ -0,0 +1,7 @@
+/*!
+*
+* IPython text editor webapp
+*
+*/
+@import "menubar.less";
+@import "edit.less";
diff --git a/notebook/static/notebook/css/override.css b/notebook/static/notebook/css/override.css
new file mode 100644
index 0000000..117fb7c
--- /dev/null
+++ b/notebook/static/notebook/css/override.css
@@ -0,0 +1,7 @@
+/*This file contains any manual css for this page that needs to override the global styles.
+This is only required when different pages style the same element differently. This is just
+a hack to deal with our current css styles and no new styling should be added in this file.*/
+
+#ipython-main-app {
+ position: relative;
+}
diff --git a/notebook/static/notebook/js/about.js b/notebook/static/notebook/js/about.js
new file mode 100644
index 0000000..fe977bb
--- /dev/null
+++ b/notebook/static/notebook/js/about.js
@@ -0,0 +1,46 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+require([
+ 'jquery',
+ 'base/js/dialog',
+ 'underscore',
+ 'base/js/namespace'
+], function ($, dialog, _, IPython) {
+ 'use strict';
+ $('#notebook_about').click(function () {
+ // use underscore template to auto html escape
+ if (sys_info) {
+ var text = 'You are using Jupyter notebook.<br/><br/>';
+ text = text + 'The version of the notebook server is ';
+ text = text + _.template('<b><%- version %></b>')({ version: sys_info.notebook_version });
+ if (sys_info.commit_hash) {
+ text = text + _.template('-<%- hash %>')({ hash: sys_info.commit_hash });
+ }
+ text = text + _.template(' and is running on:<br/><pre>Python <%- pyver %></pre>')({
+ pyver: sys_info.sys_version });
+ var kinfo = $('<div/>').attr('id', '#about-kinfo').text('Waiting for kernel to be available...');
+ var body = $('<div/>');
+ body.append($('<h4/>').text('Server Information:'));
+ body.append($('<p/>').html(text));
+ body.append($('<h4/>').text('Current Kernel Information:'));
+ body.append(kinfo);
+ } else {
+ var text = 'Could not access sys_info variable for version information.';
+ var body = $('<div/>');
+ body.append($('<h4/>').text('Cannot find sys_info!'));
+ body.append($('<p/>').html(text));
+ }
+ dialog.modal({
+ title: 'About Jupyter Notebook',
+ body: body,
+ buttons: { 'OK': {} }
+ });
+ try {
+ IPython.notebook.session.kernel.kernel_info(function (data) {
+ kinfo.html($('<pre/>').text(data.content.banner));
+ });
+ } catch (e) {
+ kinfo.html($('<p/>').text('unable to contact kernel'));
+ }
+ });
+});
diff --git a/notebook/static/notebook/js/actions.js b/notebook/static/notebook/js/actions.js
new file mode 100644
index 0000000..09dc4d2
--- /dev/null
+++ b/notebook/static/notebook/js/actions.js
@@ -0,0 +1,733 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+// How to pick action names:
+//
+// * First pick a noun and a verb for the action. For example, if the action is "restart kernel," the verb is
+// "restart" and the noun is "kernel".
+// * Omit terms like "selected" and "active" by default, so "delete-cell", rather than "delete-selected-cell".
+// Only provide a scope like "-all-" if it is other than the default "selected" or "active" scope.
+// * If an action has a secondary action, separate the secondary action with "-and-", so
+// "restart-kernel-and-clear-output".
+// * Don't ever use before/after as they have a temporal connotation that is confusing when used in a spatial
+// context.
+// * Use above/below or previous/next to indicate spacial and sequential relationships.
+// * For dialogs, use a verb that indicates what the dialog will accomplish, such as "confirm-restart-kernel".
+
+
+define(function(require){
+ "use strict";
+
+ var warn_bad_name = function(name){
+ if(name !== "" && !name.match(/:/)){
+ console.warn('You are trying to use an action/command name, where the separator between prefix and name is not `:`\n'+
+ '"'+name+'"\n'+
+ 'You are likely to not use the API in a correct way. Typically use the following:\n'+
+ '`var key = actions.register(<object>, "<name>", "<prefix>");` and reuse the `key` variable'+
+ 'instead of re-generating the key yourself.'
+ );
+ }
+ };
+
+ var ActionHandler = function (env) {
+ this.env = env || {};
+ Object.seal(this);
+ };
+
+ var $ = require('jquery');
+ var events = require('base/js/events');
+
+ /**
+ * A bunch of predefined `Simple Actions` used by Jupyter.
+ * `Simple Actions` have the following keys:
+ * help (optional): a short string the describe the action.
+ * will be used in various context, like as menu name, tool tips on buttons,
+ * and short description in help menu.
+ * help_index (optional): a string used to sort action in help menu.
+ * icon (optional): a short string that represent the icon that have to be used with this
+ * action. this should mainly correspond to a Font_awesome class.
+ * handler : a function which is called when the action is activated. It will receive at first parameter
+ * a dictionary containing various handle to element of the notebook.
+ *
+ * action need to be registered with a **name** that can be use to refer to this action.
+ *
+ * if `help` is not provided it will be derived by replacing any dash by space
+ * in the **name** of the action. It is advised to provide a prefix to action name to
+ * avoid conflict the prefix should be all lowercase and end with a dot `.`
+ * in the absence of a prefix the behavior of the action is undefined.
+ *
+ * All action provided by the Jupyter notebook are prefixed with `jupyter-notebook:`.
+ *
+ * One can register extra actions or replace an existing action with another one is possible
+ * but is considered undefined behavior.
+ *
+ **/
+ var _actions = {
+ 'restart-kernel': {
+ help: 'restart the kernel (no confirmation dialog)',
+ handler: function (env) {
+ env.notebook.restart_kernel({confirm: false});
+ },
+ },
+ 'confirm-restart-kernel':{
+ icon: 'fa-repeat',
+ help_index : 'hb',
+ help: 'restart the kernel (with dialog)',
+ handler : function (env) {
+ env.notebook.restart_kernel();
+ }
+ },
+ 'restart-kernel-and-run-all-cells': {
+ help: 'restart the kernel, then re-run the whole notebook (no confirmation dialog)',
+ handler: function (env) {
+ env.notebook.restart_run_all({confirm: false});
+ }
+ },
+ 'confirm-restart-kernel-and-run-all-cells': {
+ help: 'restart the kernel, then re-run the whole notebook (with dialog)',
+ handler: function (env) {
+ env.notebook.restart_run_all();
+ }
+ },
+ 'restart-kernel-and-clear-output': {
+ help: 'restart the kernel and clear all output (no confirmation dialog)',
+ handler: function (env) {
+ env.notebook.restart_clear_output({confirm: false});
+ }
+ },
+ 'confirm-restart-kernel-and-clear-output': {
+ help: 'restart the kernel and clear all output (with dialog)',
+ handler: function (env) {
+ env.notebook.restart_clear_output();
+ }
+ },
+ 'interrupt-kernel':{
+ icon: 'fa-stop',
+ help_index : 'ha',
+ handler : function (env) {
+ env.notebook.kernel.interrupt();
+ }
+ },
+ 'run-cell-and-select-next': {
+ icon: 'fa-step-forward',
+ help : 'run cell, select below',
+ help_index : 'ba',
+ handler : function (env) {
+ env.notebook.execute_cell_and_select_below();
+ }
+ },
+ 'run-cell':{
+ help : 'run selected cells',
+ help_index : 'bb',
+ handler : function (env) {
+ env.notebook.execute_selected_cells();
+ }
+ },
+ 'run-cell-and-insert-below':{
+ help : 'run cell, insert below',
+ help_index : 'bc',
+ handler : function (env) {
+ env.notebook.execute_cell_and_insert_below();
+ }
+ },
+ 'run-all-cells': {
+ help: 'run all cells',
+ help_index: 'bd',
+ handler: function (env) {
+ env.notebook.execute_all_cells();
+ }
+ },
+ 'run-all-cells-above':{
+ handler : function (env) {
+ env.notebook.execute_cells_above();
+ }
+ },
+ 'run-all-cells-below':{
+ handler : function (env) {
+ env.notebook.execute_cells_below();
+ }
+ },
+ 'enter-command-mode': {
+ help : 'command mode',
+ help_index : 'aa',
+ handler : function (env) {
+ env.notebook.command_mode();
+ }
+ },
+ 'split-cell-at-cursor': {
+ help : 'split cell',
+ help_index : 'ea',
+ handler : function (env) {
+ env.notebook.split_cell();
+ }
+ },
+ 'enter-edit-mode' : {
+ help_index : 'aa',
+ handler : function (env) {
+ env.notebook.edit_mode();
+ }
+ },
+ 'select-previous-cell' : {
+ help: 'select cell above',
+ help_index : 'da',
+ handler : function (env) {
+ var index = env.notebook.get_selected_index();
+ if (index !== 0 && index !== null) {
+ env.notebook.select_prev(true);
+ env.notebook.focus_cell();
+ }
+ }
+ },
+ 'select-next-cell' : {
+ help: 'select cell below',
+ help_index : 'db',
+ handler : function (env) {
+ var index = env.notebook.get_selected_index();
+ if (index !== (env.notebook.ncells()-1) && index !== null) {
+ env.notebook.select_next(true);
+ env.notebook.focus_cell();
+ }
+ }
+ },
+ 'extend-selection-above' : {
+ help: 'extend selected cells above',
+ help_index : 'dc',
+ handler : function (env) {
+ env.notebook.extend_selection_by(-1);
+ // scroll into view,
+ // do not call notebook.focus_cell(), or
+ // all the selection get thrown away
+ env.notebook.get_selected_cell().element.focus();
+ }
+ },
+ 'extend-selection-below' : {
+ help: 'extend selected cells below',
+ help_index : 'dd',
+ handler : function (env) {
+ env.notebook.extend_selection_by(1);
+ // scroll into view,
+ // do not call notebook.focus_cell(), or
+ // all the selection get thrown away
+ env.notebook.get_selected_cell().element.focus();
+ }
+ },
+ 'cut-cell' : {
+ help: 'cut selected cells',
+ icon: 'fa-cut',
+ help_index : 'ee',
+ handler : function (env) {
+ var index = env.notebook.get_selected_index();
+ env.notebook.cut_cell();
+ env.notebook.select(index);
+ }
+ },
+ 'copy-cell' : {
+ help: 'copy selected cells',
+ icon: 'fa-copy',
+ help_index : 'ef',
+ handler : function (env) {
+ env.notebook.copy_cell();
+ }
+ },
+ 'paste-cell-above' : {
+ help: 'paste cells above',
+ help_index : 'eg',
+ handler : function (env) {
+ env.notebook.paste_cell_above();
+ }
+ },
+ 'paste-cell-below' : {
+ help: 'paste cells below',
+ icon: 'fa-paste',
+ help_index : 'eh',
+ handler : function (env) {
+ env.notebook.paste_cell_below();
+ }
+ },
+ 'insert-cell-above' : {
+ help: 'insert cell above',
+ help_index : 'ec',
+ handler : function (env) {
+ env.notebook.insert_cell_above();
+ env.notebook.select_prev(true);
+ env.notebook.focus_cell();
+ }
+ },
+ 'insert-cell-below' : {
+ help: 'insert cell below',
+ icon : 'fa-plus',
+ help_index : 'ed',
+ handler : function (env) {
+ env.notebook.insert_cell_below();
+ env.notebook.select_next(true);
+ env.notebook.focus_cell();
+ }
+ },
+ 'change-cell-to-code' : {
+ help : 'to code',
+ help_index : 'ca',
+ handler : function (env) {
+ env.notebook.cells_to_code();
+ }
+ },
+ 'change-cell-to-markdown' : {
+ help : 'to markdown',
+ help_index : 'cb',
+ handler : function (env) {
+ env.notebook.cells_to_markdown();
+ }
+ },
+ 'change-cell-to-raw' : {
+ help : 'to raw',
+ help_index : 'cc',
+ handler : function (env) {
+ env.notebook.cells_to_raw();
+ }
+ },
+ 'change-cell-to-heading-1' : {
+ help : 'to heading 1',
+ help_index : 'cd',
+ handler : function (env) {
+ env.notebook.to_heading(undefined, 1);
+ }
+ },
+ 'change-cell-to-heading-2' : {
+ help : 'to heading 2',
+ help_index : 'ce',
+ handler : function (env) {
+ env.notebook.to_heading(undefined, 2);
+ }
+ },
+ 'change-cell-to-heading-3' : {
+ help : 'to heading 3',
+ help_index : 'cf',
+ handler : function (env) {
+ env.notebook.to_heading(undefined, 3);
+ }
+ },
+ 'change-cell-to-heading-4' : {
+ help : 'to heading 4',
+ help_index : 'cg',
+ handler : function (env) {
+ env.notebook.to_heading(undefined, 4);
+ }
+ },
+ 'change-cell-to-heading-5' : {
+ help : 'to heading 5',
+ help_index : 'ch',
+ handler : function (env) {
+ env.notebook.to_heading(undefined, 5);
+ }
+ },
+ 'change-cell-to-heading-6' : {
+ help : 'to heading 6',
+ help_index : 'ci',
+ handler : function (env) {
+ env.notebook.to_heading(undefined, 6);
+ }
+ },
+ 'toggle-cell-output-collapsed' : {
+ help : 'toggle output of selected cells',
+ help_index : 'gb',
+ handler : function (env) {
+ env.notebook.toggle_cells_outputs();
+ }
+ },
+ 'toggle-cell-output-scrolled' : {
+ help : 'toggle output scrolling of selected cells',
+ help_index : 'gc',
+ handler : function (env) {
+ env.notebook.toggle_cells_outputs_scroll();
+ }
+ },
+ 'clear-cell-output' : {
+ help : 'clear output of selected cells',
+ handler : function (env) {
+ env.notebook.clear_cells_outputs();
+ }
+ },
+ 'move-cell-down' : {
+ help: 'move selected cells down',
+ icon: 'fa-arrow-down',
+ help_index : 'eb',
+ handler : function (env) {
+ env.notebook.move_cell_down();
+ }
+ },
+ 'move-cell-up' : {
+ help: 'move selected cells up',
+ icon: 'fa-arrow-up',
+ help_index : 'ea',
+ handler : function (env) {
+ env.notebook.move_cell_up();
+ }
+ },
+ 'toggle-cell-line-numbers' : {
+ help : 'toggle line numbers',
+ help_index : 'ga',
+ handler : function (env) {
+ env.notebook.cell_toggle_line_numbers();
+ }
+ },
+ 'show-keyboard-shortcuts' : {
+ help_index : 'ge',
+ handler : function (env) {
+ env.quick_help.show_keyboard_shortcuts();
+ }
+ },
+ 'delete-cell': {
+ help: 'delete selected cells',
+ help_index : 'ej',
+ handler : function (env) {
+ env.notebook.delete_cell();
+ }
+ },
+ 'undo-cell-deletion' : {
+ help_index : 'ei',
+ handler : function (env) {
+ env.notebook.undelete_cell();
+ }
+ },
+ // TODO reminder
+ // open an issue, merge with above merge with last cell of notebook if at top.
+ 'merge-cell-with-previous-cell' : {
+ handler : function (env) {
+ env.notebook.merge_cell_above();
+ }
+ },
+ 'merge-cell-with-next-cell' : {
+ help : 'merge cell below',
+ help_index : 'ek',
+ handler : function (env) {
+ env.notebook.merge_cell_below();
+ }
+ },
+ 'merge-selected-cells' : {
+ help : 'merge selected cells',
+ help_index: 'el',
+ handler: function(env) {
+ env.notebook.merge_selected_cells();
+ }
+ },
+ 'merge-cells' : {
+ help : 'merge selected cells, or current cell with cell below if only one cell selected',
+ help_index: 'el',
+ handler: function(env) {
+ var l = env.notebook.get_selected_cells_indices().length;
+ if(l == 1){
+ env.notebook.merge_cell_below();
+ } else {
+ env.notebook.merge_selected_cells();
+ }
+ }
+ },
+ 'show-command-palette': {
+ help_index : 'aa',
+ help: 'open the command palette',
+ icon: 'fa-keyboard-o',
+ handler : function(env){
+ env.notebook.show_command_palette();
+ }
+ },
+ 'toggle-toolbar':{
+ help: 'hide/show the toolbar',
+ handler : function(env){
+ $('div#maintoolbar').toggle();
+ events.trigger('resize-header.Page');
+ }
+ },
+ 'toggle-header':{
+ help: 'hide/show the header',
+ handler : function(env){
+ $('#header-container').toggle();
+ $('.header-bar').toggle();
+ events.trigger('resize-header.Page');
+ }
+ },
+ 'close-pager': {
+ help : 'close the pager',
+ handler : function(env) {
+ // Collapse the page if it is open
+ if (env.pager && env.pager.expanded) {
+ env.pager.collapse();
+ }
+ }
+ },
+ };
+
+ /**
+ * A bunch of `Advance actions` for Jupyter.
+ * Cf `Simple Action` plus the following properties.
+ *
+ * handler: first argument of the handler is the event that triggerd the action
+ * (typically keypress). The handler is responsible for any modification of the
+ * event and event propagation.
+ * Is also responsible for returning false if the event have to be further ignored,
+ * true, to tell keyboard manager that it ignored the event.
+ *
+ * the second parameter of the handler is the environemnt passed to Simple Actions
+ *
+ **/
+ var custom_ignore = {
+ 'ignore':{
+ handler : function () {
+ return true;
+ }
+ },
+ 'move-cursor-up':{
+ handler : function (env, event) {
+ var index = env.notebook.get_selected_index();
+ var cell = env.notebook.get_cell(index);
+ var cm = env.notebook.get_selected_cell().code_mirror;
+ var cur = cm.getCursor();
+ if (cell && cell.at_top() && index !== 0 && cur.ch === 0) {
+ if(event){
+ event.preventDefault();
+ }
+ env.notebook.command_mode();
+ env.notebook.select_prev(true);
+ env.notebook.edit_mode();
+ cm = env.notebook.get_selected_cell().code_mirror;
+ cm.setCursor(cm.lastLine(), 0);
+ }
+ return false;
+ }
+ },
+ 'move-cursor-down':{
+ handler : function (env, event) {
+ var index = env.notebook.get_selected_index();
+ var cell = env.notebook.get_cell(index);
+ if (cell.at_bottom() && index !== (env.notebook.ncells()-1)) {
+ if(event){
+ event.preventDefault();
+ }
+ env.notebook.command_mode();
+ env.notebook.select_next(true);
+ env.notebook.edit_mode();
+ var cm = env.notebook.get_selected_cell().code_mirror;
+ cm.setCursor(0, 0);
+ }
+ return false;
+ }
+ },
+ 'scroll-notebook-down': {
+ handler: function(env, event) {
+ if(event){
+ event.preventDefault();
+ }
+ return env.notebook.scroll_manager.scroll(1);
+ },
+ },
+ 'scroll-notebook-up': {
+ handler: function(env, event) {
+ if(event){
+ event.preventDefault();
+ }
+ return env.notebook.scroll_manager.scroll(-1);
+ },
+ },
+ 'scroll-cell-center': {
+ help: "Scroll the current cell to the center",
+ handler: function (env, event) {
+ if(event){
+ event.preventDefault();
+ }
+ var cell = env.notebook.get_selected_index();
+ return env.notebook.scroll_cell_percent(cell, 50, 0);
+ }
+ },
+ 'scroll-cell-top': {
+ help: "Scroll the current cell to the top",
+ handler: function (env, event) {
+ if(event){
+ event.preventDefault();
+ }
+ var cell = env.notebook.get_selected_index();
+ return env.notebook.scroll_cell_percent(cell, 0, 0);
+ }
+ },
+ 'duplicate-notebook':{
+ help: "Create an open a copy of current notebook",
+ handler : function (env, event) {
+ env.notebook.copy_notebook();
+ }
+ },
+ 'trust-notebook':{
+ help: "Trust the current notebook",
+ handler : function (env, event) {
+ env.notebook.trust_notebook();
+ }
+ },
+ 'rename-notebook':{
+ help: "Rename current notebook",
+ handler : function (env, event) {
+ env.notebook.save_widget.rename_notebook({notebook: env.notebook});
+ }
+ },
+ 'toggle-all-cells-output-collapsed':{
+ help: "Toggle the hiddens state of all output areas",
+ handler : function (env, event) {
+ env.notebook.toggle_all_output();
+ }
+ },
+ 'toggle-all-cells-output-scrolled':{
+ help: "Toggle the scrolling state of all output areas",
+ handler : function (env, event) {
+ env.notebook.toggle_all_output_scroll();
+ }
+ },
+
+ 'clear-all-cells-output':{
+ help: "Clear the content of all the outputs",
+ handler : function (env, event) {
+ env.notebook.clear_all_output();
+ }
+ },
+ 'save-notebook':{
+ help: "Save and Checkpoint",
+ help_index : 'fb',
+ icon: 'fa-save',
+ handler : function (env, event) {
+ env.notebook.save_checkpoint();
+ if(event){
+ event.preventDefault();
+ }
+ return false;
+ }
+ },
+ };
+
+ // private stuff that prepend `jupyter-notebook:` to actions names
+ // and uniformize/fill in missing pieces in of an action.
+ var _prepare_handler = function(registry, subkey, source){
+ registry['jupyter-notebook:'+subkey] = {};
+ registry['jupyter-notebook:'+subkey].help = source[subkey].help||subkey.replace(/-/g,' ');
+ registry['jupyter-notebook:'+subkey].help_index = source[subkey].help_index;
+ registry['jupyter-notebook:'+subkey].icon = source[subkey].icon;
+ return source[subkey].handler;
+ };
+
+ // Will actually generate/register all the Jupyter actions
+ var fun = function(){
+ var final_actions = {};
+ var k;
+ for(k in _actions){
+ if(_actions.hasOwnProperty(k)){
+ // Js closure are function level not block level need to wrap in a IIFE
+ // and append jupyter-notebook: to event name these things do intercept event so are wrapped
+ // in a function that return false.
+ var handler = _prepare_handler(final_actions, k, _actions);
+ (function(key, handler){
+ final_actions['jupyter-notebook:'+key].handler = function(env, event){
+ handler(env);
+ if(event){
+ event.preventDefault();
+ }
+ return false;
+ };
+ })(k, handler);
+ }
+ }
+
+ for(k in custom_ignore){
+ // Js closure are function level not block level need to wrap in a IIFE
+ // same as above, but decide for themselves whether or not they intercept events.
+ if(custom_ignore.hasOwnProperty(k)){
+ handler = _prepare_handler(final_actions, k, custom_ignore);
+ (function(key, handler){
+ final_actions['jupyter-notebook:'+key].handler = function(env, event){
+ return handler(env, event);
+ };
+ })(k, handler);
+ }
+ }
+
+ return final_actions;
+ };
+ ActionHandler.prototype._actions = fun();
+
+
+ /**
+ * extend the environment variable that will be pass to handlers
+ **/
+ ActionHandler.prototype.extend_env = function(env){
+ for(var k in env){
+ this.env[k] = env[k];
+ }
+ };
+
+ ActionHandler.prototype.register = function(action, name, prefix){
+ /**
+ * Register an `action` with an optional name and prefix.
+ *
+ * if name and prefix are not given they will be determined automatically.
+ * if action if just a `function` it will be wrapped in an anonymous action.
+ *
+ * @return the full name to access this action .
+ **/
+ action = this.normalise(action);
+ if( !name ){
+ name = 'autogenerated-'+String(action.handler);
+ }
+ prefix = prefix || 'auto';
+ var full_name = prefix+':'+name;
+ this._actions[full_name] = action;
+ return full_name;
+
+ };
+
+
+ ActionHandler.prototype.normalise = function(data){
+ /**
+ * given an `action` or `function`, return a normalised `action`
+ * by setting all known attributes and removing unknown attributes;
+ **/
+ if(typeof(data) === 'function'){
+ data = {handler:data};
+ }
+ if(typeof(data.handler) !== 'function'){
+ throw('unknown datatype, cannot register');
+ }
+ var _data = data;
+ data = {};
+ data.handler = _data.handler;
+ data.help = _data.help || '';
+ data.icon = _data.icon || '';
+ data.help_index = _data.help_index || '';
+ return data;
+ };
+
+ ActionHandler.prototype.get_name = function(name_or_data){
+ /**
+ * given an `action` or `name` of a action, return the name attached to this action.
+ * if given the name of and corresponding actions does not exist in registry, return `null`.
+ **/
+
+ if(typeof(name_or_data) === 'string'){
+ warn_bad_name(name);
+ if(this.exists(name_or_data)){
+ return name_or_data;
+ } else {
+ return null;
+ }
+ } else {
+ return this.register(name_or_data);
+ }
+ };
+
+ ActionHandler.prototype.get = function(name){
+ warn_bad_name(name);
+ return this._actions[name];
+ };
+
+ ActionHandler.prototype.call = function(name, event, env){
+ return this._actions[name].handler(env|| this.env, event);
+ };
+
+ ActionHandler.prototype.exists = function(name){
+ return (typeof(this._actions[name]) !== 'undefined');
+ };
+
+ return {init:ActionHandler};
+
+});
diff --git a/notebook/static/notebook/js/cell.js b/notebook/static/notebook/js/cell.js
new file mode 100644
index 0000000..5eeb0b1
--- /dev/null
+++ b/notebook/static/notebook/js/cell.js
@@ -0,0 +1,747 @@
+// Distributed under the terms of the Modified BSD License.
+
+/**
+ *
+ *
+ * @module cell
+ * @namespace cell
+ * @class Cell
+ */
+
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'codemirror/lib/codemirror',
+ 'codemirror/addon/edit/matchbrackets',
+ 'codemirror/addon/edit/closebrackets',
+ 'codemirror/addon/comment/comment'
+], function($, utils, CodeMirror, cm_match, cm_closeb, cm_comment) {
+ "use strict";
+
+ var overlayHack = CodeMirror.scrollbarModel.native.prototype.overlayHack;
+
+ CodeMirror.scrollbarModel.native.prototype.overlayHack = function () {
+ overlayHack.apply(this, arguments);
+ // Reverse `min-height: 18px` scrollbar hack on OS X
+ // which causes a dead area, making it impossible to click on the last line
+ // when there is horizontal scrolling to do and the "show scrollbar only when scrolling" behavior
+ // is enabled.
+ // This, in turn, has the undesirable behavior of never showing the horizontal scrollbar,
+ // even when it should, which is less problematic, at least.
+ if (/Mac/.test(navigator.platform)) {
+ this.horiz.style.minHeight = "";
+ }
+ };
+
+ var Cell = function (options) {
+ /* Constructor
+ *
+ * The Base `Cell` class from which to inherit.
+ * @constructor
+ * @param:
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * config: dictionary
+ * keyboard_manager: KeyboardManager instance
+ */
+ options = options || {};
+ this.keyboard_manager = options.keyboard_manager;
+ this.events = options.events;
+ var config = utils.mergeopt(Cell, options.config);
+ // superclass default overwrite our default
+
+ this.placeholder = config.placeholder || '';
+ this.selected = false;
+ this.anchor = false;
+ this.rendered = false;
+ this.mode = 'command';
+
+ // Metadata property
+ var that = this;
+ this._metadata = {};
+ Object.defineProperty(this, 'metadata', {
+ get: function() { return that._metadata; },
+ set: function(value) {
+ that._metadata = value;
+ if (that.celltoolbar) {
+ that.celltoolbar.rebuild();
+ }
+ }
+ });
+
+ // backward compat.
+ Object.defineProperty(this, 'cm_config', {
+ get: function() {
+ console.warn("Warning: accessing Cell.cm_config directly is deprecated.");
+ return that._options.cm_config;
+ },
+ });
+
+ // load this from metadata later ?
+ this.user_highlight = 'auto';
+
+
+ var _local_cm_config = {};
+ if(this.class_config){
+ _local_cm_config = this.class_config.get_sync('cm_config');
+ }
+ config.cm_config = utils.mergeopt({}, config.cm_config, _local_cm_config);
+ this.cell_id = utils.uuid();
+ this._options = config;
+
+ // For JS VM engines optimization, attributes should be all set (even
+ // to null) in the constructor, and if possible, if different subclass
+ // have new attributes with same name, they should be created in the
+ // same order. Easiest is to create and set to null in parent class.
+
+ this.element = null;
+ this.cell_type = this.cell_type || null;
+ this.code_mirror = null;
+
+ this.create_element();
+ if (this.element !== null) {
+ this.element.data("cell", this);
+ this.bind_events();
+ this.init_classes();
+ }
+ };
+
+ Cell.options_default = {
+ cm_config : {
+ indentUnit : 4,
+ readOnly: false,
+ theme: "default",
+ extraKeys: {
+ "Cmd-Right":"goLineRight",
+ "End":"goLineRight",
+ "Cmd-Left":"goLineLeft"
+ }
+ }
+ };
+
+ // FIXME: Workaround CM Bug #332 (Safari segfault on drag)
+ // by disabling drag/drop altogether on Safari
+ // https://github.com/codemirror/CodeMirror/issues/332
+ if (utils.browser[0] == "Safari") {
+ Cell.options_default.cm_config.dragDrop = false;
+ }
+
+ /**
+ * Empty. Subclasses must implement create_element.
+ * This should contain all the code to create the DOM element in notebook
+ * and will be called by Base Class constructor.
+ * @method create_element
+ */
+ Cell.prototype.create_element = function () {
+ };
+
+ Cell.prototype.init_classes = function () {
+ /**
+ * Call after this.element exists to initialize the css classes
+ * related to selected, rendered and mode.
+ */
+ if (this.selected) {
+ this.element.addClass('selected');
+ } else {
+ this.element.addClass('unselected');
+ }
+ if (this.rendered) {
+ this.element.addClass('rendered');
+ } else {
+ this.element.addClass('unrendered');
+ }
+ };
+
+ /**
+ * trigger on focus and on click to bubble up to the notebook and
+ * potentially extend the selection if shift-click, contract the selection
+ * if just codemirror focus (so edit mode).
+ * We **might** be able to move that to notebook `handle_edit_mode`.
+ */
+ Cell.prototype._on_click = function (event) {
+ if (!this.selected) {
+ this.events.trigger('select.Cell', {'cell':this, 'extendSelection':event.shiftKey});
+ } else {
+ // I'm already part of the selection; contract selection to just me
+ this.events.trigger('select.Cell', {'cell': this});
+ }
+ };
+
+ /**
+ * Subclasses can implement override bind_events.
+ * Be careful to call the parent method when overwriting as it fires event.
+ * this will be triggered after create_element in constructor.
+ * @method bind_events
+ */
+ Cell.prototype.bind_events = function () {
+ var that = this;
+ // We trigger events so that Cell doesn't have to depend on Notebook.
+ that.element.click(function (event) {
+ that._on_click(event);
+ });
+ if (this.code_mirror) {
+ this.code_mirror.on("change", function(cm, change) {
+ that.events.trigger("set_dirty.Notebook", {value: true});
+ });
+ }
+ if (this.code_mirror) {
+ this.code_mirror.on('focus', function(cm, change) {
+ if (!that.selected) {
+ that.events.trigger('select.Cell', {'cell':that});
+ }
+ that.events.trigger('edit_mode.Cell', {cell: that});
+ });
+ }
+ if (this.code_mirror) {
+ this.code_mirror.on('blur', function(cm, change) {
+ that.events.trigger('command_mode.Cell', {cell: that});
+ });
+ }
+
+ this.element.dblclick(function () {
+ if (that.selected === false) {
+ this.events.trigger('select.Cell', {'cell':that});
+ }
+ });
+ };
+
+ /**
+ * This method gets called in CodeMirror's onKeyDown/onKeyPress
+ * handlers and is used to provide custom key handling.
+ *
+ * To have custom handling, subclasses should override this method, but still call it
+ * in order to process the Edit mode keyboard shortcuts.
+ *
+ * @method handle_codemirror_keyevent
+ * @param {CodeMirror} editor - The codemirror instance bound to the cell
+ * @param {event} event - key press event which either should or should not be handled by CodeMirror
+ * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
+ */
+ Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
+ var shortcuts = this.keyboard_manager.edit_shortcuts;
+
+ var cur = editor.getCursor();
+ if((cur.line !== 0 || cur.ch !==0) && event.keyCode === 38){
+ event._ipkmIgnore = true;
+ }
+ var nLastLine = editor.lastLine();
+ if ((event.keyCode === 40) &&
+ ((cur.line !== nLastLine) ||
+ (cur.ch !== editor.getLineHandle(nLastLine).text.length))
+ ) {
+ event._ipkmIgnore = true;
+ }
+ // if this is an edit_shortcuts shortcut, the global keyboard/shortcut
+ // manager will handle it
+ if (shortcuts.handles(event)) {
+ return true;
+ }
+
+ return false;
+ };
+
+
+ /**
+ * Triger typesetting of math by mathjax on current cell element
+ * @method typeset
+ */
+ Cell.prototype.typeset = function () {
+ utils.typeset(this.element);
+ };
+
+ /**
+ * handle cell level logic when a cell is selected
+ * @method select
+ * @return is the action being taken
+ */
+ Cell.prototype.select = function (moveanchor) {
+ // if anchor is true, set the move the anchor
+ moveanchor = (moveanchor === undefined)? true:moveanchor;
+ if(moveanchor){
+ this.anchor=true;
+ }
+
+ if (!this.selected) {
+ this.element.addClass('selected');
+ this.element.removeClass('unselected');
+ this.selected = true;
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * handle cell level logic when the cell is unselected
+ * @method unselect
+ * @return is the action being taken
+ */
+ Cell.prototype.unselect = function (moveanchor) {
+ // if anchor is true, remove also the anchor
+ moveanchor = (moveanchor === undefined)? true:moveanchor;
+ if (moveanchor){
+ this.anchor = false;
+ }
+ if (this.selected) {
+ this.element.addClass('unselected');
+ this.element.removeClass('selected');
+ this.selected = false;
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+
+ /**
+ * should be overwritten by subclass
+ * @method execute
+ */
+ Cell.prototype.execute = function () {
+ return;
+ };
+
+ /**
+ * handle cell level logic when a cell is rendered
+ * @method render
+ * @return is the action being taken
+ */
+ Cell.prototype.render = function () {
+ if (!this.rendered) {
+ this.element.addClass('rendered');
+ this.element.removeClass('unrendered');
+ this.rendered = true;
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * handle cell level logic when a cell is unrendered
+ * @method unrender
+ * @return is the action being taken
+ */
+ Cell.prototype.unrender = function () {
+ if (this.rendered) {
+ this.element.addClass('unrendered');
+ this.element.removeClass('rendered');
+ this.rendered = false;
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * Delegates keyboard shortcut handling to either Jupyter keyboard
+ * manager when in command mode, or CodeMirror when in edit mode
+ *
+ * @method handle_keyevent
+ * @param {CodeMirror} editor - The codemirror instance bound to the cell
+ * @param {event} - key event to be handled
+ * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
+ */
+ Cell.prototype.handle_keyevent = function (editor, event) {
+ if (this.mode === 'command') {
+ return true;
+ } else if (this.mode === 'edit') {
+ return this.handle_codemirror_keyevent(editor, event);
+ }
+ };
+
+ /**
+ * @method at_top
+ * @return {Boolean}
+ */
+ Cell.prototype.at_top = function () {
+ var cm = this.code_mirror;
+ var cursor = cm.getCursor();
+ if (cursor.line === 0 && cursor.ch === 0) {
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * @method at_bottom
+ * @return {Boolean}
+ * */
+ Cell.prototype.at_bottom = function () {
+ var cm = this.code_mirror;
+ var cursor = cm.getCursor();
+ if (cursor.line === (cm.lineCount()-1) && cursor.ch === cm.getLine(cursor.line).length) {
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * enter the command mode for the cell
+ * @method command_mode
+ * @return is the action being taken
+ */
+ Cell.prototype.command_mode = function () {
+ if (this.mode !== 'command') {
+ this.mode = 'command';
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * enter the edit mode for the cell
+ * @method command_mode
+ * @return is the action being taken
+ */
+ Cell.prototype.edit_mode = function () {
+ if (this.mode !== 'edit') {
+ this.mode = 'edit';
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ Cell.prototype.ensure_focused = function() {
+ if(this.element !== document.activeElement && !this.code_mirror.hasFocus()){
+ this.focus_cell();
+ }
+ };
+
+ /**
+ * Focus the cell in the DOM sense
+ * @method focus_cell
+ */
+ Cell.prototype.focus_cell = function () {
+ this.element.focus();
+ this._on_click({});
+ };
+
+ /**
+ * Focus the editor area so a user can type
+ *
+ * NOTE: If codemirror is focused via a mouse click event, you don't want to
+ * call this because it will cause a page jump.
+ * @method focus_editor
+ */
+ Cell.prototype.focus_editor = function () {
+ this.refresh();
+ this.code_mirror.focus();
+ };
+
+ /**
+ * Refresh codemirror instance
+ * @method refresh
+ */
+ Cell.prototype.refresh = function () {
+ if (this.code_mirror) {
+ this.code_mirror.refresh();
+ }
+ };
+
+ /**
+ * should be overritten by subclass
+ * @method get_text
+ */
+ Cell.prototype.get_text = function () {
+ };
+
+ /**
+ * should be overritten by subclass
+ * @method set_text
+ * @param {string} text
+ */
+ Cell.prototype.set_text = function (text) {
+ };
+
+ /**
+ * should be overritten by subclass
+ * serialise cell to json.
+ * @method toJSON
+ **/
+ Cell.prototype.toJSON = function () {
+ var data = {};
+ // deepcopy the metadata so copied cells don't share the same object
+ data.metadata = JSON.parse(JSON.stringify(this.metadata));
+ data.cell_type = this.cell_type;
+ return data;
+ };
+
+ /**
+ * should be overritten by subclass
+ * @method fromJSON
+ **/
+ Cell.prototype.fromJSON = function (data) {
+ if (data.metadata !== undefined) {
+ this.metadata = data.metadata;
+ }
+ };
+
+
+ /**
+ * can the cell be split into two cells (false if not deletable)
+ * @method is_splittable
+ **/
+ Cell.prototype.is_splittable = function () {
+ return this.is_deletable();
+ };
+
+
+ /**
+ * can the cell be merged with other cells (false if not deletable)
+ * @method is_mergeable
+ **/
+ Cell.prototype.is_mergeable = function () {
+ return this.is_deletable();
+ };
+
+ /**
+ * is the cell deletable? only false (undeletable) if
+ * metadata.deletable is explicitly false -- everything else
+ * counts as true
+ *
+ * @method is_deletable
+ **/
+ Cell.prototype.is_deletable = function () {
+ if (this.metadata.deletable === false) {
+ return false;
+ }
+ return true;
+ };
+
+ /**
+ * @return {String} - the text before the cursor
+ * @method get_pre_cursor
+ **/
+ Cell.prototype.get_pre_cursor = function () {
+ var cursor = this.code_mirror.getCursor();
+ var text = this.code_mirror.getRange({line:0, ch:0}, cursor);
+ text = text.replace(/^\n+/, '').replace(/\n+$/, '');
+ return text;
+ };
+
+
+ /**
+ * @return {String} - the text after the cursor
+ * @method get_post_cursor
+ **/
+ Cell.prototype.get_post_cursor = function () {
+ var cursor = this.code_mirror.getCursor();
+ var last_line_num = this.code_mirror.lineCount()-1;
+ var last_line_len = this.code_mirror.getLine(last_line_num).length;
+ var end = {line:last_line_num, ch:last_line_len};
+ var text = this.code_mirror.getRange(cursor, end);
+ text = text.replace(/^\n+/, '').replace(/\n+$/, '');
+ return text;
+ };
+
+ /**
+ * Show/Hide CodeMirror LineNumber
+ * @method show_line_numbers
+ *
+ * @param value {Bool} show (true), or hide (false) the line number in CodeMirror
+ **/
+ Cell.prototype.show_line_numbers = function (value) {
+ this.code_mirror.setOption('lineNumbers', value);
+ this.code_mirror.refresh();
+ };
+
+ /**
+ * Toggle CodeMirror LineNumber
+ * @method toggle_line_numbers
+ **/
+ Cell.prototype.toggle_line_numbers = function () {
+ var val = this.code_mirror.getOption('lineNumbers');
+ this.show_line_numbers(!val);
+ };
+
+ /**
+ * Force codemirror highlight mode
+ * @method force_highlight
+ * @param {object} - CodeMirror mode
+ **/
+ Cell.prototype.force_highlight = function(mode) {
+ this.user_highlight = mode;
+ this.auto_highlight();
+ };
+
+ /**
+ * Trigger autodetection of highlight scheme for current cell
+ * @method auto_highlight
+ */
+ Cell.prototype.auto_highlight = function () {
+ this._auto_highlight(this.class_config.get_sync('highlight_modes'));
+ };
+
+ /**
+ * Try to autodetect cell highlight mode, or use selected mode
+ * @methods _auto_highlight
+ * @private
+ * @param {String|object|undefined} - CodeMirror mode | 'auto'
+ **/
+ Cell.prototype._auto_highlight = function (modes) {
+ /**
+ *Here we handle manually selected modes
+ */
+ var that = this;
+ var mode;
+ if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
+ {
+ mode = this.user_highlight;
+ CodeMirror.autoLoadMode(this.code_mirror, mode);
+ this.code_mirror.setOption('mode', mode);
+ return;
+ }
+ var current_mode = this.code_mirror.getOption('mode', mode);
+ var first_line = this.code_mirror.getLine(0);
+ // loop on every pairs
+ for(mode in modes) {
+ var regs = modes[mode].reg;
+ // only one key every time but regexp can't be keys...
+ for(var i=0; i<regs.length; i++) {
+ // here we handle non magic_modes.
+ // TODO :
+ // On 3.0 and below, these things were regex.
+ // But now should be string for json-able config.
+ // We should get rid of assuming they might be already
+ // in a later version of Jupyter.
+ var re = regs[i];
+ if(typeof(re) === 'string'){
+ re = new RegExp(re);
+ }
+ if(first_line.match(re) !== null) {
+ if(current_mode == mode){
+ return;
+ }
+ if (mode.search('magic_') !== 0) {
+ utils.requireCodeMirrorMode(mode, function (spec) {
+ that.code_mirror.setOption('mode', spec);
+ });
+ return;
+ }
+ var magic_mode = mode;
+ mode = magic_mode.substr(6);
+ if(current_mode == magic_mode){
+ return;
+ }
+ utils.requireCodeMirrorMode(mode, function (spec) {
+ // Add an overlay mode to recognize the first line as "magic" instead
+ // of the mode used for the rest of the cell.
+ CodeMirror.defineMode(magic_mode, function(config) {
+ var magicOverlay = {
+ startState: function() {
+ return {firstMatched : false, inMagicLine: false};
+ },
+ token: function(stream, state) {
+ if(!state.firstMatched) {
+ state.firstMatched = true;
+ if (stream.match("%%", false)) {
+ state.inMagicLine = true;
+ }
+ }
+ if (state.inMagicLine) {
+ stream.eat(function any(ch) { return true; });
+ if (stream.eol()) {
+ state.inMagicLine = false;
+ }
+ return "magic";
+ }
+ stream.skipToEnd();
+ return null;
+ }
+ };
+ return CodeMirror.overlayMode(CodeMirror.getMode(config, spec), magicOverlay);
+ });
+ that.code_mirror.setOption('mode', magic_mode);
+ });
+ return;
+ }
+ }
+ }
+ // fallback on default
+ var default_mode;
+ try {
+ default_mode = this._options.cm_config.mode;
+ } catch(e) {
+ default_mode = 'text/plain';
+ }
+ if( current_mode === default_mode){
+ return;
+ }
+ this.code_mirror.setOption('mode', default_mode);
+ };
+
+ var UnrecognizedCell = function (options) {
+ /** Constructor for unrecognized cells */
+ Cell.apply(this, arguments);
+ this.cell_type = 'unrecognized';
+ this.celltoolbar = null;
+ this.data = {};
+
+ Object.seal(this);
+ };
+
+ UnrecognizedCell.prototype = Object.create(Cell.prototype);
+
+
+ // cannot merge or split unrecognized cells
+ UnrecognizedCell.prototype.is_mergeable = function () {
+ return false;
+ };
+
+ UnrecognizedCell.prototype.is_splittable = function () {
+ return false;
+ };
+
+ UnrecognizedCell.prototype.toJSON = function () {
+ /**
+ * deepcopy the metadata so copied cells don't share the same object
+ */
+ return JSON.parse(JSON.stringify(this.data));
+ };
+
+ UnrecognizedCell.prototype.fromJSON = function (data) {
+ this.data = data;
+ if (data.metadata !== undefined) {
+ this.metadata = data.metadata;
+ } else {
+ data.metadata = this.metadata;
+ }
+ this.element.find('.inner_cell').find("a").text("Unrecognized cell type: " + data.cell_type);
+ };
+
+ UnrecognizedCell.prototype.create_element = function () {
+ Cell.prototype.create_element.apply(this, arguments);
+ var cell = this.element = $("<div>").addClass('cell unrecognized_cell');
+ cell.attr('tabindex','2');
+
+ var prompt = $('<div/>').addClass('prompt input_prompt');
+ cell.append(prompt);
+ var inner_cell = $('<div/>').addClass('inner_cell');
+ inner_cell.append(
+ $("<a>")
+ .attr("href", "#")
+ .text("Unrecognized cell type")
+ );
+ cell.append(inner_cell);
+ this.element = cell;
+ };
+
+ UnrecognizedCell.prototype.bind_events = function () {
+ Cell.prototype.bind_events.apply(this, arguments);
+ var cell = this;
+
+ this.element.find('.inner_cell').find("a").click(function () {
+ cell.events.trigger('unrecognized_cell.Cell', {cell: cell});
+ });
+ };
+
+ return {
+ Cell: Cell,
+ UnrecognizedCell: UnrecognizedCell
+ };
+});
diff --git a/notebook/static/notebook/js/celltoolbar.js b/notebook/static/notebook/js/celltoolbar.js
new file mode 100644
index 0000000..b0bb31a
--- /dev/null
+++ b/notebook/static/notebook/js/celltoolbar.js
@@ -0,0 +1,466 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'base/js/namespace',
+ 'jquery',
+ 'base/js/events'
+], function(IPython, $, events) {
+ "use strict";
+
+ var CellToolbar = function (options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * cell: Cell instance
+ * notebook: Notebook instance
+ *
+ * TODO: This leaks, when cell are deleted
+ * There is still a reference to each celltoolbars.
+ */
+ CellToolbar._instances.push(this);
+ this.notebook = options.notebook;
+ this.cell = options.cell;
+ this.create_element();
+ this.rebuild();
+ return this;
+ };
+
+
+ CellToolbar.prototype.create_element = function () {
+ this.inner_element = $('<div/>').addClass('celltoolbar');
+ this.element = $('<div/>').addClass('ctb_hideshow')
+ .append(this.inner_element);
+ };
+
+
+ // The default css style for the outer celltoolbar div
+ // (ctb_hideshow) is display: none.
+ // To show the cell toolbar, *both* of the following conditions must be met:
+ // - A parent container has class `ctb_global_show`
+ // - The celltoolbar has the class `ctb_show`
+ // This allows global show/hide, as well as per-cell show/hide.
+
+ CellToolbar.global_hide = function () {
+ $('body').removeClass('ctb_global_show');
+ };
+
+
+ CellToolbar.global_show = function () {
+ $('body').addClass('ctb_global_show');
+ };
+
+
+ CellToolbar.prototype.hide = function () {
+ this.element.removeClass('ctb_show');
+ };
+
+
+ CellToolbar.prototype.show = function () {
+ this.element.addClass('ctb_show');
+ };
+
+
+ /**
+ * Class variable that should contain a dict of all available callback
+ * we need to think of wether or not we allow nested namespace
+ * @property _callback_dict
+ * @private
+ * @static
+ * @type Dict
+ */
+ CellToolbar._callback_dict = {};
+
+
+ /**
+ * Class variable that should contain the reverse order list of the button
+ * to add to the toolbar of each cell
+ * @property _ui_controls_list
+ * @private
+ * @static
+ * @type List
+ */
+ CellToolbar._ui_controls_list = [];
+
+
+ /**
+ * Class variable that should contain the CellToolbar instances for each
+ * cell of the notebook
+ *
+ * @private
+ * @property _instances
+ * @static
+ * @type List
+ */
+ CellToolbar._instances = [];
+
+
+ /**
+ * keep a list of all the available presets for the toolbar
+ * @private
+ * @property _presets
+ * @static
+ * @type Dict
+ */
+ CellToolbar._presets = {};
+
+
+ // this is by design not a prototype.
+ /**
+ * Register a callback to create an UI element in a cell toolbar.
+ * @method register_callback
+ * @param name {String} name to use to refer to the callback. It is advised to use a prefix with the name
+ * for easier sorting and avoid collision
+ * @param callback {function(div, cell)} callback that will be called to generate the ui element
+ * @param [cell_types] {List_of_String|undefined} optional list of cell types. If present the UI element
+ * will be added only to cells of types in the list.
+ *
+ *
+ * The callback will receive the following element :
+ *
+ * * a div in which to add element.
+ * * the cell it is responsible from
+ *
+ * @example
+ *
+ * Example that create callback for a button that toggle between `true` and `false` label,
+ * with the metadata under the key 'foo' to reflect the status of the button.
+ *
+ * // first param reference to a DOM div
+ * // second param reference to the cell.
+ * var toggle = function(div, cell) {
+ * var button_container = $(div)
+ *
+ * // let's create a button that show the current value of the metadata
+ * var button = $('<div/>').button({label:String(cell.metadata.foo)});
+ *
+ * // On click, change the metadata value and update the button label
+ * button.click(function(){
+ * var v = cell.metadata.foo;
+ * cell.metadata.foo = !v;
+ * button.button("option", "label", String(!v));
+ * })
+ *
+ * // add the button to the DOM div.
+ * button_container.append(button);
+ * }
+ *
+ * // now we register the callback under the name `foo` to give the
+ * // user the ability to use it later
+ * CellToolbar.register_callback('foo', toggle);
+ */
+ CellToolbar.register_callback = function(name, callback, cell_types) {
+ // Overwrite if it already exists.
+ CellToolbar._callback_dict[name] = cell_types ? {callback: callback, cell_types: cell_types} : callback;
+ };
+
+
+ /**
+ * Register a preset of UI element in a cell toolbar.
+ * Not supported Yet.
+ * @method register_preset
+ * @param name {String} name to use to refer to the preset. It is advised to use a prefix with the name
+ * for easier sorting and avoid collision
+ * @param preset_list {List_of_String} reverse order of the button in the toolbar. Each String of the list
+ * should correspond to a name of a registerd callback.
+ *
+ * @private
+ * @example
+ *
+ * CellToolbar.register_callback('foo.c1', function(div, cell){...});
+ * CellToolbar.register_callback('foo.c2', function(div, cell){...});
+ * CellToolbar.register_callback('foo.c3', function(div, cell){...});
+ * CellToolbar.register_callback('foo.c4', function(div, cell){...});
+ * CellToolbar.register_callback('foo.c5', function(div, cell){...});
+ *
+ * CellToolbar.register_preset('foo.foo_preset1', ['foo.c1', 'foo.c2', 'foo.c5'])
+ * CellToolbar.register_preset('foo.foo_preset2', ['foo.c4', 'foo.c5'])
+ */
+ CellToolbar.register_preset = function(name, preset_list, notebook) {
+ CellToolbar._presets[name] = preset_list;
+ events.trigger('preset_added.CellToolbar', {name: name});
+ // When "register_callback" is called by a custom extension, it may be executed after notebook is loaded.
+ // In that case, activate the preset if needed.
+ if (notebook && notebook.metadata && notebook.metadata.celltoolbar === name){
+ CellToolbar.activate_preset(name);
+ }
+ };
+
+ /**
+ * unregister the selected preset,
+ *
+ * return true if preset successfully unregistered
+ * false otherwise
+ *
+ **/
+ CellToolbar.unregister_preset = function(name){
+ if(CellToolbar._presets[name]){
+ delete CellToolbar._presets[name];
+ events.trigger('unregistered_preset.CellToolbar', {name: name});
+ return true
+ }
+ return false
+ }
+
+
+ /**
+ * List the names of the presets that are currently registered.
+ *
+ * @method list_presets
+ * @static
+ */
+ CellToolbar.list_presets = function() {
+ var keys = [];
+ for (var k in CellToolbar._presets) {
+ keys.push(k);
+ }
+ return keys;
+ };
+
+
+ /**
+ * Activate an UI preset from `register_preset`
+ *
+ * This does not update the selection UI.
+ *
+ * @method activate_preset
+ * @param preset_name {String} string corresponding to the preset name
+ *
+ * @static
+ * @private
+ * @example
+ *
+ * CellToolbar.activate_preset('foo.foo_preset1');
+ */
+ CellToolbar.activate_preset = function(preset_name){
+ var preset = CellToolbar._presets[preset_name];
+
+ if(preset !== undefined){
+ CellToolbar._ui_controls_list = preset;
+ CellToolbar.rebuild_all();
+ }
+
+ events.trigger('preset_activated.CellToolbar', {name: preset_name});
+ };
+
+
+ /**
+ * This should be called on the class and not on a instance as it will trigger
+ * rebuild of all the instances.
+ * @method rebuild_all
+ * @static
+ *
+ */
+ CellToolbar.rebuild_all = function(){
+ for(var i=0; i < CellToolbar._instances.length; i++){
+ CellToolbar._instances[i].rebuild();
+ }
+ };
+
+ /**
+ * Rebuild all the button on the toolbar to update its state.
+ * @method rebuild
+ */
+ CellToolbar.prototype.rebuild = function(){
+ /**
+ * strip evrything from the div
+ * which is probably inner_element
+ * or this.element.
+ */
+ this.inner_element.empty();
+ this.ui_controls_list = [];
+
+ var callbacks = CellToolbar._callback_dict;
+ var preset = CellToolbar._ui_controls_list;
+ // Yes we iterate on the class variable, not the instance one.
+ for (var i=0; i < preset.length; i++) {
+ var key = preset[i];
+ var callback = callbacks[key];
+ if (!callback) continue;
+
+ if (typeof callback === 'object') {
+ if (callback.cell_types.indexOf(this.cell.cell_type) === -1) continue;
+ callback = callback.callback;
+ }
+
+ var local_div = $('<div/>').addClass('button_container');
+ try {
+ callback(local_div, this.cell, this);
+ this.ui_controls_list.push(key);
+ } catch (e) {
+ console.log("Error in cell toolbar callback " + key, e);
+ continue;
+ }
+ // only append if callback succeeded.
+ this.inner_element.append(local_div);
+ }
+
+ // If there are no controls or the cell is a rendered TextCell hide the toolbar.
+ if (!this.ui_controls_list.length) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ };
+
+
+ CellToolbar.utils = {};
+
+
+ /**
+ * A utility function to generate bindings between a checkbox and cell/metadata
+ * @method utils.checkbox_ui_generator
+ * @static
+ *
+ * @param name {string} Label in front of the checkbox
+ * @param setter {function( cell, newValue )}
+ * A setter method to set the newValue
+ * @param getter {function( cell )}
+ * A getter methods which return the current value.
+ *
+ * @return callback {function( div, cell )} Callback to be passed to `register_callback`
+ *
+ * @example
+ *
+ * An exmple that bind the subkey `slideshow.isSectionStart` to a checkbox with a `New Slide` label
+ *
+ * var newSlide = CellToolbar.utils.checkbox_ui_generator('New Slide',
+ * // setter
+ * function(cell, value){
+ * // we check that the slideshow namespace exist and create it if needed
+ * if (cell.metadata.slideshow == undefined){cell.metadata.slideshow = {}}
+ * // set the value
+ * cell.metadata.slideshow.isSectionStart = value
+ * },
+ * //geter
+ * function(cell){ var ns = cell.metadata.slideshow;
+ * // if the slideshow namespace does not exist return `undefined`
+ * // (will be interpreted as `false` by checkbox) otherwise
+ * // return the value
+ * return (ns == undefined)? undefined: ns.isSectionStart
+ * }
+ * );
+ *
+ * CellToolbar.register_callback('newSlide', newSlide);
+ *
+ */
+ CellToolbar.utils.checkbox_ui_generator = function(name, setter, getter){
+ return function(div, cell, celltoolbar) {
+ var button_container = $(div);
+
+ var chkb = $('<input/>').attr('type', 'checkbox');
+ var lbl = $('<label/>').append($('<span/>').text(name));
+ chkb.attr("checked", getter(cell));
+
+ chkb.click(function(){
+ var v = getter(cell);
+ setter(cell, !v);
+ chkb.attr("checked", !v);
+ });
+ button_container.append($('<span/>').append(lbl).append(chkb));
+ };
+ };
+
+
+ /**
+ * A utility function to generate bindings between a input field and cell/metadata
+ * @method utils.input_ui_generator
+ * @static
+ *
+ * @param name {string} Label in front of the input field
+ * @param setter {function( cell, newValue )}
+ * A setter method to set the newValue
+ * @param getter {function( cell )}
+ * A getter methods which return the current value.
+ *
+ * @return callback {function( div, cell )} Callback to be passed to `register_callback`
+ *
+ */
+ CellToolbar.utils.input_ui_generator = function(name, setter, getter){
+ return function(div, cell, celltoolbar) {
+ var button_container = $(div);
+
+ var text = $('<input/>').attr('type', 'text');
+ var lbl = $('<label/>').append($('<span/>').text(name));
+ text.attr("value", getter(cell));
+
+ text.keyup(function(){
+ setter(cell, text.val());
+ });
+ button_container.append($('<span/>').append(lbl).append(text));
+ IPython.keyboard_manager.register_events(text);
+ };
+ };
+
+ /**
+ * A utility function to generate bindings between a dropdown list cell
+ * @method utils.select_ui_generator
+ * @static
+ *
+ * @param list_list {list_of_sublist} List of sublist of metadata value and name in the dropdown list.
+ * subslit shoud contain 2 element each, first a string that woul be displayed in the dropdown list,
+ * and second the corresponding value to be passed to setter/return by getter. the corresponding value
+ * should not be "undefined" or behavior can be unexpected.
+ * @param setter {function( cell, newValue )}
+ * A setter method to set the newValue
+ * @param getter {function( cell )}
+ * A getter methods which return the current value of the metadata.
+ * @param [label=""] {String} optionnal label for the dropdown menu
+ *
+ * @return callback {function( div, cell )} Callback to be passed to `register_callback`
+ *
+ * @example
+ *
+ * var select_type = CellToolbar.utils.select_ui_generator([
+ * ["<None>" , "None" ],
+ * ["Header Slide" , "header_slide" ],
+ * ["Slide" , "slide" ],
+ * ["Fragment" , "fragment" ],
+ * ["Skip" , "skip" ],
+ * ],
+ * // setter
+ * function(cell, value){
+ * // we check that the slideshow namespace exist and create it if needed
+ * if (cell.metadata.slideshow == undefined){cell.metadata.slideshow = {}}
+ * // set the value
+ * cell.metadata.slideshow.slide_type = value
+ * },
+ * //geter
+ * function(cell){ var ns = cell.metadata.slideshow;
+ * // if the slideshow namespace does not exist return `undefined`
+ * // (will be interpreted as `false` by checkbox) otherwise
+ * // return the value
+ * return (ns == undefined)? undefined: ns.slide_type
+ * }
+ * CellToolbar.register_callback('slideshow.select', select_type);
+ *
+ */
+ CellToolbar.utils.select_ui_generator = function(list_list, setter, getter, label) {
+ label = label || "";
+ return function(div, cell, celltoolbar) {
+ var button_container = $(div);
+ var lbl = $("<label/>").append($('<span/>').text(label));
+ var select = $('<select/>');
+ for(var i=0; i < list_list.length; i++){
+ var opt = $('<option/>')
+ .attr('value', list_list[i][1])
+ .text(list_list[i][0]);
+ select.append(opt);
+ }
+ select.val(getter(cell));
+ select.change(function(){
+ setter(cell, select.val());
+ });
+ button_container.append($('<span/>').append(lbl).append(select));
+ };
+ };
+
+ // Backwards compatability.
+ IPython.CellToolbar = CellToolbar;
+
+ return {'CellToolbar': CellToolbar};
+});
diff --git a/notebook/static/notebook/js/celltoolbarpresets/default.js b/notebook/static/notebook/js/celltoolbarpresets/default.js
new file mode 100644
index 0000000..fe457f6
--- /dev/null
+++ b/notebook/static/notebook/js/celltoolbarpresets/default.js
@@ -0,0 +1,51 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'notebook/js/celltoolbar',
+ 'base/js/dialog',
+], function($, celltoolbar, dialog) {
+ "use strict";
+
+ var CellToolbar = celltoolbar.CellToolbar;
+
+ var raw_edit = function (cell) {
+ dialog.edit_metadata({
+ md: cell.metadata,
+ callback: function (md) {
+ cell.metadata = md;
+ },
+ name: 'Cell',
+ notebook: this.notebook,
+ keyboard_manager: this.keyboard_manager
+ });
+ };
+
+ var add_raw_edit_button = function(div, cell) {
+ var button_container = $(div);
+ var button = $('<button/>')
+ .addClass("btn btn-default btn-xs")
+ .text("Edit Metadata")
+ .click( function () {
+ raw_edit(cell);
+ return false;
+ });
+ button_container.append(button);
+ };
+
+ var register = function (notebook) {
+ CellToolbar.register_callback('default.rawedit', add_raw_edit_button);
+ raw_edit = $.proxy(raw_edit, {
+ notebook: notebook,
+ keyboard_manager: notebook.keyboard_manager
+ });
+
+ var example_preset = [];
+ example_preset.push('default.rawedit');
+
+ CellToolbar.register_preset('Edit Metadata', example_preset, notebook);
+ console.log('Default extension for cell metadata editing loaded.');
+ };
+ return {'register': register};
+});
diff --git a/notebook/static/notebook/js/celltoolbarpresets/example.js b/notebook/static/notebook/js/celltoolbarpresets/example.js
new file mode 100644
index 0000000..0e63170
--- /dev/null
+++ b/notebook/static/notebook/js/celltoolbarpresets/example.js
@@ -0,0 +1,150 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+// Example Use for the CellToolbar library
+// add the following to your custom.js to load
+// Celltoolbar UI for slideshow
+
+// ```
+// $.getScript('/static/js/celltoolbarpresets/example.js');
+// ```
+define([
+ 'jquery',
+ 'notebook/js/celltoolbar',
+], function($, celltoolbar) {
+ "use strict";
+
+ var CellToolbar = celltoolbar.CellToolbar;
+
+ var example_preset = [];
+
+ var simple_button = function(div, cell) {
+ var button_container = $(div);
+ var button = $('<div/>').button({icons:{primary:'ui-icon-locked'}});
+ var fun = function(value){
+ try{
+ if(value){
+ cell.code_mirror.setOption('readOnly','nocursor');
+ button.button('option','icons',{primary:'ui-icon-locked'});
+ } else {
+ cell.code_mirror.setOption('readOnly',false);
+ button.button('option','icons',{primary:'ui-icon-unlocked'});
+ }
+ } catch(e){}
+
+ };
+ fun(cell.metadata.ro);
+ button.click(function(){
+ var v = cell.metadata.ro;
+ var locked = !v;
+ cell.metadata.ro = locked;
+ fun(locked);
+ })
+ .css('height','16px')
+ .css('width','35px');
+ button_container.append(button);
+ };
+
+ CellToolbar.register_callback('example.lock',simple_button);
+ example_preset.push('example.lock');
+
+ var toggle_test = function(div, cell) {
+ var button_container = $(div);
+ var button = $('<div/>')
+ .button({label:String(cell.metadata.foo)}).
+ css('width','65px');
+ button.click(function(){
+ var v = cell.metadata.foo;
+ cell.metadata.foo = !v;
+ button.button("option","label",String(!v));
+ });
+ button_container.append(button);
+ };
+
+ CellToolbar.register_callback('example.toggle',toggle_test);
+ example_preset.push('example.toggle');
+
+ var checkbox_test = CellToolbar.utils.checkbox_ui_generator('Yes/No',
+ // setter
+ function(cell, value){
+ // we check that the slideshow namespace exist and create it if needed
+ if (cell.metadata.yn_test === undefined){cell.metadata.yn_test = {};}
+ // set the value
+ cell.metadata.yn_test.value = value;
+ },
+ //geter
+ function(cell){ var ns = cell.metadata.yn_test;
+ // if the slideshow namespace does not exist return `undefined`
+ // (will be interpreted as `false` by checkbox) otherwise
+ // return the value
+ return (ns === undefined)? undefined: ns.value;
+ }
+ );
+
+
+ CellToolbar.register_callback('example.checkbox',checkbox_test);
+ example_preset.push('example.checkbox');
+
+ var select_test = CellToolbar.utils.select_ui_generator([
+ ["-" ,undefined ],
+ ["Header Slide" ,"header_slide" ],
+ ["Slide" ,"slide" ],
+ ["Fragment" ,"fragment" ],
+ ["Skip" ,"skip" ],
+ ],
+ // setter
+ function(cell,value){
+ // we check that the slideshow namespace exist and create it if needed
+ if (cell.metadata.test === undefined){cell.metadata.test = {};}
+ // set the value
+ cell.metadata.test.slide_type = value;
+ },
+ //geter
+ function(cell){ var ns = cell.metadata.test;
+ // if the slideshow namespace does not exist return `undefined`
+ // (will be interpreted as `false` by checkbox) otherwise
+ // return the value
+ return (ns === undefined)? undefined: ns.slide_type;
+ });
+
+ CellToolbar.register_callback('example.select',select_test);
+ example_preset.push('example.select');
+
+ var simple_dialog = function(title,text){
+ var dlg = $('<div/>').attr('title',title)
+ .append($('<p/>').text(text));
+ $(dlg).dialog({
+ autoOpen: true,
+ height: 300,
+ width: 650,
+ modal: true,
+ close: function() {
+ /**
+ *cleanup on close
+ */
+ $(this).remove();
+ }
+ });
+ };
+
+ var add_simple_dialog_button = function(div, cell) {
+ var help_text = ["This is the Metadata editting UI.",
+ "It heavily rely on plugin to work ",
+ "and is still under developpement. You shouldn't wait too long before",
+ " seeing some customisable buttons in those toolbar."
+ ].join('\n');
+ var button_container = $(div);
+ var button = $('<div/>').button({label:'?'})
+ .click(function(){simple_dialog('help',help_text); return false;});
+ button_container.append(button);
+ };
+
+ var register = function (notebook) {
+ CellToolbar.register_callback('example.help',add_simple_dialog_button);
+ example_preset.push('example.help');
+
+ CellToolbar.register_preset('Example',example_preset, notebook);
+ console.log('Example extension for metadata editing loaded.');
+ };
+ return {'register': register};
+});
diff --git a/notebook/static/notebook/js/celltoolbarpresets/rawcell.js b/notebook/static/notebook/js/celltoolbarpresets/rawcell.js
new file mode 100644
index 0000000..f3e2345
--- /dev/null
+++ b/notebook/static/notebook/js/celltoolbarpresets/rawcell.js
@@ -0,0 +1,86 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'notebook/js/celltoolbar',
+ 'base/js/dialog',
+ 'base/js/keyboard',
+], function($, celltoolbar, dialog, keyboard) {
+ "use strict";
+
+ var CellToolbar = celltoolbar.CellToolbar;
+ var raw_cell_preset = [];
+
+ var select_type = CellToolbar.utils.select_ui_generator([
+ ["None", "-"],
+ ["LaTeX", "text/latex"],
+ ["reST", "text/restructuredtext"],
+ ["HTML", "text/html"],
+ ["Markdown", "text/markdown"],
+ ["Python", "text/x-python"],
+ ["Custom", "dialog"],
+
+ ],
+ // setter
+ function(cell, value) {
+ if (value === "-") {
+ delete cell.metadata.raw_mimetype;
+ } else if (value === 'dialog'){
+ var dialog = $('<div/>').append(
+ $("<p/>")
+ .text("Set the MIME type of the raw cell:")
+ ).append(
+ $("<br/>")
+ ).append(
+ $('<input/>').attr('type','text').attr('size','25')
+ .val(cell.metadata.raw_mimetype || "-")
+ );
+ dialog.modal({
+ title: "Raw Cell MIME Type",
+ body: dialog,
+ buttons : {
+ "Cancel": {},
+ "OK": {
+ class: "btn-primary",
+ click: function () {
+ console.log(cell);
+ cell.metadata.raw_mimetype = $(this).find('input').val();
+ console.log(cell.metadata);
+ }
+ }
+ },
+ open : function (event, ui) {
+ var that = $(this);
+ // Upon ENTER, click the OK button.
+ that.find('input[type="text"]').keydown(function (event, ui) {
+ if (event.which === keyboard.keycodes.enter) {
+ that.find('.btn-primary').first().click();
+ return false;
+ }
+ });
+ that.find('input[type="text"]').focus().select();
+ }
+ });
+ } else {
+ cell.metadata.raw_mimetype = value;
+ }
+ },
+ //getter
+ function(cell) {
+ return cell.metadata.raw_mimetype || "";
+ },
+ // name
+ "Raw NBConvert Format"
+ );
+
+ var register = function (notebook) {
+ CellToolbar.register_callback('raw_cell.select', select_type, ['raw']);
+ raw_cell_preset.push('raw_cell.select');
+
+ CellToolbar.register_preset('Raw Cell Format', raw_cell_preset, notebook);
+ console.log('Raw Cell Format toolbar preset loaded.');
+ };
+ return {'register': register};
+
+});
diff --git a/notebook/static/notebook/js/celltoolbarpresets/slideshow.js b/notebook/static/notebook/js/celltoolbarpresets/slideshow.js
new file mode 100644
index 0000000..50dc96d
--- /dev/null
+++ b/notebook/static/notebook/js/celltoolbarpresets/slideshow.js
@@ -0,0 +1,46 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'notebook/js/celltoolbar',
+], function($, celltoolbar) {
+ "use strict";
+
+
+ var CellToolbar = celltoolbar.CellToolbar;
+ var slideshow_preset = [];
+
+ var select_type = CellToolbar.utils.select_ui_generator([
+ ["-" ,"-" ],
+ ["Slide" ,"slide" ],
+ ["Sub-Slide" ,"subslide" ],
+ ["Fragment" ,"fragment" ],
+ ["Skip" ,"skip" ],
+ ["Notes" ,"notes" ],
+ ],
+ // setter
+ function(cell, value){
+ // we check that the slideshow namespace exist and create it if needed
+ if (cell.metadata.slideshow === undefined){cell.metadata.slideshow = {};}
+ // set the value
+ cell.metadata.slideshow.slide_type = value;
+ },
+ //geter
+ function(cell){ var ns = cell.metadata.slideshow;
+ // if the slideshow namespace does not exist return `undefined`
+ // (will be interpreted as `false` by checkbox) otherwise
+ // return the value
+ return (ns === undefined)? undefined: ns.slide_type;
+ },
+ "Slide Type");
+
+ var register = function (notebook) {
+ CellToolbar.register_callback('slideshow.select',select_type);
+ slideshow_preset.push('slideshow.select');
+
+ CellToolbar.register_preset('Slideshow',slideshow_preset, notebook);
+ console.log('Slideshow extension for metadata editing loaded.');
+ };
+ return {'register': register};
+});
diff --git a/notebook/static/notebook/js/codecell.js b/notebook/static/notebook/js/codecell.js
new file mode 100644
index 0000000..df0958d
--- /dev/null
+++ b/notebook/static/notebook/js/codecell.js
@@ -0,0 +1,569 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+/**
+ *
+ *
+ * @module codecell
+ * @namespace codecell
+ * @class CodeCell
+ */
+
+
+define([
+ 'base/js/namespace',
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/keyboard',
+ 'services/config',
+ 'notebook/js/cell',
+ 'notebook/js/outputarea',
+ 'notebook/js/completer',
+ 'notebook/js/celltoolbar',
+ 'codemirror/lib/codemirror',
+ 'codemirror/mode/python/python',
+ 'notebook/js/codemirror-ipython'
+], function(IPython,
+ $,
+ utils,
+ keyboard,
+ configmod,
+ cell,
+ outputarea,
+ completer,
+ celltoolbar,
+ CodeMirror,
+ cmpython,
+ cmip
+ ) {
+ "use strict";
+
+ var Cell = cell.Cell;
+
+ /* local util for codemirror */
+ var posEq = function(a, b) {return a.line === b.line && a.ch === b.ch;};
+
+ /**
+ *
+ * function to delete until previous non blanking space character
+ * or first multiple of 4 tabstop.
+ * @private
+ */
+ CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
+ var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
+ if (sel) {
+ var ranges = cm.listSelections();
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var head = ranges[i].head;
+ var anchor = ranges[i].anchor;
+ cm.replaceRange("", CodeMirror.Pos(head.line, head.ch), CodeMirror.Pos(anchor.line, anchor.ch));
+ }
+ return;
+ }
+ var cur = cm.getCursor(), line = cm.getLine(cur.line);
+ var tabsize = cm.getOption('tabSize');
+ var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
+ from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
+ var select = cm.getRange(from,cur);
+ if( select.match(/^\ +$/) !== null){
+ cm.replaceRange("",from,cur);
+ } else {
+ cm.deleteH(-1,"char");
+ }
+ };
+
+ var keycodes = keyboard.keycodes;
+
+ var CodeCell = function (kernel, options) {
+ /**
+ * Constructor
+ *
+ * A Cell conceived to write code.
+ *
+ * Parameters:
+ * kernel: Kernel instance
+ * The kernel doesn't have to be set at creation time, in that case
+ * it will be null and set_kernel has to be called later.
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * config: dictionary
+ * keyboard_manager: KeyboardManager instance
+ * notebook: Notebook instance
+ * tooltip: Tooltip instance
+ */
+ this.kernel = kernel || null;
+ this.notebook = options.notebook;
+ this.collapsed = false;
+ this.events = options.events;
+ this.tooltip = options.tooltip;
+ this.config = options.config;
+ this.class_config = new configmod.ConfigWithDefaults(this.config,
+ CodeCell.config_defaults, 'CodeCell');
+
+ // create all attributed in constructor function
+ // even if null for V8 VM optimisation
+ this.input_prompt_number = null;
+ this.celltoolbar = null;
+ this.output_area = null;
+
+ this.last_msg_id = null;
+ this.completer = null;
+
+ Cell.apply(this,[{
+ config: $.extend({}, CodeCell.options_default),
+ keyboard_manager: options.keyboard_manager,
+ events: this.events}]);
+
+ // Attributes we want to override in this subclass.
+ this.cell_type = "code";
+ var that = this;
+ this.element.focusout(
+ function() { that.auto_highlight(); }
+ );
+ };
+
+ CodeCell.options_default = {
+ cm_config : {
+ extraKeys: {
+ "Tab" : "indentMore",
+ "Shift-Tab" : "indentLess",
+ "Backspace" : "delSpaceToPrevTabStop",
+ "Cmd-/" : "toggleComment",
+ "Ctrl-/" : "toggleComment"
+ },
+ mode: 'text',
+ theme: 'ipython',
+ matchBrackets: true,
+ autoCloseBrackets: true
+ },
+ highlight_modes : {
+ 'magic_javascript' :{'reg':['^%%javascript']},
+ 'magic_perl' :{'reg':['^%%perl']},
+ 'magic_ruby' :{'reg':['^%%ruby']},
+ 'magic_python' :{'reg':['^%%python3?']},
+ 'magic_shell' :{'reg':['^%%bash']},
+ 'magic_r' :{'reg':['^%%R']},
+ 'magic_text/x-cython' :{'reg':['^%%cython']},
+ },
+ };
+
+ CodeCell.config_defaults = CodeCell.options_default;
+
+ CodeCell.msg_cells = {};
+
+ CodeCell.prototype = Object.create(Cell.prototype);
+
+ /** @method create_element */
+ CodeCell.prototype.create_element = function () {
+ Cell.prototype.create_element.apply(this, arguments);
+ var that = this;
+
+ var cell = $('<div></div>').addClass('cell code_cell');
+ cell.attr('tabindex','2');
+
+ var input = $('<div></div>').addClass('input');
+ this.input = input;
+ var prompt = $('<div/>').addClass('prompt input_prompt');
+ var inner_cell = $('<div/>').addClass('inner_cell');
+ this.celltoolbar = new celltoolbar.CellToolbar({
+ cell: this,
+ notebook: this.notebook});
+ inner_cell.append(this.celltoolbar.element);
+ var input_area = $('<div/>').addClass('input_area');
+ this.code_mirror = new CodeMirror(input_area.get(0), this._options.cm_config);
+ // In case of bugs that put the keyboard manager into an inconsistent state,
+ // ensure KM is enabled when CodeMirror is focused:
+ this.code_mirror.on('focus', function () {
+ if (that.keyboard_manager) {
+ that.keyboard_manager.enable();
+ }
+ });
+ this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this));
+ $(this.code_mirror.getInputField()).attr("spellcheck", "false");
+ inner_cell.append(input_area);
+ input.append(prompt).append(inner_cell);
+
+ var output = $('<div></div>');
+ cell.append(input).append(output);
+ this.element = cell;
+ this.output_area = new outputarea.OutputArea({
+ selector: output,
+ prompt_area: true,
+ events: this.events,
+ keyboard_manager: this.keyboard_manager});
+ this.completer = new completer.Completer(this, this.events);
+ };
+
+ /** @method bind_events */
+ CodeCell.prototype.bind_events = function () {
+ Cell.prototype.bind_events.apply(this, arguments);
+ var that = this;
+
+ this.element.focusout(
+ function() { that.auto_highlight(); }
+ );
+ };
+
+
+ /**
+ * This method gets called in CodeMirror's onKeyDown/onKeyPress
+ * handlers and is used to provide custom key handling. Its return
+ * value is used to determine if CodeMirror should ignore the event:
+ * true = ignore, false = don't ignore.
+ * @method handle_codemirror_keyevent
+ */
+
+ CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
+
+ var that = this;
+ // whatever key is pressed, first, cancel the tooltip request before
+ // they are sent, and remove tooltip if any, except for tab again
+ var tooltip_closed = null;
+ if (event.type === 'keydown' && event.which !== keycodes.tab ) {
+ tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
+ }
+
+ var cur = editor.getCursor();
+ if (event.keyCode === keycodes.enter){
+ this.auto_highlight();
+ }
+
+ if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
+ // triger on keypress (!) otherwise inconsistent event.which depending on plateform
+ // browser and keyboard layout !
+ // Pressing '(' , request tooltip, don't forget to reappend it
+ // The second argument says to hide the tooltip if the docstring
+ // is actually empty
+ this.tooltip.pending(that, true);
+ } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
+ // If tooltip is active, cancel it. The call to
+ // remove_and_cancel_tooltip above doesn't pass, force=true.
+ // Because of this it won't actually close the tooltip
+ // if it is in sticky mode. Thus, we have to check again if it is open
+ // and close it with force=true.
+ if (!this.tooltip._hidden) {
+ this.tooltip.remove_and_cancel_tooltip(true);
+ }
+ // If we closed the tooltip, don't let CM or the global handlers
+ // handle this event.
+ event.codemirrorIgnore = true;
+ event._ipkmIgnore = true;
+ event.preventDefault();
+ return true;
+ } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
+ if (editor.somethingSelected() || editor.getSelections().length !== 1){
+ var anchor = editor.getCursor("anchor");
+ var head = editor.getCursor("head");
+ if( anchor.line !== head.line){
+ return false;
+ }
+ }
+ var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
+ if (pre_cursor.trim() === "") {
+ // Don't show tooltip if the part of the line before the cursor
+ // is empty. In this case, let CodeMirror handle indentation.
+ return false;
+ }
+ this.tooltip.request(that);
+ event.codemirrorIgnore = true;
+ event.preventDefault();
+ return true;
+ } else if (event.keyCode === keycodes.tab && event.type === 'keydown') {
+ // Tab completion.
+ this.tooltip.remove_and_cancel_tooltip();
+
+ // completion does not work on multicursor, it might be possible though in some cases
+ if (editor.somethingSelected() || editor.getSelections().length > 1) {
+ return false;
+ }
+ var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
+ if (pre_cursor.trim() === "") {
+ // Don't autocomplete if the part of the line before the cursor
+ // is empty. In this case, let CodeMirror handle indentation.
+ return false;
+ } else {
+ event.codemirrorIgnore = true;
+ event.preventDefault();
+ this.completer.startCompletion();
+ return true;
+ }
+ }
+
+ // keyboard event wasn't one of those unique to code cells, let's see
+ // if it's one of the generic ones (i.e. check edit mode shortcuts)
+ return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
+ };
+
+ // Kernel related calls.
+
+ CodeCell.prototype.set_kernel = function (kernel) {
+ this.kernel = kernel;
+ };
+
+ /**
+ * Execute current code cell to the kernel
+ * @method execute
+ */
+ CodeCell.prototype.execute = function (stop_on_error) {
+ if (!this.kernel) {
+ console.log("Can't execute cell since kernel is not set.");
+ return;
+ }
+
+ if (stop_on_error === undefined) {
+ stop_on_error = true;
+ }
+
+ this.output_area.clear_output(false, true);
+ var old_msg_id = this.last_msg_id;
+ if (old_msg_id) {
+ this.kernel.clear_callbacks_for_msg(old_msg_id);
+ delete CodeCell.msg_cells[old_msg_id];
+ this.last_msg_id = null;
+ }
+ if (this.get_text().trim().length === 0) {
+ // nothing to do
+ this.set_input_prompt(null);
+ return;
+ }
+ this.set_input_prompt('*');
+ this.element.addClass("running");
+ var callbacks = this.get_callbacks();
+
+ this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true,
+ stop_on_error : stop_on_error});
+ CodeCell.msg_cells[this.last_msg_id] = this;
+ this.render();
+ this.events.trigger('execute.CodeCell', {cell: this});
+ };
+
+ /**
+ * Construct the default callbacks for
+ * @method get_callbacks
+ */
+ CodeCell.prototype.get_callbacks = function () {
+ var that = this;
+ return {
+ shell : {
+ reply : $.proxy(this._handle_execute_reply, this),
+ payload : {
+ set_next_input : $.proxy(this._handle_set_next_input, this),
+ page : $.proxy(this._open_with_pager, this)
+ }
+ },
+ iopub : {
+ output : function() {
+ that.output_area.handle_output.apply(that.output_area, arguments);
+ },
+ clear_output : function() {
+ that.output_area.handle_clear_output.apply(that.output_area, arguments);
+ },
+ },
+ input : $.proxy(this._handle_input_request, this)
+ };
+ };
+
+ CodeCell.prototype._open_with_pager = function (payload) {
+ this.events.trigger('open_with_text.Pager', payload);
+ };
+
+ /**
+ * @method _handle_execute_reply
+ * @private
+ */
+ CodeCell.prototype._handle_execute_reply = function (msg) {
+ this.set_input_prompt(msg.content.execution_count);
+ this.element.removeClass("running");
+ this.events.trigger('set_dirty.Notebook', {value: true});
+ };
+
+ /**
+ * @method _handle_set_next_input
+ * @private
+ */
+ CodeCell.prototype._handle_set_next_input = function (payload) {
+ var data = {
+ cell: this,
+ text: payload.text,
+ replace: payload.replace,
+ clear_output: payload.clear_output,
+ };
+ this.events.trigger('set_next_input.Notebook', data);
+ };
+
+ /**
+ * @method _handle_input_request
+ * @private
+ */
+ CodeCell.prototype._handle_input_request = function (msg) {
+ this.output_area.append_raw_input(msg);
+ };
+
+
+ // Basic cell manipulation.
+
+ CodeCell.prototype.select = function () {
+ var cont = Cell.prototype.select.apply(this, arguments);
+ if (cont) {
+ this.code_mirror.refresh();
+ this.auto_highlight();
+ }
+ return cont;
+ };
+
+ CodeCell.prototype.render = function () {
+ var cont = Cell.prototype.render.apply(this, arguments);
+ // Always execute, even if we are already in the rendered state
+ return cont;
+ };
+
+ CodeCell.prototype.select_all = function () {
+ var start = {line: 0, ch: 0};
+ var nlines = this.code_mirror.lineCount();
+ var last_line = this.code_mirror.getLine(nlines-1);
+ var end = {line: nlines-1, ch: last_line.length};
+ this.code_mirror.setSelection(start, end);
+ };
+
+
+ CodeCell.prototype.collapse_output = function () {
+ this.output_area.collapse();
+ };
+
+
+ CodeCell.prototype.expand_output = function () {
+ this.output_area.expand();
+ this.output_area.unscroll_area();
+ };
+
+ CodeCell.prototype.scroll_output = function () {
+ this.output_area.expand();
+ this.output_area.scroll_if_long();
+ };
+
+ CodeCell.prototype.toggle_output = function () {
+ this.output_area.toggle_output();
+ };
+
+ CodeCell.prototype.toggle_output_scroll = function () {
+ this.output_area.toggle_scroll();
+ };
+
+
+ CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
+ var ns;
+ if (prompt_value === undefined || prompt_value === null) {
+ ns = "&nbsp;";
+ } else {
+ ns = encodeURIComponent(prompt_value);
+ }
+ return 'In&nbsp;[' + ns + ']:';
+ };
+
+ CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
+ var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
+ for(var i=1; i < lines_number; i++) {
+ html.push(['...:']);
+ }
+ return html.join('<br/>');
+ };
+
+ CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
+
+
+ CodeCell.prototype.set_input_prompt = function (number) {
+ var nline = 1;
+ if (this.code_mirror !== undefined) {
+ nline = this.code_mirror.lineCount();
+ }
+ this.input_prompt_number = number;
+ var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
+ // This HTML call is okay because the user contents are escaped.
+ this.element.find('div.input_prompt').html(prompt_html);
+ };
+
+
+ CodeCell.prototype.clear_input = function () {
+ this.code_mirror.setValue('');
+ };
+
+
+ CodeCell.prototype.get_text = function () {
+ return this.code_mirror.getValue();
+ };
+
+
+ CodeCell.prototype.set_text = function (code) {
+ return this.code_mirror.setValue(code);
+ };
+
+
+ CodeCell.prototype.clear_output = function (wait) {
+ this.output_area.clear_output(wait);
+ this.set_input_prompt();
+ };
+
+
+ // JSON serialization
+
+ CodeCell.prototype.fromJSON = function (data) {
+ Cell.prototype.fromJSON.apply(this, arguments);
+ if (data.cell_type === 'code') {
+ if (data.source !== undefined) {
+ this.set_text(data.source);
+ // make this value the starting point, so that we can only undo
+ // to this state, instead of a blank cell
+ this.code_mirror.clearHistory();
+ this.auto_highlight();
+ }
+ this.set_input_prompt(data.execution_count);
+ this.output_area.trusted = data.metadata.trusted || false;
+ this.output_area.fromJSON(data.outputs, data.metadata);
+ }
+ };
+
+
+ CodeCell.prototype.toJSON = function () {
+ var data = Cell.prototype.toJSON.apply(this);
+ data.source = this.get_text();
+ // is finite protect against undefined and '*' value
+ if (isFinite(this.input_prompt_number)) {
+ data.execution_count = this.input_prompt_number;
+ } else {
+ data.execution_count = null;
+ }
+ var outputs = this.output_area.toJSON();
+ data.outputs = outputs;
+ data.metadata.trusted = this.output_area.trusted;
+ data.metadata.collapsed = this.output_area.collapsed;
+ if (this.output_area.scroll_state === 'auto') {
+ delete data.metadata.scrolled;
+ } else {
+ data.metadata.scrolled = this.output_area.scroll_state;
+ }
+ return data;
+ };
+
+ /**
+ * handle cell level logic when the cell is unselected
+ * @method unselect
+ * @return is the action being taken
+ */
+ CodeCell.prototype.unselect = function() {
+ var cont = Cell.prototype.unselect.apply(this, arguments);
+ if (cont) {
+ // When a code cell is unselected, make sure that the corresponding
+ // tooltip and completer to that cell is closed.
+ this.tooltip.remove_and_cancel_tooltip(true);
+ if (this.completer !== null) {
+ this.completer.close();
+ }
+ }
+ return cont;
+ };
+
+ // Backwards compatability.
+ IPython.CodeCell = CodeCell;
+
+ return {'CodeCell': CodeCell};
+});
diff --git a/notebook/static/notebook/js/codemirror-ipython.js b/notebook/static/notebook/js/codemirror-ipython.js
new file mode 100644
index 0000000..b58229a
--- /dev/null
+++ b/notebook/static/notebook/js/codemirror-ipython.js
@@ -0,0 +1,38 @@
+// IPython mode is just a slightly altered Python Mode with `?` beeing a extra
+// single operator. Here we define `ipython` mode in the require `python`
+// callback to auto-load python mode, which is more likely not the best things
+// to do, but at least the simple one for now.
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object"){ // CommonJS
+ mod(require("codemirror/lib/codemirror"),
+ require("codemirror/mode/python/python")
+ );
+ } else if (typeof define == "function" && define.amd){ // AMD
+ define(["codemirror/lib/codemirror",
+ "codemirror/mode/python/python"], mod);
+ } else {// Plain browser env
+ mod(CodeMirror);
+ }
+})(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineMode("ipython", function(conf, parserConf) {
+ var pythonConf = {};
+ for (var prop in parserConf) {
+ if (parserConf.hasOwnProperty(prop)) {
+ pythonConf[prop] = parserConf[prop];
+ }
+ }
+ pythonConf.name = 'python';
+ pythonConf.singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!\\?]");
+ if (pythonConf.version === 3) {
+ pythonConf.identifiers = new RegExp("^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*");
+ } else if (pythonConf.version === 2) {
+ pythonConf.identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
+ }
+ return CodeMirror.getMode(conf, pythonConf);
+ }, 'python');
+
+ CodeMirror.defineMIME("text/x-ipython", "ipython");
+})
diff --git a/notebook/static/notebook/js/codemirror-ipythongfm.js b/notebook/static/notebook/js/codemirror-ipythongfm.js
new file mode 100644
index 0000000..9a6bbc3
--- /dev/null
+++ b/notebook/static/notebook/js/codemirror-ipythongfm.js
@@ -0,0 +1,62 @@
+// IPython GFM (GitHub Flavored Markdown) mode is just a slightly altered GFM
+// Mode with support for latex.
+//
+// Latex support was supported by Codemirror GFM as of
+// https://github.com/codemirror/CodeMirror/pull/567
+// But was later removed in
+// https://github.com/codemirror/CodeMirror/commit/d9c9f1b1ffe984aee41307f3e927f80d1f23590c
+
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object"){ // CommonJS
+ mod(require("codemirror/lib/codemirror")
+ ,require("codemirror/addon/mode/multiplex")
+ ,require("codemirror/mode/gfm/gfm")
+ ,require("codemirror/mode/stex/stex")
+ );
+ } else if (typeof define == "function" && define.amd){ // AMD
+ define(["codemirror/lib/codemirror"
+ ,"codemirror/addon/mode/multiplex"
+ ,"codemirror/mode/python/python"
+ ,"codemirror/mode/stex/stex"
+ ], mod);
+ } else {// Plain browser env
+ mod(CodeMirror);
+ }
+})( function(CodeMirror){
+ "use strict";
+
+ CodeMirror.defineMode("ipythongfm", function(config, parserConfig) {
+
+ var gfm_mode = CodeMirror.getMode(config, "gfm");
+ var tex_mode = CodeMirror.getMode(config, "stex");
+
+ return CodeMirror.multiplexingMode(
+ gfm_mode,
+ {
+ open: "$", close: "$",
+ mode: tex_mode,
+ delimStyle: "delimit"
+ },
+ {
+ // not sure this works as $$ is interpreted at (opening $, closing $, as defined just above)
+ open: "$$", close: "$$",
+ mode: tex_mode,
+ delimStyle: "delimit"
+ },
+ {
+ open: "\\(", close: "\\)",
+ mode: tex_mode,
+ delimStyle: "delimit"
+ },
+ {
+ open: "\\[", close: "\\]",
+ mode: tex_mode,
+ delimStyle: "delimit"
+ }
+ // .. more multiplexed styles can follow here
+ );
+ }, 'gfm');
+
+ CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
+})
diff --git a/notebook/static/notebook/js/commandpalette.js b/notebook/static/notebook/js/commandpalette.js
new file mode 100644
index 0000000..b97ce10
--- /dev/null
+++ b/notebook/static/notebook/js/commandpalette.js
@@ -0,0 +1,187 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define(function(require){
+ "use strict";
+
+ var QH = require("notebook/js/quickhelp");
+ var $ = require("jquery");
+
+ /**
+ * Humanize the action name to be consumed by user.
+ * internaly the actions anem are of the form
+ * <namespace>:<description-with-dashes>
+ * we drop <namesapce> and replace dashes for space.
+ */
+ var humanize_action_id = function(str) {
+ return str.split(':')[1].replace(/-/g, ' ').replace(/_/g, '-');
+ };
+
+ /**
+ * given an action id return 'command-shortcut', 'edit-shortcut' or 'no-shortcut'
+ * for the action. This allows us to tag UI in order to visually distinguish
+ * wether an action have a keybinding or not.
+ **/
+ var get_mode_for_action_id = function(name, notebook) {
+ var shortcut = notebook.keyboard_manager.command_shortcuts.get_action_shortcut(name);
+ if (shortcut) {
+ return 'command-shortcut';
+ }
+ shortcut = notebook.keyboard_manager.edit_shortcuts.get_action_shortcut(name);
+ if (shortcut) {
+ return 'edit-shortcut';
+ }
+ return 'no-shortcut';
+ };
+
+ var CommandPalette = function(notebook) {
+ if(!notebook){
+ throw new Error("CommandPalette takes a notebook non-null mandatory arguement");
+ }
+
+ // typeahead lib need a specific layout with specific class names.
+ // the following just does that
+ var form = $('<form/>');
+ var container = $('<div/>').addClass('typeahead-container');
+ var field = $('<div/>').addClass('typeahead-field');
+ var input = $('<input/>').attr('type', 'search');
+
+ field
+ .append(
+ $('<span>').addClass('typeahead-query').append(
+ input
+ )
+ )
+ .append(
+ $('<span/>').addClass('typeahead-button').append(
+ $('<button/>').attr('type', 'submit').append(
+ $('<span/>').addClass('typeahead-search-icon')
+ )
+ )
+ );
+
+ container.append(field);
+ form.append(container);
+
+
+ var mod = $('<div/>').addClass('modal cmd-palette').append(
+ $('<div/>').addClass('modal-dialog')
+ .append(
+ $('<div/>').addClass('modal-content').append(
+ $('<div/>').addClass('modal-body')
+ .append(
+ form
+ )
+ )
+ )
+ )
+ // end setting up right layout
+ .modal({show: false, backdrop:true})
+ .on('shown.bs.modal', function () {
+ // click on button trigger de-focus on mouse up.
+ // or somethign like that.
+ setTimeout(function(){input.focus();}, 100);
+ });
+
+ notebook.keyboard_manager.disable();
+
+ var before_close = function() {
+ // little trick to trigger early in onsubmit
+ // when the action called pop-up a dialog
+ // insure this function is only called once
+ if (before_close.ok) {
+ return;
+ }
+ var cell = notebook.get_selected_cell();
+ if (cell) {
+ cell.select();
+ }
+ if (notebook.keyboard_manager) {
+ notebook.keyboard_manager.enable();
+ notebook.keyboard_manager.command_mode();
+ }
+ before_close.ok = true; // avoid double call.
+ };
+
+ mod.on("hide.bs.modal", before_close);
+
+
+ // will be trigger when user select action
+ var onSubmit = function(node, query, result, resultCount) {
+ if (actions.indexOf(result.key) >= 0) {
+ before_close();
+ notebook.keyboard_manager.actions.call(result.key);
+ } else {
+ console.warning("No command " + result.key);
+ }
+ mod.modal('hide');
+ };
+
+ /* Whenever a result is rendered, if there is only one resulting
+ * element then automatically select that element.
+ */
+ var onResult = function(node, query, result, resultCount) {
+ if (resultCount == 1) {
+ requestAnimationFrame(function() {
+ $('.typeahead-list > li:nth-child(2)').addClass('active');
+ });
+ }
+ };
+
+ // generate structure needed for typeahead layout and ability to search
+ var src = {};
+
+ var actions = Object.keys(notebook.keyboard_manager.actions._actions);
+
+ for (var i = 0; i < actions.length; i++) {
+ var action_id = actions[i];
+ var action = notebook.keyboard_manager.actions.get(action_id);
+ var group = action_id.split(':')[0];
+
+ src[group] = src[group] || {
+ data: [],
+ display: 'display'
+ };
+
+ var short = notebook.keyboard_manager.command_shortcuts.get_action_shortcut(action_id) ||
+ notebook.keyboard_manager.edit_shortcuts.get_action_shortcut(action_id);
+ if (short) {
+ short = QH.humanize_sequence(short);
+ }
+
+ src[group].data.push({
+ display: humanize_action_id(action_id),
+ shortcut: short,
+ mode_shortcut: get_mode_for_action_id(action_id, notebook),
+ group: group,
+ icon: action.icon,
+ help: action.help,
+ key: action_id,
+ });
+ }
+
+ // now src is the right structure for typeahead
+
+ input.typeahead({
+ emptyTemplate: "No results found for <pre>{{query}}</pre>",
+ maxItem: 1e3,
+ minLength: 0,
+ hint: true,
+ group: ["group", "{{group}} command group"],
+ searchOnFocus: true,
+ mustSelectItem: true,
+ template: '<i class="fa fa-icon {{icon}}"></i>{{display}} <div class="pull-right {{mode_shortcut}}">{{shortcut}}</div>',
+ order: "asc",
+ source: src,
+ callback: {
+ onSubmit: onSubmit,
+ onClickAfter: onSubmit,
+ onResult: onResult
+ },
+ debug: false,
+ });
+
+ mod.modal('show');
+ };
+ return {'CommandPalette': CommandPalette};
+});
diff --git a/notebook/static/notebook/js/completer.js b/notebook/static/notebook/js/completer.js
new file mode 100644
index 0000000..3ac2b3e
--- /dev/null
+++ b/notebook/static/notebook/js/completer.js
@@ -0,0 +1,412 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/keyboard',
+ 'notebook/js/contexthint',
+ 'codemirror/lib/codemirror',
+], function($, utils, keyboard, CodeMirror) {
+ "use strict";
+
+ // easier key mapping
+ var keycodes = keyboard.keycodes;
+
+ var prepend_n_prc = function(str, n) {
+ for( var i =0 ; i< n ; i++){
+ str = '%'+str ;
+ }
+ return str;
+ };
+
+ var _existing_completion = function(item, completion_array){
+ for( var i=0; i < completion_array.length; i++) {
+ if (completion_array[i].trim().substr(-item.length) == item) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // what is the common start of all completions
+ function shared_start(B, drop_prct) {
+ if (B.length == 1) {
+ return B[0];
+ }
+ var A = [];
+ var common;
+ var min_lead_prct = 10;
+ for (var i = 0; i < B.length; i++) {
+ var str = B[i].str;
+ var localmin = 0;
+ if(drop_prct === true){
+ while ( str.substr(0, 1) == '%') {
+ localmin = localmin+1;
+ str = str.substring(1);
+ }
+ }
+ min_lead_prct = Math.min(min_lead_prct, localmin);
+ A.push(str);
+ }
+
+ if (A.length > 1) {
+ var tem1, tem2, s;
+ A = A.slice(0).sort();
+ tem1 = A[0];
+ s = tem1.length;
+ tem2 = A.pop();
+ while (s && tem2.indexOf(tem1) == -1) {
+ tem1 = tem1.substring(0, --s);
+ }
+ if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
+ return {
+ str:prepend_n_prc('', min_lead_prct),
+ type: "computed",
+ from: B[0].from,
+ to: B[0].to
+ };
+ }
+ return {
+ str: prepend_n_prc(tem1, min_lead_prct),
+ type: "computed",
+ from: B[0].from,
+ to: B[0].to
+ };
+ }
+ return null;
+ }
+
+
+ var Completer = function (cell, events) {
+ this.cell = cell;
+ this.editor = cell.code_mirror;
+ var that = this;
+ events.on('kernel_busy.Kernel', function () {
+ that.skip_kernel_completion = true;
+ });
+ events.on('kernel_idle.Kernel', function () {
+ that.skip_kernel_completion = false;
+ });
+ };
+
+ Completer.prototype.startCompletion = function () {
+ /**
+ * call for a 'first' completion, that will set the editor and do some
+ * special behavior like autopicking if only one completion available.
+ */
+ if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) return;
+ this.done = false;
+ // use to get focus back on opera
+ this.carry_on_completion(true);
+ };
+
+
+ // easy access for julia to monkeypatch
+ //
+ Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
+
+ Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
+ return Completer.reinvoke_re.test(pre_cursor);
+ };
+
+ /**
+ *
+ * pass true as parameter if this is the first invocation of the completer
+ * this will prevent the completer to dissmiss itself if it is not on a
+ * word boundary like pressing tab after a space, and make it autopick the
+ * only choice if there is only one which prevent from popping the UI. as
+ * well as fast-forwarding the typing if all completion have a common
+ * shared start
+ **/
+ Completer.prototype.carry_on_completion = function (first_invocation) {
+ /**
+ * Pass true as parameter if you want the completer to autopick when
+ * only one completion. This function is automatically reinvoked at
+ * each keystroke with first_invocation = false
+ */
+ var cur = this.editor.getCursor();
+ var line = this.editor.getLine(cur.line);
+ var pre_cursor = this.editor.getRange({
+ line: cur.line,
+ ch: cur.ch - 1
+ }, cur);
+
+ // we need to check that we are still on a word boundary
+ // because while typing the completer is still reinvoking itself
+ // so dismiss if we are on a "bad" caracter
+ if (!this.reinvoke(pre_cursor) && !first_invocation) {
+ this.close();
+ return;
+ }
+
+ this.autopick = false;
+ if (first_invocation) {
+ this.autopick = true;
+ }
+
+ // We want a single cursor position.
+ if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) {
+ return;
+ }
+
+ // one kernel completion came back, finish_completing will be called with the results
+ // we fork here and directly call finish completing if kernel is busy
+ var cursor_pos = this.editor.indexFromPos(cur);
+ if (this.skip_kernel_completion) {
+ this.finish_completing({ content: {
+ matches: [],
+ cursor_start: cursor_pos,
+ cursor_end: cursor_pos,
+ }});
+ } else {
+ this.cell.kernel.complete(this.editor.getValue(), cursor_pos,
+ $.proxy(this.finish_completing, this)
+ );
+ }
+ };
+
+ Completer.prototype.finish_completing = function (msg) {
+ /**
+ * let's build a function that wrap all that stuff into what is needed
+ * for the new completer:
+ */
+ var content = msg.content;
+ var start = content.cursor_start;
+ var end = content.cursor_end;
+ var matches = content.matches;
+
+ var cur = this.editor.getCursor();
+ if (end === null) {
+ // adapted message spec replies don't have cursor position info,
+ // interpret end=null as current position,
+ // and negative start relative to that
+ end = this.editor.indexFromPos(cur);
+ if (start === null) {
+ start = end;
+ } else if (start < 0) {
+ start = end + start;
+ }
+ }
+ var results = CodeMirror.contextHint(this.editor);
+ var filtered_results = [];
+ //remove results from context completion
+ //that are already in kernel completion
+ var i;
+ for (i=0; i < results.length; i++) {
+ if (!_existing_completion(results[i].str, matches)) {
+ filtered_results.push(results[i]);
+ }
+ }
+
+ // append the introspection result, in order, at at the beginning of
+ // the table and compute the replacement range from current cursor
+ // positon and matched_text length.
+ var from = this.editor.posFromIndex(start);
+ var to = this.editor.posFromIndex(end);
+ for (i = matches.length - 1; i >= 0; --i) {
+ filtered_results.unshift({
+ str: matches[i],
+ type: "introspection",
+ from: from,
+ to: to
+ });
+ }
+
+ // one the 2 sources results have been merge, deal with it
+ this.raw_result = filtered_results;
+
+ // if empty result return
+ if (!this.raw_result || !this.raw_result.length) return;
+
+ // When there is only one completion, use it directly.
+ if (this.autopick && this.raw_result.length == 1) {
+ this.insert(this.raw_result[0]);
+ return;
+ }
+
+ if (this.raw_result.length == 1) {
+ // test if first and only completion totally matches
+ // what is typed, in this case dismiss
+ var str = this.raw_result[0].str;
+ var pre_cursor = this.editor.getRange({
+ line: cur.line,
+ ch: cur.ch - str.length
+ }, cur);
+ if (pre_cursor == str) {
+ this.close();
+ return;
+ }
+ }
+
+ if (!this.visible) {
+ this.complete = $('<div/>').addClass('completions');
+ this.complete.attr('id', 'complete');
+
+ // Currently webkit doesn't use the size attr correctly. See:
+ // https://code.google.com/p/chromium/issues/detail?id=4579
+ this.sel = $('<select/>')
+ .attr('tabindex', -1)
+ .attr('multiple', 'true');
+ this.complete.append(this.sel);
+ this.visible = true;
+ $('body').append(this.complete);
+
+ //build the container
+ var that = this;
+ this.sel.dblclick(function () {
+ that.pick();
+ });
+ this.sel.focus(function () {
+ that.editor.focus();
+ });
+ this._handle_keydown = function (cm, event) {
+ that.keydown(event);
+ };
+ this.editor.on('keydown', this._handle_keydown);
+ this._handle_keypress = function (cm, event) {
+ that.keypress(event);
+ };
+ this.editor.on('keypress', this._handle_keypress);
+ }
+ this.sel.attr('size', Math.min(10, this.raw_result.length));
+
+ // After everything is on the page, compute the postion.
+ // We put it above the code if it is too close to the bottom of the page.
+ var pos = this.editor.cursorCoords(
+ this.editor.posFromIndex(start)
+ );
+ var left = pos.left-3;
+ var top;
+ var cheight = this.complete.height();
+ var wheight = $(window).height();
+ if (pos.bottom+cheight+5 > wheight) {
+ top = pos.top-cheight-4;
+ } else {
+ top = pos.bottom+1;
+ }
+ this.complete.css('left', left + 'px');
+ this.complete.css('top', top + 'px');
+
+ // Clear and fill the list.
+ this.sel.text('');
+ this.build_gui_list(this.raw_result);
+ return true;
+ };
+
+ Completer.prototype.insert = function (completion) {
+ this.editor.replaceRange(completion.str, completion.from, completion.to);
+ };
+
+ Completer.prototype.build_gui_list = function (completions) {
+ for (var i = 0; i < completions.length; ++i) {
+ var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
+ this.sel.append(opt);
+ }
+ this.sel.children().first().attr('selected', 'true');
+ this.sel.scrollTop(0);
+ };
+
+ Completer.prototype.close = function () {
+ this.done = true;
+ $('#complete').remove();
+ this.editor.off('keydown', this._handle_keydown);
+ this.editor.off('keypress', this._handle_keypress);
+ this.visible = false;
+ };
+
+ Completer.prototype.pick = function () {
+ this.insert(this.raw_result[this.sel[0].selectedIndex]);
+ this.close();
+ };
+
+ Completer.prototype.keydown = function (event) {
+ var code = event.keyCode;
+
+ // Enter
+ var options;
+ var index;
+ if (code == keycodes.enter) {
+ event.codemirrorIgnore = true;
+ event._ipkmIgnore = true;
+ event.preventDefault();
+ this.pick();
+ // Escape or backspace
+ } else if (code == keycodes.esc || code == keycodes.backspace) {
+ event.codemirrorIgnore = true;
+ event._ipkmIgnore = true;
+ event.preventDefault();
+ this.close();
+ } else if (code == keycodes.tab) {
+ //all the fastforwarding operation,
+ //Check that shared start is not null which can append with prefixed completion
+ // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
+ // to erase py
+ var sh = shared_start(this.raw_result, true);
+ if (sh.str !== '') {
+ this.insert(sh);
+ }
+ this.close();
+ this.carry_on_completion();
+ } else if (code == keycodes.up || code == keycodes.down) {
+ // need to do that to be able to move the arrow
+ // when on the first or last line ofo a code cell
+ event.codemirrorIgnore = true;
+ event._ipkmIgnore = true;
+ event.preventDefault();
+
+ options = this.sel.find('option');
+ index = this.sel[0].selectedIndex;
+ if (code == keycodes.up) {
+ index--;
+ }
+ if (code == keycodes.down) {
+ index++;
+ }
+ index = Math.min(Math.max(index, 0), options.length-1);
+ this.sel[0].selectedIndex = index;
+ } else if (code == keycodes.pageup || code == keycodes.pagedown) {
+ event._ipkmIgnore = true;
+
+ options = this.sel.find('option');
+ index = this.sel[0].selectedIndex;
+ if (code == keycodes.pageup) {
+ index -= 10; // As 10 is the hard coded size of the drop down menu
+ } else {
+ index += 10;
+ }
+ index = Math.min(Math.max(index, 0), options.length-1);
+ this.sel[0].selectedIndex = index;
+ } else if (code == keycodes.left || code == keycodes.right) {
+ this.close();
+ }
+ };
+
+ Completer.prototype.keypress = function (event) {
+ /**
+ * FIXME: This is a band-aid.
+ * on keypress, trigger insertion of a single character.
+ * This simulates the old behavior of completion as you type,
+ * before events were disconnected and CodeMirror stopped
+ * receiving events while the completer is focused.
+ */
+
+ var that = this;
+ var code = event.keyCode;
+
+ // don't handle keypress if it's not a character (arrows on FF)
+ // or ENTER/TAB
+ if (event.charCode === 0 ||
+ code == keycodes.tab ||
+ code == keycodes.enter
+ ) return;
+
+ this.close();
+ this.editor.focus();
+ setTimeout(function () {
+ that.carry_on_completion();
+ }, 50);
+ };
+
+ return {'Completer': Completer};
+});
diff --git a/notebook/static/notebook/js/contexthint.js b/notebook/static/notebook/js/contexthint.js
new file mode 100644
index 0000000..22fdbcd
--- /dev/null
+++ b/notebook/static/notebook/js/contexthint.js
@@ -0,0 +1,98 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+// highly adapted for codemiror jshint
+define(['codemirror/lib/codemirror'], function(CodeMirror) {
+ "use strict";
+
+ var forEach = function(arr, f) {
+ for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
+ };
+
+ var arrayContains = function(arr, item) {
+ if (!Array.prototype.indexOf) {
+ var i = arr.length;
+ while (i--) {
+ if (arr[i] === item) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return arr.indexOf(item) != -1;
+ };
+
+ CodeMirror.contextHint = function (editor) {
+ // Find the token at the cursor
+ var cur = editor.getCursor(),
+ token = editor.getTokenAt(cur),
+ tprop = token;
+ // If it's not a 'word-style' token, ignore the token.
+ // If it is a property, find out what it is a property of.
+ var list = [];
+ var clist = getCompletions(token, editor);
+ for (var i = 0; i < clist.length; i++) {
+ list.push({
+ str: clist[i],
+ type: "context",
+ from: {
+ line: cur.line,
+ ch: token.start
+ },
+ to: {
+ line: cur.line,
+ ch: token.end
+ }
+ });
+ }
+ return list;
+ };
+
+ // find all 'words' of current cell
+ var getAllTokens = function (editor) {
+ var found = [];
+
+ // add to found if not already in it
+
+
+ function maybeAdd(str) {
+ if (!arrayContains(found, str)) found.push(str);
+ }
+
+ // loop through all token on all lines
+ var lineCount = editor.lineCount();
+ // loop on line
+ for (var l = 0; l < lineCount; l++) {
+ var line = editor.getLine(l);
+ //loop on char
+ for (var c = 1; c < line.length; c++) {
+ var tk = editor.getTokenAt({
+ line: l,
+ ch: c
+ });
+ // if token has a class, it has geat chances of beeing
+ // of interest. Add it to the list of possible completions.
+ // we could skip token of ClassName 'comment'
+ // or 'number' and 'operator'
+ if (tk.className !== null) {
+ maybeAdd(tk.string);
+ }
+ // jump to char after end of current token
+ c = tk.end;
+ }
+ }
+ return found;
+ };
+
+ var getCompletions = function(token, editor) {
+ var candidates = getAllTokens(editor);
+ // filter all token that have a common start (but nox exactly) the lenght of the current token
+ var lambda = function (x) {
+ return (x.indexOf(token.string) === 0 && x != token.string);
+ };
+ var filterd = candidates.filter(lambda);
+ return filterd;
+ };
+
+ return {'contextHint': CodeMirror.contextHint};
+});
diff --git a/notebook/static/notebook/js/kernelselector.js b/notebook/static/notebook/js/kernelselector.js
new file mode 100644
index 0000000..c2a0022
--- /dev/null
+++ b/notebook/static/notebook/js/kernelselector.js
@@ -0,0 +1,347 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/namespace',
+ 'base/js/dialog',
+ 'base/js/utils',
+ 'require',
+], function($, IPython, dialog, utils, require) {
+ "use strict";
+
+ var KernelSelector = function(selector, notebook) {
+ var that = this;
+ this.selector = selector;
+ this.notebook = notebook;
+ this.notebook.set_kernelselector(this);
+ this.events = notebook.events;
+ this.current_selection = null;
+ this.kernelspecs = {};
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.request_kernelspecs();
+ }
+ this.bind_events();
+ // Make the object globally available for user convenience & inspection
+ IPython.kernelselector = this;
+ this._finish_load = null;
+ this._loaded = false;
+ this.loaded = new Promise(function(resolve) {
+ that._finish_load = resolve;
+ });
+
+ Object.seal(this);
+ };
+
+ KernelSelector.prototype.request_kernelspecs = function() {
+ // Preliminary documentation for kernelspecs api is at
+ // https://github.com/ipython/ipython/wiki/IPEP-25%3A-Registry-of-installed-kernels#rest-api
+ var url = utils.url_path_join(this.notebook.base_url, 'api/kernelspecs');
+ utils.promising_ajax(url).then($.proxy(this._got_kernelspecs, this));
+ };
+
+ var _sorted_names = function(kernelspecs) {
+ // sort kernel names
+ return Object.keys(kernelspecs).sort(function (a, b) {
+ // sort by display_name
+ var da = kernelspecs[a].spec.display_name;
+ var db = kernelspecs[b].spec.display_name;
+ if (da === db) {
+ return 0;
+ } else if (da > db) {
+ return 1;
+ } else {
+ return -1;
+ }
+ });
+ };
+
+ KernelSelector.prototype._got_kernelspecs = function(data) {
+ var that = this;
+ this.kernelspecs = data.kernelspecs;
+ var change_kernel_submenu = $("#menu-change-kernel-submenu");
+ var new_notebook_submenu = $("#menu-new-notebook-submenu");
+ var keys = _sorted_names(data.kernelspecs);
+
+ keys.map(function (key) {
+ // Create the Kernel > Change kernel submenu
+ var ks = data.kernelspecs[key];
+ change_kernel_submenu.append(
+ $("<li>").attr("id", "kernel-submenu-"+ks.name).append(
+ $('<a>')
+ .attr('href', '#')
+ .click( function () {
+ that.set_kernel(ks.name);
+ })
+ .text(ks.spec.display_name)
+ )
+ );
+ // Create the File > New Notebook submenu
+ new_notebook_submenu.append(
+ $("<li>").attr("id", "new-notebook-submenu-"+ks.name).append(
+ $('<a>')
+ .attr('href', '#')
+ .click( function () {
+ that.new_notebook(ks.name);
+ })
+ .text(ks.spec.display_name)
+ )
+ );
+
+ });
+ // trigger loaded promise
+ this._loaded = true;
+ this._finish_load();
+ };
+
+ KernelSelector.prototype._spec_changed = function (event, ks) {
+ /** event handler for spec_changed */
+ var that = this;
+
+ // update selection
+ this.current_selection = ks.name;
+
+ // put the current kernel at the top of File > New Notebook
+ var cur_kernel_entry = $("#new-notebook-submenu-" + ks.name);
+ var parent = cur_kernel_entry.parent();
+ // do something only if there is more than one kernel
+ if (parent.children().length > 1) {
+ // first, sort back the submenu
+ parent.append(
+ parent.children("li[class!='divider']").sort(
+ function (a,b) {
+ var da = $("a",a).text();
+ var db = $("a",b).text();
+ if (da === db) {
+ return 0;
+ } else if (da > db) {
+ return 1;
+ } else {
+ return -1;
+ }}));
+ // then, if there is no divider yet, add one
+ if (!parent.children("li[class='divider']").length) {
+ parent.prepend($("<li>").attr("class","divider"));
+ }
+ // finally, put the current kernel at the top
+ parent.prepend(cur_kernel_entry);
+ }
+
+ // load logo
+ var logo_img = this.element.find("img.current_kernel_logo");
+ $("#kernel_indicator").find('.kernel_indicator_name').text(ks.spec.display_name);
+ if (ks.resources['logo-64x64']) {
+ logo_img.attr("src", ks.resources['logo-64x64']);
+ logo_img.show();
+ } else {
+ logo_img.hide();
+ }
+
+ // load kernel css
+ var css_url = ks.resources['kernel.css'];
+ if (css_url) {
+ $('#kernel-css').attr('href', css_url);
+ } else {
+ $('#kernel-css').attr('href', '');
+ }
+
+ // load kernel js
+ if (ks.resources['kernel.js']) {
+
+ // Debug added for Notebook 4.2, please remove at some point in the
+ // future if the following does not append anymore when kernels
+ // have kernel.js
+ //
+ // > Uncaught (in promise) TypeError: require is not a function
+ //
+ console.info('Dynamically requiring kernel.js, `require` is ', require);
+ require([ks.resources['kernel.js']],
+ function (kernel_mod) {
+ if (kernel_mod && kernel_mod.onload) {
+ kernel_mod.onload();
+ } else {
+ console.warn("Kernel " + ks.name + " has a kernel.js file that does not contain "+
+ "any asynchronous module definition. This is undefined behavior "+
+ "and not recommended.");
+ }
+ }, function (err) {
+ console.warn("Failed to load kernel.js from ", ks.resources['kernel.js'], err);
+ }
+ );
+ this.events.on('spec_changed.Kernel', function (evt, new_ks) {
+ if (ks.name != new_ks.name) {
+ console.warn("kernelspec %s had custom kernel.js. Forcing page reload for %s.",
+ ks.name, new_ks.name);
+ that.notebook.save_notebook().then(function () {
+ window.location.reload();
+ });
+ }
+ });
+ }
+ };
+
+ KernelSelector.prototype.set_kernel = function (selected) {
+ /** set the kernel by name, ensuring kernelspecs have been loaded, first
+
+ kernel can be just a kernel name, or a notebook kernelspec metadata
+ (name, language, display_name).
+ */
+ var that = this;
+ if (typeof selected === 'string') {
+ selected = {
+ name: selected
+ };
+ }
+ if (this._loaded) {
+ this._set_kernel(selected);
+ } else {
+ return this.loaded.then(function () {
+ that._set_kernel(selected);
+ });
+ }
+ };
+
+ KernelSelector.prototype._set_kernel = function (selected) {
+ /** Actually set the kernel (kernelspecs have been loaded) */
+ if (selected.name === this.current_selection) {
+ // only trigger event if value changed
+ return;
+ }
+ var kernelspecs = this.kernelspecs;
+ var ks = kernelspecs[selected.name];
+ if (ks === undefined) {
+ var available = _sorted_names(kernelspecs);
+ var matches = [];
+ if (selected.language && selected.language.length > 0) {
+ available.map(function (name) {
+ if (kernelspecs[name].spec.language.toLowerCase() === selected.language.toLowerCase()) {
+ matches.push(name);
+ }
+ });
+ }
+ if (matches.length === 1) {
+ ks = kernelspecs[matches[0]];
+ console.log("No exact match found for " + selected.name +
+ ", using only kernel that matches language=" + selected.language, ks);
+ this.events.trigger("spec_match_found.Kernel", {
+ selected: selected,
+ found: ks,
+ });
+ }
+ // if still undefined, trigger failure event
+ if (ks === undefined) {
+ this.events.trigger("spec_not_found.Kernel", {
+ selected: selected,
+ matches: matches,
+ available: available,
+ });
+ return;
+ }
+ }
+ if (this.notebook._session_starting &&
+ this.notebook.session.kernel.name !== ks.name) {
+ console.error("Cannot change kernel while waiting for pending session start.");
+ return;
+ }
+ this.current_selection = ks.name;
+ this.events.trigger('spec_changed.Kernel', ks);
+ };
+
+ KernelSelector.prototype._spec_not_found = function (event, data) {
+ var that = this;
+ var select = $("<select>").addClass('form-control');
+ console.warn("Kernelspec not found:", data);
+ var names;
+ if (data.matches.length > 1) {
+ names = data.matches;
+ } else {
+ names = data.available;
+ }
+ names.map(function (name) {
+ var ks = that.kernelspecs[name];
+ select.append(
+ $('<option/>').attr('value', ks.name).text(ks.spec.display_name || ks.name)
+ );
+ });
+
+ var body = $("<form>").addClass("form-inline").append(
+ $("<span>").text(
+ "I couldn't find a kernel matching " + (data.selected.display_name || data.selected.name) + "." +
+ " Please select a kernel:"
+ )
+ ).append(select);
+
+ dialog.modal({
+ title : 'Kernel not found',
+ body : body,
+ buttons : {
+ 'Continue without kernel' : {
+ class : 'btn-danger',
+ click : function () {
+ that.events.trigger('no_kernel.Kernel');
+ }
+ },
+ OK : {
+ class : 'btn-primary',
+ click : function () {
+ that.set_kernel(select.val());
+ }
+ }
+ }
+ });
+ };
+
+ KernelSelector.prototype.new_notebook = function (kernel_name) {
+
+ var w = window.open('', IPython._target);
+ // Create a new notebook in the same path as the current
+ // notebook's path.
+ var that = this;
+ var parent = utils.url_path_split(that.notebook.notebook_path)[0];
+ that.notebook.contents.new_untitled(parent, {type: "notebook"}).then(
+ function (data) {
+ var url = utils.url_path_join(
+ that.notebook.base_url, 'notebooks',
+ utils.encode_uri_components(data.path)
+ );
+ url += "?kernel_name=" + kernel_name;
+ w.location = url;
+ },
+ function(error) {
+ w.close();
+ dialog.modal({
+ title : 'Creating Notebook Failed',
+ body : "The error was: " + error.message,
+ buttons : {'OK' : {'class' : 'btn-primary'}}
+ });
+ }
+ );
+ };
+
+ KernelSelector.prototype.lock_switch = function() {
+ // should set a flag and display warning+reload if user want to
+ // re-change kernel. As UI discussion never finish
+ // making that a separate PR.
+ console.warn('switching kernel is not guaranteed to work !');
+ };
+
+ KernelSelector.prototype.bind_events = function() {
+ var that = this;
+ this.events.on('spec_changed.Kernel', $.proxy(this._spec_changed, this));
+ this.events.on('spec_not_found.Kernel', $.proxy(this._spec_not_found, this));
+ this.events.on('kernel_created.Session', function (event, data) {
+ that.set_kernel(data.kernel.name);
+ });
+
+ var logo_img = this.element.find("img.current_kernel_logo");
+ logo_img.on("load", function() {
+ logo_img.show();
+ });
+ logo_img.on("error", function() {
+ logo_img.hide();
+ });
+ };
+
+ return {'KernelSelector': KernelSelector};
+});
diff --git a/notebook/static/notebook/js/keyboardmanager.js b/notebook/static/notebook/js/keyboardmanager.js
new file mode 100644
index 0000000..37d8e0b
--- /dev/null
+++ b/notebook/static/notebook/js/keyboardmanager.js
@@ -0,0 +1,231 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+/**
+ *
+ *
+ * @module keyboardmanager
+ * @namespace keyboardmanager
+ * @class KeyboardManager
+ */
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/keyboard',
+], function($, utils, keyboard) {
+ "use strict";
+
+ // Main keyboard manager for the notebook
+ var keycodes = keyboard.keycodes;
+
+ var KeyboardManager = function (options) {
+ /**
+ * A class to deal with keyboard event and shortcut
+ *
+ * @class KeyboardManager
+ * @constructor
+ * @param options {dict} Dictionary of keyword arguments :
+ * @param options.events {$(Events)} instance
+ * @param options.pager: {Pager} pager instance
+ */
+ this.mode = 'command';
+ this.enabled = true;
+ this.pager = options.pager;
+ this.quick_help = undefined;
+ this.notebook = undefined;
+ this.last_mode = undefined;
+ this.bind_events();
+ this.env = {pager:this.pager};
+ this.actions = options.actions;
+ this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env );
+ this.command_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
+ this.command_shortcuts.add_shortcuts(this.get_default_command_shortcuts());
+ this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env);
+ this.edit_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
+ this.edit_shortcuts.add_shortcuts(this.get_default_edit_shortcuts());
+ Object.seal(this);
+ };
+
+
+
+
+ /**
+ * Return a dict of common shortcut
+ * @method get_default_common_shortcuts
+ *
+ * @example Example of returned shortcut
+ * ```
+ * 'shortcut-key': 'action-name'
+ * // a string representing the shortcut as dash separated value.
+ * // e.g. 'shift' , 'shift-enter', 'cmd-t'
+ *```
+ */
+ KeyboardManager.prototype.get_default_common_shortcuts = function() {
+ return {
+ 'shift' : 'jupyter-notebook:ignore',
+ 'shift-enter' : 'jupyter-notebook:run-cell-and-select-next',
+ 'ctrl-enter' : 'jupyter-notebook:run-cell',
+ 'alt-enter' : 'jupyter-notebook:run-cell-and-insert-below',
+ // cmd on mac, ctrl otherwise
+ 'cmdtrl-s' : 'jupyter-notebook:save-notebook',
+ };
+ };
+
+ KeyboardManager.prototype.get_default_edit_shortcuts = function() {
+ return {
+ 'cmdtrl-shift-p' : 'jupyter-notebook:show-command-palette',
+ 'esc' : 'jupyter-notebook:enter-command-mode',
+ 'ctrl-m' : 'jupyter-notebook:enter-command-mode',
+ 'up' : 'jupyter-notebook:move-cursor-up',
+ 'down' : 'jupyter-notebook:move-cursor-down',
+ 'ctrl-shift--' : 'jupyter-notebook:split-cell-at-cursor',
+ };
+ };
+
+ KeyboardManager.prototype.get_default_command_shortcuts = function() {
+ return {
+ 'cmdtrl-shift-p': 'jupyter-notebook:show-command-palette',
+ 'shift-space': 'jupyter-notebook:scroll-notebook-up',
+ 'shift-v' : 'jupyter-notebook:paste-cell-above',
+ 'shift-m' : 'jupyter-notebook:merge-cells',
+ 'shift-o' : 'jupyter-notebook:toggle-cell-output-scrolled',
+ 'enter' : 'jupyter-notebook:enter-edit-mode',
+ 'space' : 'jupyter-notebook:scroll-notebook-down',
+ 'down' : 'jupyter-notebook:select-next-cell',
+ 'i,i' : 'jupyter-notebook:interrupt-kernel',
+ '0,0' : 'jupyter-notebook:confirm-restart-kernel',
+ 'd,d' : 'jupyter-notebook:delete-cell',
+ 'esc': 'jupyter-notebook:close-pager',
+ 'up' : 'jupyter-notebook:select-previous-cell',
+ 'k' : 'jupyter-notebook:select-previous-cell',
+ 'j' : 'jupyter-notebook:select-next-cell',
+ 'shift-k': 'jupyter-notebook:extend-selection-above',
+ 'shift-j': 'jupyter-notebook:extend-selection-below',
+ 'shift-up': 'jupyter-notebook:extend-selection-above',
+ 'shift-down': 'jupyter-notebook:extend-selection-below',
+ 'x' : 'jupyter-notebook:cut-cell',
+ 'c' : 'jupyter-notebook:copy-cell',
+ 'v' : 'jupyter-notebook:paste-cell-below',
+ 'a' : 'jupyter-notebook:insert-cell-above',
+ 'b' : 'jupyter-notebook:insert-cell-below',
+ 'y' : 'jupyter-notebook:change-cell-to-code',
+ 'm' : 'jupyter-notebook:change-cell-to-markdown',
+ 'r' : 'jupyter-notebook:change-cell-to-raw',
+ '1' : 'jupyter-notebook:change-cell-to-heading-1',
+ '2' : 'jupyter-notebook:change-cell-to-heading-2',
+ '3' : 'jupyter-notebook:change-cell-to-heading-3',
+ '4' : 'jupyter-notebook:change-cell-to-heading-4',
+ '5' : 'jupyter-notebook:change-cell-to-heading-5',
+ '6' : 'jupyter-notebook:change-cell-to-heading-6',
+ 'o' : 'jupyter-notebook:toggle-cell-output-collapsed',
+ 's' : 'jupyter-notebook:save-notebook',
+ 'l' : 'jupyter-notebook:toggle-cell-line-numbers',
+ 'h' : 'jupyter-notebook:show-keyboard-shortcuts',
+ 'z' : 'jupyter-notebook:undo-cell-deletion',
+ 'q' : 'jupyter-notebook:close-pager',
+ };
+ };
+
+ KeyboardManager.prototype.bind_events = function () {
+ var that = this;
+ $(document).keydown(function (event) {
+ if(event._ipkmIgnore===true||(event.originalEvent||{})._ipkmIgnore===true){
+ return false;
+ }
+ return that.handle_keydown(event);
+ });
+ };
+
+ KeyboardManager.prototype.set_notebook = function (notebook) {
+ this.notebook = notebook;
+ this.actions.extend_env({notebook:notebook});
+ };
+
+ KeyboardManager.prototype.set_quickhelp = function (notebook) {
+ this.actions.extend_env({quick_help:notebook});
+ };
+
+
+ KeyboardManager.prototype.handle_keydown = function (event) {
+ /**
+ * returning false from this will stop event propagation
+ **/
+
+ if (event.which === keycodes.esc) {
+ // Intercept escape at highest level to avoid closing
+ // websocket connection with firefox
+ event.preventDefault();
+ }
+
+ if (!this.enabled) {
+ if (event.which === keycodes.esc) {
+ this.notebook.command_mode();
+ return false;
+ }
+ return true;
+ }
+
+ if (this.mode === 'edit') {
+ return this.edit_shortcuts.call_handler(event);
+ } else if (this.mode === 'command') {
+ return this.command_shortcuts.call_handler(event);
+ }
+ return true;
+ };
+
+ KeyboardManager.prototype.edit_mode = function () {
+ this.last_mode = this.mode;
+ this.mode = 'edit';
+ };
+
+ KeyboardManager.prototype.command_mode = function () {
+ this.last_mode = this.mode;
+ this.mode = 'command';
+ };
+
+ KeyboardManager.prototype.enable = function () {
+ this.enabled = true;
+ };
+
+ KeyboardManager.prototype.disable = function () {
+ this.enabled = false;
+ };
+
+ KeyboardManager.prototype.register_events = function (e) {
+ e = $(e);
+ var that = this;
+ var handle_focus = function () {
+ that.disable();
+ };
+ var handle_blur = function () {
+ that.enable();
+ };
+ e.on('focusin', handle_focus);
+ e.on('focusout', handle_blur);
+ // TODO: Very strange. The focusout event does not seem fire for the
+ // bootstrap textboxes on FF25&26... This works around that by
+ // registering focus and blur events recursively on all inputs within
+ // registered element.
+ e.find('input').blur(handle_blur);
+ e.on('DOMNodeInserted', function (event) {
+ var target = $(event.target);
+ if (target.is('input')) {
+ target.blur(handle_blur);
+ } else {
+ target.find('input').blur(handle_blur);
+ }
+ });
+ // There are times (raw_input) where we remove the element from the DOM before
+ // focusout is called. In this case we bind to the remove event of jQueryUI,
+ // which gets triggered upon removal, iff it is focused at the time.
+ // is_focused must be used to check for the case where an element within
+ // the element being removed is focused.
+ e.on('remove', function () {
+ if (utils.is_focused(e[0])) {
+ that.enable();
+ }
+ });
+ };
+
+ return {'KeyboardManager': KeyboardManager};
+});
diff --git a/notebook/static/notebook/js/main.js b/notebook/static/notebook/js/main.js
new file mode 100644
index 0000000..fe7bc06
--- /dev/null
+++ b/notebook/static/notebook/js/main.js
@@ -0,0 +1,195 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+require([
+ 'base/js/namespace',
+ 'jquery',
+ 'notebook/js/notebook',
+ 'contents',
+ 'services/config',
+ 'base/js/utils',
+ 'base/js/page',
+ 'base/js/events',
+ 'auth/js/loginwidget',
+ 'notebook/js/maintoolbar',
+ 'notebook/js/pager',
+ 'notebook/js/quickhelp',
+ 'notebook/js/menubar',
+ 'notebook/js/notificationarea',
+ 'notebook/js/savewidget',
+ 'notebook/js/actions',
+ 'notebook/js/keyboardmanager',
+ 'notebook/js/kernelselector',
+ 'codemirror/lib/codemirror',
+ 'notebook/js/about',
+ 'typeahead',
+ 'notebook/js/searchandreplace',
+ 'custom',
+], function(
+ IPython,
+ $,
+ notebook,
+ contents,
+ configmod,
+ utils,
+ page,
+ events,
+ loginwidget,
+ maintoolbar,
+ pager,
+ quickhelp,
+ menubar,
+ notificationarea,
+ savewidget,
+ actions,
+ keyboardmanager,
+ kernelselector,
+ CodeMirror,
+ about,
+ typeahead,
+ searchandreplace
+ ) {
+ "use strict";
+
+ // compat with old IPython, remove for IPython > 3.0
+ window.CodeMirror = CodeMirror;
+
+ var common_options = {
+ ws_url : utils.get_body_data("wsUrl"),
+ base_url : utils.get_body_data("baseUrl"),
+ notebook_path : utils.get_body_data("notebookPath"),
+ notebook_name : utils.get_body_data('notebookName')
+ };
+
+ var config_section = new configmod.ConfigSection('notebook', common_options);
+ config_section.load();
+ var common_config = new configmod.ConfigSection('common', common_options);
+ common_config.load();
+ var page = new page.Page();
+ var pager = new pager.Pager('div#pager', {
+ events: events});
+ var acts = new actions.init();
+ var keyboard_manager = new keyboardmanager.KeyboardManager({
+ pager: pager,
+ events: events,
+ actions: acts });
+ var save_widget = new savewidget.SaveWidget('span#save_widget', {
+ events: events,
+ keyboard_manager: keyboard_manager});
+ acts.extend_env({save_widget:save_widget});
+ var contents = new contents.Contents({
+ base_url: common_options.base_url,
+ common_config: common_config
+ });
+ var notebook = new notebook.Notebook('div#notebook', $.extend({
+ events: events,
+ keyboard_manager: keyboard_manager,
+ save_widget: save_widget,
+ contents: contents,
+ config: config_section},
+ common_options));
+ var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options);
+ var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', {
+ notebook: notebook,
+ events: events,
+ actions: acts});
+ var quick_help = new quickhelp.QuickHelp({
+ keyboard_manager: keyboard_manager,
+ events: events,
+ notebook: notebook});
+ keyboard_manager.set_notebook(notebook);
+ keyboard_manager.set_quickhelp(quick_help);
+ var menubar = new menubar.MenuBar('#menubar', $.extend({
+ notebook: notebook,
+ contents: contents,
+ events: events,
+ save_widget: save_widget,
+ quick_help: quick_help,
+ actions: acts},
+ common_options));
+ var notification_area = new notificationarea.NotebookNotificationArea(
+ '#notification_area', {
+ events: events,
+ save_widget: save_widget,
+ notebook: notebook,
+ keyboard_manager: keyboard_manager});
+ notification_area.init_notification_widgets();
+ var kernel_selector = new kernelselector.KernelSelector(
+ '#kernel_logo_widget', notebook);
+ searchandreplace.load(keyboard_manager);
+
+ $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
+ '<span id="test2" style="font-weight: bold;">x</span>'+
+ '<span id="test3" style="font-style: italic;">x</span></pre></div>');
+ var nh = $('#test1').innerHeight();
+ var bh = $('#test2').innerHeight();
+ var ih = $('#test3').innerHeight();
+ if(nh != bh || nh != ih) {
+ $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
+ }
+ $('#fonttest').remove();
+
+ page.show();
+
+ events.one('notebook_loaded.Notebook', function () {
+ var hash = document.location.hash;
+ if (hash) {
+ document.location.hash = '';
+ document.location.hash = hash;
+ }
+ notebook.set_autosave_interval(notebook.minimum_autosave_interval);
+ });
+
+ IPython.page = page;
+ IPython.notebook = notebook;
+ IPython.contents = contents;
+ IPython.pager = pager;
+ IPython.quick_help = quick_help;
+ IPython.login_widget = login_widget;
+ IPython.menubar = menubar;
+ IPython.toolbar = toolbar;
+ IPython.notification_area = notification_area;
+ IPython.keyboard_manager = keyboard_manager;
+ IPython.save_widget = save_widget;
+ IPython.tooltip = notebook.tooltip;
+
+ try {
+ events.trigger('app_initialized.NotebookApp');
+ } catch (e) {
+ console.error("Error in app_initialized callback", e);
+ }
+
+ Object.defineProperty( IPython, 'actions', {
+ get: function() {
+ console.warn('accessing "actions" on the global IPython/Jupyter is not recommended. Pass it to your objects contructors at creation time');
+ return acts;
+ },
+ enumerable: true,
+ configurable: false
+ });
+
+ // Now actually load nbextensions from config
+ Promise.all([
+ utils.load_extensions_from_config(config_section),
+ utils.load_extensions_from_config(common_config),
+ ])
+ .catch(function(error) {
+ console.error('Could not load nbextensions from user config files', error);
+ })
+ // BEGIN HARDCODED WIDGETS HACK
+ .then(function() {
+ if (!utils.is_loaded('jupyter-js-widgets/extension')) {
+ // Fallback to the ipywidgets extension
+ utils.load_extension('widgets/notebook/js/extension').catch(function () {
+ console.warn('Widgets are not available. Please install widgetsnbextension or ipywidgets 4.0');
+ });
+ }
+ })
+ .catch(function(error) {
+ console.error('Could not load ipywidgets', error);
+ });
+ // END HARDCODED WIDGETS HACK
+
+ notebook.load_notebook(common_options.notebook_path);
+
+});
diff --git a/notebook/static/notebook/js/maintoolbar.js b/notebook/static/notebook/js/maintoolbar.js
new file mode 100644
index 0000000..4c42772
--- /dev/null
+++ b/notebook/static/notebook/js/maintoolbar.js
@@ -0,0 +1,144 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'require',
+ 'jquery',
+ './toolbar',
+ './celltoolbar'
+], function(require, $, toolbar, celltoolbar) {
+ "use strict";
+
+ var MainToolBar = function (selector, options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * selector: string
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * notebook: Notebook instance
+ **/
+ toolbar.ToolBar.apply(this, [selector, options] );
+ this.events = options.events;
+ this.notebook = options.notebook;
+ this._make();
+ Object.seal(this);
+ };
+
+ MainToolBar.prototype = Object.create(toolbar.ToolBar.prototype);
+
+ MainToolBar.prototype._make = function () {
+ var grps = [
+ [
+ ['jupyter-notebook:save-notebook'],
+ 'save-notbook'
+ ],
+ [
+ ['jupyter-notebook:insert-cell-below'],
+ 'insert_above_below'],
+ [
+ ['jupyter-notebook:cut-cell',
+ 'jupyter-notebook:copy-cell',
+ 'jupyter-notebook:paste-cell-below'
+ ] ,
+ 'cut_copy_paste'],
+ [
+ ['jupyter-notebook:move-cell-up',
+ 'jupyter-notebook:move-cell-down'
+ ],
+ 'move_up_down'],
+ [ ['jupyter-notebook:run-cell-and-select-next',
+ 'jupyter-notebook:interrupt-kernel',
+ 'jupyter-notebook:confirm-restart-kernel'
+ ],
+ 'run_int'],
+ ['<add_celltype_list>'],
+ [['jupyter-notebook:show-command-palette']],
+ ['<add_celltoolbar_reminder>']
+ ];
+ this.construct(grps);
+ };
+
+ MainToolBar.prototype._pseudo_actions = {};
+
+
+ // reminder of where the celltoolbar new menu is, remove for 5.0
+ MainToolBar.prototype._pseudo_actions.add_celltoolbar_reminder = function () {
+ var _b = $('<button/>').attr('title','show new celltoolbar selector location').addClass('btn btn-default').text('CellToolbar')
+ var btn = $('<div/>').addClass('btn-group').append(_b)
+
+ _b.on('click', function(){
+ setTimeout(function(){$('#view_menu').parent().addClass('pulse')},0)
+ setTimeout(function(){$('#view_menu').parent().addClass('open')},1000)
+ setTimeout(function(){$('#menu-cell-toolbar').children('a').addClass('pulse')},2000)
+ setTimeout(function(){$('#menu-cell-toolbar').children('ul').css('display','block')},3000)
+ setTimeout(function(){$('#menu-cell-toolbar').children('ul').css('display','')},5400)
+ setTimeout(function(){$('#menu-cell-toolbar').children('a').removeClass('pulse')},5600)
+ setTimeout(function(){$('#view_menu').parent().removeClass('open')},5800)
+ setTimeout(function(){$('#view_menu').parent().removeClass('pulse')},6000)
+ })
+
+ return btn;
+ };
+
+
+ // add a cell type drop down to the maintoolbar.
+ // triggered when the pseudo action `<add_celltype_list>` is
+ // encountered when building a toolbar.
+ MainToolBar.prototype._pseudo_actions.add_celltype_list = function () {
+ var that = this;
+ var multiselect = $('<option/>').attr('value','multiselect').attr('disabled','').text('-');
+ var sel = $('<select/>')
+ .attr('id','cell_type')
+ .addClass('form-control select-xs')
+ .append($('<option/>').attr('value','code').text('Code'))
+ .append($('<option/>').attr('value','markdown').text('Markdown'))
+ .append($('<option/>').attr('value','raw').text('Raw NBConvert'))
+ .append($('<option/>').attr('value','heading').text('Heading'))
+ .append(multiselect);
+ this.notebook.keyboard_manager.register_events(sel);
+ this.events.on('selected_cell_type_changed.Notebook', function (event, data) {
+ if ( that.notebook.get_selected_cells_indices().length > 1) {
+ multiselect.show();
+ sel.val('multiselect');
+ } else {
+ multiselect.hide()
+ if (data.cell_type === 'heading') {
+ sel.val('Markdown');
+ } else {
+ sel.val(data.cell_type);
+ }
+ }
+ });
+ sel.change(function () {
+ var cell_type = $(this).val();
+ switch (cell_type) {
+ case 'code':
+ that.notebook.cells_to_code();
+ break;
+ case 'markdown':
+ that.notebook.cells_to_markdown();
+ break;
+ case 'raw':
+ that.notebook.cells_to_raw();
+ break;
+ case 'heading':
+ that.notebook._warn_heading();
+ that.notebook.to_heading();
+ sel.val('markdown');
+ break;
+ case 'multiselect':
+ break;
+ default:
+ console.log("unrecognized cell type:", cell_type);
+ }
+ that.notebook.focus_cell();
+ });
+ return sel;
+
+ };
+
+ return {'MainToolBar': MainToolBar};
+});
diff --git a/notebook/static/notebook/js/mathjaxutils.js b/notebook/static/notebook/js/mathjaxutils.js
new file mode 100644
index 0000000..cfbe266
--- /dev/null
+++ b/notebook/static/notebook/js/mathjaxutils.js
@@ -0,0 +1,212 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/dialog',
+], function($, utils, dialog) {
+ "use strict";
+
+ var init = function () {
+ if (window.MathJax) {
+ // MathJax loaded
+ MathJax.Hub.Config({
+ tex2jax: {
+ inlineMath: [ ['$','$'], ["\\(","\\)"] ],
+ displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
+ processEscapes: true,
+ processEnvironments: true
+ },
+ // Center justify equations in code and markdown cells. Elsewhere
+ // we use CSS to left justify single line equations in code cells.
+ displayAlign: 'center',
+ "HTML-CSS": {
+ availableFonts: [],
+ imageFont: null,
+ preferredFont: null,
+ webFont: "STIX-Web",
+ styles: {'.MathJax_Display': {"margin": 0}},
+ linebreaks: { automatic: true }
+ },
+ });
+ MathJax.Hub.Configured();
+ } else if (window.mathjax_url !== "") {
+ // Don't have MathJax, but should. Show dialog.
+ dialog.modal({
+ title : "Failed to retrieve MathJax from '" + window.mathjax_url + "'",
+ body : $("<p/>").addClass('dialog').text(
+ "Math/LaTeX rendering will be disabled."
+ ),
+ buttons : {
+ OK : {class: "btn-danger"}
+ }
+ });
+ }
+ };
+
+ // Some magic for deferring mathematical expressions to MathJax
+ // by hiding them from the Markdown parser.
+ // Some of the code here is adapted with permission from Davide Cervone
+ // under the terms of the Apache2 license governing the MathJax project.
+ // Other minor modifications are also due to StackExchange and are used with
+ // permission.
+
+ var inline = "$"; // the inline math delimiter
+
+ // MATHSPLIT contains the pattern for math delimiters and special symbols
+ // needed for searching for math in the text input.
+ var MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@)/i;
+
+ // The math is in blocks i through j, so
+ // collect it into one block and clear the others.
+ // Replace &, <, and > by named entities.
+ // For IE, put <br> at the ends of comments since IE removes \n.
+ // Clear the current math positions and store the index of the
+ // math, then push the math string onto the storage array.
+ // The preProcess function is called on all blocks if it has been passed in
+ var process_math = function (i, j, pre_process, math, blocks) {
+ var block = blocks.slice(i, j + 1).join("").replace(/&/g, "&amp;") // use HTML entity for &
+ .replace(/</g, "&lt;") // use HTML entity for <
+ .replace(/>/g, "&gt;") // use HTML entity for >
+ ;
+ if (utils.browser === 'msie') {
+ block = block.replace(/(%[^\n]*)\n/g, "$1<br/>\n");
+ }
+ while (j > i) {
+ blocks[j] = "";
+ j--;
+ }
+ blocks[i] = "@@" + math.length + "@@"; // replace the current block text with a unique tag to find later
+ if (pre_process){
+ block = pre_process(block);
+ }
+ math.push(block);
+ return blocks;
+ };
+
+ // Break up the text into its component parts and search
+ // through them for math delimiters, braces, linebreaks, etc.
+ // Math delimiters must match and braces must balance.
+ // Don't allow math to pass through a double linebreak
+ // (which will be a paragraph).
+ //
+ var remove_math = function (text) {
+ var math = []; // stores math strings for later
+ var start;
+ var end;
+ var last;
+ var braces;
+
+ // Except for extreme edge cases, this should catch precisely those pieces of the markdown
+ // source that will later be turned into code spans. While MathJax will not TeXify code spans,
+ // we still have to consider them at this point; the following issue has happened several times:
+ //
+ // `$foo` and `$bar` are varibales. --> <code>$foo ` and `$bar</code> are variables.
+
+ var hasCodeSpans = /`/.test(text),
+ de_tilde;
+ if (hasCodeSpans) {
+ text = text.replace(/~/g, "~T").replace(/(^|[^\\])(`+)([^\n]*?[^`\n])\2(?!`)/gm, function (wholematch) {
+ return wholematch.replace(/\$/g, "~D");
+ });
+ de_tilde = function (text) {
+ return text.replace(/~([TD])/g, function (wholematch, character) {
+ return { T: "~", D: "$" }[character];
+ });
+ };
+ } else {
+ de_tilde = function (text) { return text; };
+ }
+
+ var blocks = utils.regex_split(text.replace(/\r\n?/g, "\n"),MATHSPLIT);
+
+ for (var i = 1, m = blocks.length; i < m; i += 2) {
+ var block = blocks[i];
+ if (block.charAt(0) === "@") {
+ //
+ // Things that look like our math markers will get
+ // stored and then retrieved along with the math.
+ //
+ blocks[i] = "@@" + math.length + "@@";
+ math.push(block);
+ }
+ else if (start) {
+ //
+ // If we are in math, look for the end delimiter,
+ // but don't go past double line breaks, and
+ // and balance braces within the math.
+ //
+ if (block === end) {
+ if (braces) {
+ last = i;
+ }
+ else {
+ blocks = process_math(start, i, de_tilde, math, blocks);
+ start = null;
+ end = null;
+ last = null;
+ }
+ }
+ else if (block.match(/\n.*\n/)) {
+ if (last) {
+ i = last;
+ blocks = process_math(start, i, de_tilde, math, blocks);
+ }
+ start = null;
+ end = null;
+ last = null;
+ braces = 0;
+ }
+ else if (block === "{") {
+ braces++;
+ }
+ else if (block === "}" && braces) {
+ braces--;
+ }
+ }
+ else {
+ //
+ // Look for math start delimiters and when
+ // found, set up the end delimiter.
+ //
+ if (block === inline || block === "$$") {
+ start = i;
+ end = block;
+ braces = 0;
+ }
+ else if (block.substr(1, 5) === "begin") {
+ start = i;
+ end = "\\end" + block.substr(6);
+ braces = 0;
+ }
+ }
+ }
+ if (last) {
+ blocks = process_math(start, last, de_tilde, math, blocks);
+ start = null;
+ end = null;
+ last = null;
+ }
+ return [de_tilde(blocks.join("")), math];
+ };
+
+ //
+ // Put back the math strings that were saved,
+ // and clear the math array (no need to keep it around).
+ //
+ var replace_math = function (text, math) {
+ text = text.replace(/@@(\d+)@@/g, function (match, n) {
+ return math[n];
+ });
+ return text;
+ };
+
+ var mathjaxutils = {
+ init : init,
+ remove_math : remove_math,
+ replace_math : replace_math
+ };
+
+ return mathjaxutils;
+});
diff --git a/notebook/static/notebook/js/menubar.js b/notebook/static/notebook/js/menubar.js
new file mode 100644
index 0000000..9215000
--- /dev/null
+++ b/notebook/static/notebook/js/menubar.js
@@ -0,0 +1,417 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/namespace',
+ 'base/js/dialog',
+ 'base/js/utils',
+ './celltoolbar',
+ './tour',
+ 'moment',
+], function($, IPython, dialog, utils, celltoolbar, tour, moment) {
+ "use strict";
+
+ var MenuBar = function (selector, options) {
+ /**
+ * Constructor
+ *
+ * A MenuBar Class to generate the menubar of Jupyter notebook
+ *
+ * Parameters:
+ * selector: string
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * notebook: Notebook instance
+ * contents: ContentManager instance
+ * events: $(Events) instance
+ * save_widget: SaveWidget instance
+ * quick_help: QuickHelp instance
+ * base_url : string
+ * notebook_path : string
+ * notebook_name : string
+ */
+ options = options || {};
+ this.base_url = options.base_url || utils.get_body_data("baseUrl");
+ this.selector = selector;
+ this.notebook = options.notebook;
+ this.actions = this.notebook.keyboard_manager.actions;
+ this.contents = options.contents;
+ this.events = options.events;
+ this.save_widget = options.save_widget;
+ this.quick_help = options.quick_help;
+ this.actions = options.actions;
+
+ try {
+ this.tour = new tour.Tour(this.notebook, this.events);
+ } catch (e) {
+ this.tour = undefined;
+ console.log("Failed to instantiate Notebook Tour", e);
+ }
+
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.style();
+ this.bind_events();
+ }
+ };
+
+ // TODO: This has definitively nothing to do with style ...
+ MenuBar.prototype.style = function () {
+ var that = this;
+ this.element.find("li").click(function (event, ui) {
+ // The selected cell loses focus when the menu is entered, so we
+ // re-select it upon selection.
+ var i = that.notebook.get_selected_index();
+ that.notebook.select(i, false);
+ }
+ );
+ };
+
+ MenuBar.prototype._nbconvert = function (format, download) {
+ download = download || false;
+ var notebook_path = utils.encode_uri_components(this.notebook.notebook_path);
+ var url = utils.url_path_join(
+ this.base_url,
+ 'nbconvert',
+ format,
+ notebook_path
+ ) + "?download=" + download.toString();
+
+ var w = window.open('', IPython._target);
+ if (this.notebook.dirty && this.notebook.writable) {
+ this.notebook.save_notebook().then(function() {
+ w.location = url;
+ });
+ } else {
+ w.location = url;
+ }
+ };
+
+ MenuBar.prototype._size_header = function() {
+ /**
+ * Update header spacer size.
+ */
+ console.warn('`_size_header` is deprecated and will be removed in future versions.'+
+ ' Please trigger the `resize-header.Page` manually if you rely on it.');
+ this.events.trigger('resize-header.Page');
+ };
+
+ MenuBar.prototype.bind_events = function () {
+ /**
+ * File
+ */
+ var that = this;
+
+ this.element.find('#open_notebook').click(function () {
+ var parent = utils.url_path_split(that.notebook.notebook_path)[0];
+ window.open(
+ utils.url_path_join(
+ that.base_url, 'tree',
+ utils.encode_uri_components(parent)
+ ), IPython._target);
+ });
+ this.element.find('#copy_notebook').click(function () {
+ that.notebook.copy_notebook();
+ return false;
+ });
+ this.element.find('#download_ipynb').click(function () {
+ var base_url = that.notebook.base_url;
+ var notebook_path = utils.encode_uri_components(that.notebook.notebook_path);
+ var w = window.open('');
+ var url = utils.url_path_join(
+ base_url, 'files', notebook_path
+ ) + '?download=1';
+ if (that.notebook.dirty && that.notebook.writable) {
+ that.notebook.save_notebook().then(function() {
+ w.location = url;
+ });
+ } else {
+ w.location = url;
+ }
+ });
+
+ this.element.find('#print_preview').click(function () {
+ that._nbconvert('html', false);
+ });
+
+ this.element.find('#download_html').click(function () {
+ that._nbconvert('html', true);
+ });
+
+ this.element.find('#download_markdown').click(function () {
+ that._nbconvert('markdown', true);
+ });
+
+ this.element.find('#download_rst').click(function () {
+ that._nbconvert('rst', true);
+ });
+
+ this.element.find('#download_pdf').click(function () {
+ that._nbconvert('pdf', true);
+ });
+
+ this.element.find('#download_script').click(function () {
+ that._nbconvert('script', true);
+ });
+
+
+ this.events.on('trust_changed.Notebook', function (event, trusted) {
+ if (trusted) {
+ that.element.find('#trust_notebook')
+ .addClass("disabled").off('click')
+ .find("a").text("Trusted Notebook");
+ } else {
+ that.element.find('#trust_notebook')
+ .removeClass("disabled").on('click', function () {
+ that.notebook.trust_notebook();
+ })
+ .find("a").text("Trust Notebook");
+ }
+ });
+
+ this.element.find('#kill_and_exit').click(function () {
+ var close_window = function () {
+ /**
+ * allow closing of new tabs in Chromium, impossible in FF
+ */
+ window.open('', '_self', '');
+ window.close();
+ };
+ // finish with close on success or failure
+ that.notebook.session.delete(close_window, close_window);
+ });
+
+ // View
+ this._add_celltoolbar_list();
+
+ // Edit
+ this.element.find('#edit_nb_metadata').click(function () {
+ that.notebook.edit_metadata({
+ notebook: that.notebook,
+ keyboard_manager: that.notebook.keyboard_manager});
+ });
+
+ var id_actions_dict = {
+ '#trust_notebook' : 'trust-notebook',
+ '#rename_notebook' : 'rename-notebook',
+ '#find_and_replace' : 'find-and-replace',
+ '#save_checkpoint': 'save-notebook',
+ '#restart_kernel': 'confirm-restart-kernel',
+ '#restart_clear_output': 'confirm-restart-kernel-and-clear-output',
+ '#restart_run_all': 'confirm-restart-kernel-and-run-all-cells',
+ '#int_kernel': 'interrupt-kernel',
+ '#cut_cell': 'cut-cell',
+ '#copy_cell': 'copy-cell',
+ '#delete_cell': 'delete-cell',
+ '#undelete_cell': 'undo-cell-deletion',
+ '#split_cell': 'split-cell-at-cursor',
+ '#merge_cell_above': 'merge-cell-with-previous-cell',
+ '#merge_cell_below': 'merge-cell-with-next-cell',
+ '#move_cell_up': 'move-cell-up',
+ '#move_cell_down': 'move-cell-down',
+ '#toggle_header': 'toggle-header',
+ '#toggle_toolbar': 'toggle-toolbar',
+ '#insert_cell_above': 'insert-cell-above',
+ '#insert_cell_below': 'insert-cell-below',
+ '#run_cell': 'run-cell',
+ '#run_cell_select_below': 'run-cell-and-select-next',
+ '#run_cell_insert_below': 'run-cell-and-insert-below',
+ '#run_all_cells': 'run-all-cells',
+ '#run_all_cells_above': 'run-all-cells-above',
+ '#run_all_cells_below': 'run-all-cells-below',
+ '#to_code': 'change-cell-to-code',
+ '#to_markdown': 'change-cell-to-markdown',
+ '#to_raw': 'change-cell-to-raw',
+ '#toggle_current_output': 'toggle-cell-output-collapsed',
+ '#toggle_current_output_scroll': 'toggle-cell-output-scrolled',
+ '#clear_current_output': 'clear-cell-output',
+ '#toggle_all_output': 'toggle-all-cells-output-collapsed',
+ '#toggle_all_output_scroll': 'toggle-all-cells-output-scrolled',
+ '#clear_all_output': 'clear-all-cells-output',
+ };
+
+ for(var idx in id_actions_dict){
+ if (!id_actions_dict.hasOwnProperty(idx)){
+ continue;
+ }
+ var id_act = 'jupyter-notebook:'+id_actions_dict[idx];
+ if(!that.actions.exists(id_act)){
+ console.warn('actions', id_act, 'does not exist, still binding it in case it will be defined later...');
+ }
+ // Immediately-Invoked Function Expression cause JS.
+ (function(that, id_act, idx){
+ that.element.find(idx).click(function(event){
+ that.actions.call(id_act, event);
+ });
+ })(that, id_act, idx);
+ }
+
+
+ // Kernel
+ this.element.find('#reconnect_kernel').click(function () {
+ that.notebook.kernel.reconnect();
+ });
+ // Help
+ if (this.tour) {
+ this.element.find('#notebook_tour').click(function () {
+ that.tour.start();
+ });
+ } else {
+ this.element.find('#notebook_tour').addClass("disabled");
+ }
+ this.element.find('#keyboard_shortcuts').click(function () {
+ that.quick_help.show_keyboard_shortcuts();
+ });
+
+ this.update_restore_checkpoint(null);
+
+ this.events.on('checkpoints_listed.Notebook', function (event, data) {
+ that.update_restore_checkpoint(that.notebook.checkpoints);
+ });
+
+ this.events.on('checkpoint_created.Notebook', function (event, data) {
+ that.update_restore_checkpoint(that.notebook.checkpoints);
+ });
+
+ this.events.on('notebook_loaded.Notebook', function() {
+ var langinfo = that.notebook.metadata.language_info || {};
+ that.update_nbconvert_script(langinfo);
+ });
+
+ this.events.on('kernel_ready.Kernel', function(event, data) {
+ var langinfo = data.kernel.info_reply.language_info || {};
+ that.update_nbconvert_script(langinfo);
+ that.add_kernel_help_links(data.kernel.info_reply.help_links || []);
+ });
+ };
+
+ MenuBar.prototype._add_celltoolbar_list = function () {
+ var that = this;
+ var submenu = $("#menu-cell-toolbar-submenu");
+
+ function preset_added(event, data) {
+ var name = data.name;
+ submenu.append(
+ $("<li/>")
+ .attr('data-name', encodeURIComponent(name))
+ .append(
+ $("<a/>")
+ .attr('href', '#')
+ .text(name)
+ .click(function () {
+ if (name ==='None') {
+ celltoolbar.CellToolbar.global_hide();
+ delete that.notebook.metadata.celltoolbar;
+ } else {
+ celltoolbar.CellToolbar.global_show();
+ celltoolbar.CellToolbar.activate_preset(name, that.events);
+ that.notebook.metadata.celltoolbar = name;
+ }
+ that.notebook.focus_cell();
+ })
+ )
+ );
+ }
+
+ // Setup the existing presets
+ var presets = celltoolbar.CellToolbar.list_presets();
+ preset_added(null, {name: "None"});
+ presets.map(function (name) {
+ preset_added(null, {name: name});
+ });
+
+ // Setup future preset registrations
+ this.events.on('preset_added.CellToolbar', preset_added);
+
+ // Handle unregistered presets
+ this.events.on('unregistered_preset.CellToolbar', function (event, data) {
+ submenu.find("li[data-name='" + encodeURIComponent(data.name) + "']").remove();
+ });
+ };
+
+ MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
+ var ul = this.element.find("#restore_checkpoint").find("ul");
+ ul.empty();
+ if (!checkpoints || checkpoints.length === 0) {
+ ul.append(
+ $("<li/>")
+ .addClass("disabled")
+ .append(
+ $("<a/>")
+ .text("No checkpoints")
+ )
+ );
+ return;
+ }
+
+ var that = this;
+ checkpoints.map(function (checkpoint) {
+ var d = new Date(checkpoint.last_modified);
+ ul.append(
+ $("<li/>").append(
+ $("<a/>")
+ .attr("href", "#")
+ .text(moment(d).format("LLLL"))
+ .click(function () {
+ that.notebook.restore_checkpoint_dialog(checkpoint);
+ })
+ )
+ );
+ });
+ };
+
+ MenuBar.prototype.update_nbconvert_script = function(langinfo) {
+ /**
+ * Set the 'Download as foo' menu option for the relevant language.
+ */
+ var el = this.element.find('#download_script');
+
+ // Set menu entry text to e.g. "Python (.py)"
+ var langname = (langinfo.name || 'Script');
+ langname = langname.charAt(0).toUpperCase()+langname.substr(1); // Capitalise
+ el.find('a').text(langname + ' ('+(langinfo.file_extension || 'txt')+')');
+ };
+
+ MenuBar.prototype.add_kernel_help_links = function(help_links) {
+ /** add links from kernel_info to the help menu */
+ var divider = $("#kernel-help-links");
+ if (divider.length === 0) {
+ // insert kernel help section above about link
+ var about = $("#notebook_about").parent();
+ divider = $("<li>")
+ .attr('id', "kernel-help-links")
+ .addClass('divider');
+ about.prev().before(divider);
+ }
+ // remove previous entries
+ while (!divider.next().hasClass('divider')) {
+ divider.next().remove();
+ }
+ if (help_links.length === 0) {
+ // no help links, remove the divider
+ divider.remove();
+ return;
+ }
+ var cursor = divider;
+ help_links.map(function (link) {
+ cursor.after($("<li>")
+ .append($("<a>")
+ .attr('target', '_blank')
+ .attr('title', 'Opens in a new window')
+ .attr('href', require.toUrl(link.url))
+ .append($("<i>")
+ .addClass("fa fa-external-link menu-icon pull-right")
+ )
+ .append($("<span>")
+ .text(link.text)
+ )
+ )
+ );
+ cursor = cursor.next();
+ });
+
+ };
+
+ return {'MenuBar': MenuBar};
+});
diff --git a/notebook/static/notebook/js/notebook.js b/notebook/static/notebook/js/notebook.js
new file mode 100644
index 0000000..af2bfef
--- /dev/null
+++ b/notebook/static/notebook/js/notebook.js
@@ -0,0 +1,3045 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+/**
+ * @module notebook
+ */
+define(function (require) {
+ "use strict";
+ var IPython = require('base/js/namespace');
+ var $ = require('jquery');
+ var _ = require('underscore');
+ var utils = require('base/js/utils');
+ var dialog = require('base/js/dialog');
+ var cellmod = require('notebook/js/cell');
+ var textcell = require('notebook/js/textcell');
+ var codecell = require('notebook/js/codecell');
+ var moment = require('moment');
+ var configmod = require('services/config');
+ var session = require('services/sessions/session');
+ var celltoolbar = require('notebook/js/celltoolbar');
+ var marked = require('components/marked/lib/marked');
+ var CodeMirror = require('codemirror/lib/codemirror');
+ var runMode = require('codemirror/addon/runmode/runmode');
+ var mathjaxutils = require('notebook/js/mathjaxutils');
+ var keyboard = require('base/js/keyboard');
+ var tooltip = require('notebook/js/tooltip');
+ var default_celltoolbar = require('notebook/js/celltoolbarpresets/default');
+ var rawcell_celltoolbar = require('notebook/js/celltoolbarpresets/rawcell');
+ var slideshow_celltoolbar = require('notebook/js/celltoolbarpresets/slideshow');
+ var scrollmanager = require('notebook/js/scrollmanager');
+ var commandpalette = require('notebook/js/commandpalette');
+
+ var _SOFT_SELECTION_CLASS = 'jupyter-soft-selected';
+
+ function soft_selected(cell){
+ return cell.element.hasClass(_SOFT_SELECTION_CLASS);
+ }
+
+ /**
+ * Contains and manages cells.
+ * @class Notebook
+ * @param {string} selector
+ * @param {object} options - Dictionary of keyword arguments.
+ * @param {jQuery} options.events - selector of Events
+ * @param {KeyboardManager} options.keyboard_manager
+ * @param {Contents} options.contents
+ * @param {SaveWidget} options.save_widget
+ * @param {object} options.config
+ * @param {string} options.base_url
+ * @param {string} options.notebook_path
+ * @param {string} options.notebook_name
+ */
+ var Notebook = function (selector, options) {
+ this.config = options.config;
+ this.class_config = new configmod.ConfigWithDefaults(this.config,
+ Notebook.options_default, 'Notebook');
+ this.base_url = options.base_url;
+ this.notebook_path = options.notebook_path;
+ this.notebook_name = options.notebook_name;
+ this.events = options.events;
+ this.keyboard_manager = options.keyboard_manager;
+ this.contents = options.contents;
+ this.save_widget = options.save_widget;
+ this.tooltip = new tooltip.Tooltip(this.events);
+ this.ws_url = options.ws_url;
+ this._session_starting = false;
+ this.last_modified = null;
+ // debug 484
+ this._last_modified = 'init';
+ // Firefox workaround
+ this._ff_beforeunload_fired = false;
+
+ // Create default scroll manager.
+ this.scroll_manager = new scrollmanager.ScrollManager(this);
+
+ // TODO: This code smells (and the other `= this` line a couple lines down)
+ // We need a better way to deal with circular instance references.
+ this.keyboard_manager.notebook = this;
+ this.save_widget.notebook = this;
+
+ mathjaxutils.init();
+
+ if (marked) {
+ marked.setOptions({
+ gfm : true,
+ tables: true,
+ // FIXME: probably want central config for CodeMirror theme when we have js config
+ langPrefix: "cm-s-ipython language-",
+ highlight: function(code, lang, callback) {
+ if (!lang) {
+ // no language, no highlight
+ if (callback) {
+ callback(null, code);
+ return;
+ } else {
+ return code;
+ }
+ }
+ utils.requireCodeMirrorMode(lang, function (spec) {
+ var el = document.createElement("div");
+ var mode = CodeMirror.getMode({}, spec);
+ if (!mode) {
+ console.log("No CodeMirror mode: " + lang);
+ callback(null, code);
+ return;
+ }
+ try {
+ CodeMirror.runMode(code, spec, el);
+ callback(null, el.innerHTML);
+ } catch (err) {
+ console.log("Failed to highlight " + lang + " code", err);
+ callback(err, code);
+ }
+ }, function (err) {
+ console.log("No CodeMirror mode: " + lang);
+ console.log("Require CodeMirror mode error: " + err);
+ callback(null, code);
+ });
+ }
+ });
+ }
+
+ this.element = $(selector);
+ this.element.scroll();
+ this.element.data("notebook", this);
+ this.session = null;
+ this.kernel = null;
+ this.kernel_busy = false;
+ this.clipboard = null;
+ this.undelete_backup = null;
+ this.undelete_index = null;
+ this.undelete_below = false;
+ this.paste_enabled = false;
+ this.writable = false;
+ // It is important to start out in command mode to match the intial mode
+ // of the KeyboardManager.
+ this.mode = 'command';
+ this.set_dirty(false);
+ this.metadata = {};
+ this._checkpoint_after_save = false;
+ this.last_checkpoint = null;
+ this.checkpoints = [];
+ this.autosave_interval = 0;
+ this.autosave_timer = null;
+ // autosave *at most* every two minutes
+ this.minimum_autosave_interval = 120000;
+ this.notebook_name_blacklist_re = /[\/\\:]/;
+ this.nbformat = 4; // Increment this when changing the nbformat
+ this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
+ this.codemirror_mode = 'text';
+ this.create_elements();
+ this.bind_events();
+ this.kernel_selector = null;
+ this.dirty = null;
+ this.trusted = null;
+ this._fully_loaded = false;
+
+ // Trigger cell toolbar registration.
+ default_celltoolbar.register(this);
+ rawcell_celltoolbar.register(this);
+ slideshow_celltoolbar.register(this);
+
+ // prevent assign to miss-typed properties.
+ Object.seal(this);
+ };
+
+ Notebook.options_default = {
+ // can be any cell type, or the special values of
+ // 'above', 'below', or 'selected' to get the value from another cell.
+ default_cell_type: 'code'
+ };
+
+ /**
+ * Create an HTML and CSS representation of the notebook.
+ */
+ Notebook.prototype.create_elements = function () {
+ var that = this;
+ this.element.attr('tabindex','-1');
+ this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
+ // We add this end_space div to the end of the notebook div to:
+ // i) provide a margin between the last cell and the end of the notebook
+ // ii) to prevent the div from scrolling up when the last cell is being
+ // edited, but is too low on the page, which browsers will do automatically.
+ var end_space = $('<div/>')
+ .addClass('end_space');
+ end_space.dblclick(function (e) {
+ var ncells = that.ncells();
+ that.insert_cell_below('code',ncells-1);
+ });
+ this.element.append(this.container);
+ this.container.after(end_space);
+ };
+
+ /**
+ * Bind JavaScript events: key presses and custom Jupyter events.
+ */
+ Notebook.prototype.bind_events = function () {
+ var that = this;
+
+
+ this.events.on('set_next_input.Notebook', function (event, data) {
+ if (data.replace) {
+ data.cell.set_text(data.text);
+ if (data.clear_output !== false) {
+ // default (undefined) is true to preserve prior behavior
+ data.cell.clear_output();
+ }
+ } else {
+ var index = that.find_cell_index(data.cell);
+ var new_cell = that.insert_cell_below('code',index);
+ new_cell.set_text(data.text);
+ }
+ that.dirty = true;
+ });
+
+ this.events.on('unrecognized_cell.Cell', function () {
+ that.warn_nbformat_minor();
+ });
+
+ this.events.on('unrecognized_output.OutputArea', function () {
+ that.warn_nbformat_minor();
+ });
+
+ this.events.on('set_dirty.Notebook', function (event, data) {
+ that.dirty = data.value;
+ });
+
+ this.events.on('trust_changed.Notebook', function (event, trusted) {
+ that.trusted = trusted;
+ });
+
+ this.events.on('select.Cell', function (event, data) {
+ var index = that.find_cell_index(data.cell);
+ that.select(index, !data.extendSelection);
+ });
+
+ this.events.on('edit_mode.Cell', function (event, data) {
+ that.handle_edit_mode(data.cell);
+ });
+
+ this.events.on('command_mode.Cell', function (event, data) {
+ that.handle_command_mode(data.cell);
+ });
+
+ this.events.on('spec_changed.Kernel', function(event, data) {
+ var existing_spec = that.metadata.kernelspec;
+ that.metadata.kernelspec = {
+ name: data.name,
+ display_name: data.spec.display_name,
+ language: data.spec.language,
+ };
+ if (!existing_spec || ! _.isEqual(existing_spec, that.metadata.kernelspec)) {
+ that.set_dirty(true);
+ }
+ // start session if the current session isn't already correct
+ if (!(that.session && that.session.kernel && that.session.kernel.name === data.name)) {
+ that.start_session(data.name);
+ }
+ });
+
+ this.events.on('kernel_ready.Kernel', function(event, data) {
+ var kinfo = data.kernel.info_reply;
+ if (!kinfo.language_info) {
+ delete that.metadata.language_info;
+ return;
+ }
+ var existing_info = that.metadata.language_info;
+ var langinfo = kinfo.language_info;
+ that.metadata.language_info = langinfo;
+ if (!existing_info || ! _.isEqual(existing_info, langinfo)) {
+ that.set_dirty(true);
+ }
+ // Mode 'null' should be plain, unhighlighted text.
+ var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
+ that.set_codemirror_mode(cm_mode);
+ });
+
+ this.events.on('kernel_idle.Kernel', function () {
+ that.kernel_busy = false;
+ });
+
+ this.events.on('kernel_busy.Kernel', function () {
+ that.kernel_busy = true;
+ });
+
+ var collapse_time = function (time) {
+ var app_height = $('#ipython-main-app').height(); // content height
+ var splitter_height = $('div#pager_splitter').outerHeight(true);
+ var new_height = app_height - splitter_height;
+ that.element.animate({height : new_height + 'px'}, time);
+ };
+
+ this.element.bind('collapse_pager', function (event, extrap) {
+ var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
+ collapse_time(time);
+ });
+
+ var expand_time = function (time) {
+ var app_height = $('#ipython-main-app').height(); // content height
+ var splitter_height = $('div#pager_splitter').outerHeight(true);
+ var pager_height = $('div#pager').outerHeight(true);
+ var new_height = app_height - pager_height - splitter_height;
+ that.element.animate({height : new_height + 'px'}, time);
+ };
+
+ this.element.bind('expand_pager', function (event, extrap) {
+ var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
+ expand_time(time);
+ });
+
+
+ // Firefox 22 broke $(window).on("beforeunload")
+ // I'm not sure why or how.
+ window.onbeforeunload = function (e) {
+ // TODO: Make killing the kernel configurable.
+ var kill_kernel = false;
+ if (kill_kernel) {
+ that.session.delete();
+ }
+ if ( utils.browser[0] === "Firefox") {
+ // Workaround ancient Firefox bug showing beforeunload twice: https://bugzilla.mozilla.org/show_bug.cgi?id=531199
+ if (that._ff_beforeunload_fired) {
+ return; // don't show twice on FF
+ }
+ that._ff_beforeunload_fired = true;
+ // unset flag immediately after dialog is dismissed
+ setTimeout(function () {
+ that._ff_beforeunload_fired = false;
+ }, 1);
+ }
+ // if we are autosaving, trigger an autosave on nav-away.
+ // still warn, because if we don't the autosave may fail.
+ if (that.dirty) {
+ if ( that.autosave_interval ) {
+ // schedule autosave in a timeout
+ // this gives you a chance to forcefully discard changes
+ // by reloading the page if you *really* want to.
+ // the timer doesn't start until you *dismiss* the dialog.
+ setTimeout(function () {
+ if (that.dirty) {
+ that.save_notebook();
+ }
+ }, 1000);
+ return "Autosave in progress, latest changes may be lost.";
+ } else {
+ return "Unsaved changes will be lost.";
+ }
+ }
+ // if the kernel is busy, prompt the user if he’s sure
+ if (that.kernel_busy) {
+ return "The Kernel is busy, outputs may be lost.";
+ }
+ // IE treats null as a string. Instead just return which will avoid the dialog.
+ return;
+ };
+ };
+
+
+ Notebook.prototype.show_command_palette = function() {
+ var x = new commandpalette.CommandPalette(this);
+ };
+
+ /**
+ * Trigger a warning dialog about missing functionality from newer minor versions
+ */
+ Notebook.prototype.warn_nbformat_minor = function (event) {
+ var v = 'v' + this.nbformat + '.';
+ var orig_vs = v + this.nbformat_minor;
+ var this_vs = v + this.current_nbformat_minor;
+ var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
+ this_vs + ". You can still work with this notebook, but cell and output types " +
+ "introduced in later notebook versions will not be available.";
+
+ dialog.modal({
+ notebook: this,
+ keyboard_manager: this.keyboard_manager,
+ title : "Newer Notebook",
+ body : msg,
+ buttons : {
+ OK : {
+ "class" : "btn-danger"
+ }
+ }
+ });
+ };
+
+ /**
+ * Set the dirty flag, and trigger the set_dirty.Notebook event
+ */
+ Notebook.prototype.set_dirty = function (value) {
+ if (value === undefined) {
+ value = true;
+ }
+ if (this.dirty === value) {
+ return;
+ }
+ this.events.trigger('set_dirty.Notebook', {value: value});
+ };
+
+ /**
+ * Scroll the top of the page to a given cell.
+ *
+ * @param {integer} index - An index of the cell to view
+ * @param {integer} time - Animation time in milliseconds
+ * @return {integer} Pixel offset from the top of the container
+ */
+ Notebook.prototype.scroll_to_cell = function (index, time) {
+ return this.scroll_cell_percent(index, 0, time);
+ };
+
+ /**
+ * Scroll the middle of the page to a given cell.
+ *
+ * @param {integer} index - An index of the cell to view
+ * @param {integer} percent - 0-100, the location on the screen to scroll.
+ * 0 is the top, 100 is the bottom.
+ * @param {integer} time - Animation time in milliseconds
+ * @return {integer} Pixel offset from the top of the container
+ */
+ Notebook.prototype.scroll_cell_percent = function (index, percent, time) {
+ var cells = this.get_cells();
+ time = time || 0;
+ percent = percent || 0;
+ index = Math.min(cells.length-1,index);
+ index = Math.max(0 ,index);
+ var sme = this.scroll_manager.element;
+ var h = sme.height();
+ var st = sme.scrollTop();
+ var t = sme.offset().top;
+ var ct = cells[index].element.offset().top;
+ var scroll_value = st + ct - (t + 0.01 * percent * h);
+ this.scroll_manager.element.animate({scrollTop:scroll_value}, time);
+ return scroll_value;
+ };
+
+ /**
+ * Scroll to the bottom of the page.
+ */
+ Notebook.prototype.scroll_to_bottom = function () {
+ this.scroll_manager.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
+ };
+
+ /**
+ * Scroll to the top of the page.
+ */
+ Notebook.prototype.scroll_to_top = function () {
+ this.scroll_manager.element.animate({scrollTop:0}, 0);
+ };
+
+ // Edit Notebook metadata
+
+ /**
+ * Display a dialog that allows the user to edit the Notebook's metadata.
+ */
+ Notebook.prototype.edit_metadata = function () {
+ var that = this;
+ dialog.edit_metadata({
+ md: this.metadata,
+ callback: function (md) {
+ that.metadata = md;
+ },
+ name: 'Notebook',
+ notebook: this,
+ keyboard_manager: this.keyboard_manager});
+ };
+
+ // Cell indexing, retrieval, etc.
+
+ /**
+ * Get all cell elements in the notebook.
+ *
+ * @return {jQuery} A selector of all cell elements
+ */
+ Notebook.prototype.get_cell_elements = function () {
+ return this.container.find(".cell").not('.cell .cell');
+ };
+
+ /**
+ * Get a particular cell element.
+ *
+ * @param {integer} index An index of a cell to select
+ * @return {jQuery} A selector of the given cell.
+ */
+ Notebook.prototype.get_cell_element = function (index) {
+ var result = null;
+ var e = this.get_cell_elements().eq(index);
+ if (e.length !== 0) {
+ result = e;
+ }
+ return result;
+ };
+
+ /**
+ * Try to get a particular cell by msg_id.
+ *
+ * @param {string} msg_id A message UUID
+ * @return {Cell} Cell or null if no cell was found.
+ */
+ Notebook.prototype.get_msg_cell = function (msg_id) {
+ return codecell.CodeCell.msg_cells[msg_id] || null;
+ };
+
+ /**
+ * Count the cells in this notebook.
+ *
+ * @return {integer} The number of cells in this notebook
+ */
+ Notebook.prototype.ncells = function () {
+ return this.get_cell_elements().length;
+ };
+
+ /**
+ * Get all Cell objects in this notebook.
+ *
+ * @return {Array} This notebook's Cell objects
+ */
+ Notebook.prototype.get_cells = function () {
+ // TODO: we are often calling cells as cells()[i], which we should optimize
+ // to cells(i) or a new method.
+ return this.get_cell_elements().toArray().map(function (e) {
+ return $(e).data("cell");
+ });
+ };
+
+ /**
+ * Get a Cell objects from this notebook.
+ *
+ * @param {integer} index - An index of a cell to retrieve
+ * @return {Cell} Cell or null if no cell was found.
+ */
+ Notebook.prototype.get_cell = function (index) {
+ var result = null;
+ var ce = this.get_cell_element(index);
+ if (ce !== null) {
+ result = ce.data('cell');
+ }
+ return result;
+ };
+
+ /**
+ * Get the cell below a given cell.
+ *
+ * @param {Cell} cell
+ * @return {Cell} the next cell or null if no cell was found.
+ */
+ Notebook.prototype.get_next_cell = function (cell) {
+ var result = null;
+ var index = this.find_cell_index(cell);
+ if (this.is_valid_cell_index(index+1)) {
+ result = this.get_cell(index+1);
+ }
+ return result;
+ };
+
+ /**
+ * Get the cell above a given cell.
+ *
+ * @param {Cell} cell
+ * @return {Cell} The previous cell or null if no cell was found.
+ */
+ Notebook.prototype.get_prev_cell = function (cell) {
+ var result = null;
+ var index = this.find_cell_index(cell);
+ if (index !== null && index > 0) {
+ result = this.get_cell(index-1);
+ }
+ return result;
+ };
+
+ /**
+ * Get the numeric index of a given cell.
+ *
+ * @param {Cell} cell
+ * @return {integer} The cell's numeric index or null if no cell was found.
+ */
+ Notebook.prototype.find_cell_index = function (cell) {
+ var result = null;
+ this.get_cell_elements().filter(function (index) {
+ if ($(this).data("cell") === cell) {
+ result = index;
+ }
+ });
+ return result;
+ };
+
+ /**
+ * Return given index if defined, or the selected index if not.
+ *
+ * @param {integer} [index] - A cell's index
+ * @return {integer} cell index
+ */
+ Notebook.prototype.index_or_selected = function (index) {
+ var i;
+ if (index === undefined || index === null) {
+ i = this.get_selected_index();
+ if (i === null) {
+ i = 0;
+ }
+ } else {
+ i = index;
+ }
+ return i;
+ };
+
+
+ Notebook.prototype.get_selected_cells = function () {
+ return this.get_cells().filter(function(cell, index){ return cell.selected || soft_selected(cell) || cell.anchor;});
+ };
+
+ Notebook.prototype.get_selected_cells_indices = function () {
+
+ var result = [];
+ this.get_cells().filter(function (cell, index) {
+ if (cell.selected || soft_selected(cell) || cell.anchor) {
+ result.push(index);
+ }
+ });
+ return result;
+ };
+
+
+ /**
+ * Get the currently selected cell.
+ *
+ * @return {Cell} The selected cell
+ */
+ Notebook.prototype.get_selected_cell = function () {
+ var index = this.get_selected_index();
+ return this.get_cell(index);
+ };
+
+ /**
+ * Check whether a cell index is valid.
+ *
+ * @param {integer} index - A cell index
+ * @return True if the index is valid, false otherwise
+ */
+ Notebook.prototype.is_valid_cell_index = function (index) {
+ if (index !== null && index >= 0 && index < this.ncells()) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * Returns the index of the cell that the selection is currently anchored on.
+ *
+ * @return {integer} Index of first cell selected in selection
+ */
+ Notebook.prototype.get_anchor_index = function () {
+ var result = null;
+ this.get_cell_elements().filter(function (index) {
+ if ($(this).data("cell").anchor === true) {
+ result = index;
+ }
+ });
+ return result;
+ };
+
+ /**
+ * Get the index of the currently selected cell.
+ *
+ * @return {integer} The selected cell's numeric index
+ */
+ Notebook.prototype.get_selected_index = function () {
+ var result = null;
+ this.get_cell_elements().filter(function (index) {
+ if ($(this).data("cell").selected === true) {
+ result = index;
+ }
+ });
+ return result;
+ };
+
+
+ // Cell selection.
+
+ Notebook.prototype.extend_selection_by = function(delta) {
+ var index = this.get_selected_index();
+ // do not move anchor
+ return this.select(index+delta, false);
+ };
+
+
+ Notebook.prototype.update_soft_selection = function(){
+ var i1 = this.get_selected_index();
+ var i2 = this.get_anchor_index();
+ var low = Math.min(i1, i2);
+ var high = Math.max(i1, i2);
+ this.get_cells().map(function(cell, index, all){
+ if( low <= index && index <= high && low !== high){
+ cell.element.addClass(_SOFT_SELECTION_CLASS);
+ } else {
+ cell.element.removeClass(_SOFT_SELECTION_CLASS);
+ }
+ });
+ };
+
+ Notebook.prototype._contract_selection = function(){
+ var i = this.get_selected_index();
+ this.select(i, true);
+ };
+
+ /**
+ * Programmatically select a cell.
+ *
+ * @param {integer} index - A cell's index
+ * @param {bool} moveanchor – whether to move the selection
+ * anchor, default to true.
+ * @return {Notebook} This notebook
+ */
+ Notebook.prototype.select = function (index, moveanchor) {
+ moveanchor = (moveanchor===undefined)? true : moveanchor;
+
+ if (this.is_valid_cell_index(index)) {
+ var sindex = this.get_selected_index();
+ if (sindex !== null && index !== sindex) {
+ // If we are about to select a different cell, make sure we are
+ // first in command mode.
+ if (this.mode !== 'command') {
+ this.command_mode();
+ }
+ this.get_cell(sindex).unselect(moveanchor);
+ }
+ if(moveanchor){
+ this.get_cell(this.get_anchor_index()).unselect(moveanchor);
+ }
+ var cell = this.get_cell(index);
+ cell.select(moveanchor);
+ this.update_soft_selection();
+ if (cell.cell_type === 'heading') {
+ this.events.trigger('selected_cell_type_changed.Notebook',
+ {'cell_type':cell.cell_type, level:cell.level}
+ );
+ } else {
+ this.events.trigger('selected_cell_type_changed.Notebook',
+ {'cell_type':cell.cell_type}
+ );
+ }
+ }
+ return this;
+ };
+
+ /**
+ * Programmatically select the next cell.
+ *
+ * @param {bool} moveanchor – whether to move the selection
+ * anchor, default to true.
+ * @return {Notebook} This notebook
+ */
+ Notebook.prototype.select_next = function (moveanchor) {
+ var index = this.get_selected_index();
+ this.select(index+1, moveanchor);
+ return this;
+ };
+
+ /**
+ * Programmatically select the previous cell.
+ *
+ * @return {Notebook} This notebook
+ */
+ Notebook.prototype.select_prev = function (moveanchor) {
+ var index = this.get_selected_index();
+ this.select(index-1, moveanchor);
+ return this;
+ };
+
+
+ // Edit/Command mode
+
+ /**
+ * Gets the index of the cell that is in edit mode.
+ *
+ * @return {integer} index
+ */
+ Notebook.prototype.get_edit_index = function () {
+ var result = null;
+ this.get_cell_elements().filter(function (index) {
+ if ($(this).data("cell").mode === 'edit') {
+ result = index;
+ }
+ });
+ return result;
+ };
+
+ /**
+ * Handle when a a cell blurs and the notebook should enter command mode.
+ *
+ * @param {Cell} [cell] - Cell to enter command mode on.
+ */
+ Notebook.prototype.handle_command_mode = function (cell) {
+ if (this.mode !== 'command') {
+ cell.command_mode();
+ this.mode = 'command';
+ this.events.trigger('command_mode.Notebook');
+ this.keyboard_manager.command_mode();
+ }
+ };
+
+ /**
+ * Make the notebook enter command mode.
+ */
+ Notebook.prototype.command_mode = function () {
+ var cell = this.get_cell(this.get_edit_index());
+ if (cell && this.mode !== 'command') {
+ // We don't call cell.command_mode, but rather blur the CM editor
+ // which will trigger the call to handle_command_mode.
+ cell.code_mirror.getInputField().blur();
+ }
+ };
+
+ /**
+ * Handle when a cell fires it's edit_mode event.
+ *
+ * @param {Cell} [cell] Cell to enter edit mode on.
+ */
+ Notebook.prototype.handle_edit_mode = function (cell) {
+ this._contract_selection();
+ if (cell && this.mode !== 'edit') {
+ cell.edit_mode();
+ this.mode = 'edit';
+ this.events.trigger('edit_mode.Notebook');
+ this.keyboard_manager.edit_mode();
+ }
+ };
+
+ /**
+ * Make a cell enter edit mode.
+ */
+ Notebook.prototype.edit_mode = function () {
+ this._contract_selection();
+ var cell = this.get_selected_cell();
+ if (cell && this.mode !== 'edit') {
+ cell.unrender();
+ cell.focus_editor();
+ }
+ };
+
+ /**
+ * Ensure either cell, or codemirror is focused. Is none
+ * is focused, focus the cell.
+ */
+ Notebook.prototype.ensure_focused = function(){
+ var cell = this.get_selected_cell();
+ if (cell === null) {return;} // No cell is selected
+ cell.ensure_focused();
+ };
+
+ /**
+ * Focus the currently selected cell.
+ */
+ Notebook.prototype.focus_cell = function () {
+ var cell = this.get_selected_cell();
+ if (cell === null) {return;} // No cell is selected
+ cell.focus_cell();
+ };
+
+ // Cell movement
+
+ /**
+ * Move the current selection up, keeping the same cells selected
+ * No op if the selection is at the beginning of the notebook
+ */
+ Notebook.prototype.move_selection_up = function(){
+ // actually will move the cell before the selection, after the selection
+ var indices = this.get_selected_cells_indices();
+ var first = indices[0];
+ var last = indices[indices.length - 1];
+
+ var selected = this.get_selected_index();
+ var anchored = this.get_anchor_index();
+
+ if (first === 0){
+ return;
+ }
+ var tomove = this.get_cell_element(first - 1);
+ var pivot = this.get_cell_element(last);
+
+ tomove.detach();
+ pivot.after(tomove);
+
+ this.get_cell(selected-1).focus_cell();
+ this.select(anchored - 1);
+ this.select(selected - 1, false);
+ };
+
+ /**
+ * Move the current selection down, keeping the same cells selected.
+ * No op if the selection is at the end of the notebook
+ */
+ Notebook.prototype.move_selection_down = function(){
+ // actually will move the cell after the selection, before the selection
+ var indices = this.get_selected_cells_indices();
+ var first = indices[0];
+ var last = indices[indices.length - 1];
+
+ var selected = this.get_selected_index();
+ var anchored = this.get_anchor_index();
+
+ if(!this.is_valid_cell_index(last + 1)){
+ return;
+ }
+ var tomove = this.get_cell_element(last + 1);
+ var pivot = this.get_cell_element(first);
+
+ tomove.detach();
+ pivot.before(tomove);
+
+ this.get_cell(selected+1).focus_cell();
+ this.select(first);
+ this.select(anchored + 1);
+ this.select(selected + 1, false);
+ };
+
+ /**
+ * Move given (or selected) cell up and select it.
+ *
+ * @param {integer} [index] - cell index
+ * @return {Notebook} This notebook
+ */
+ Notebook.prototype.move_cell_up = function (index) {
+ console.warn('Notebook.move_cell_up is deprecated as of v4.1 and will be removed in v5.0');
+
+ if(index === undefined){
+ this.move_selection_up();
+ return this;
+ }
+
+ var i = this.index_or_selected(index);
+ if (this.is_valid_cell_index(i) && i > 0) {
+ var pivot = this.get_cell_element(i-1);
+ var tomove = this.get_cell_element(i);
+ if (pivot !== null && tomove !== null) {
+ tomove.detach();
+ pivot.before(tomove);
+ this.select(i-1);
+ var cell = this.get_selected_cell();
+ cell.focus_cell();
+ }
+ this.set_dirty(true);
+ }
+ return this;
+ };
+
+
+ /**
+ * Move given (or selected) cell down and select it.
+ *
+ * @param {integer} [index] - cell index
+ * @return {Notebook} This notebook
+ */
+ Notebook.prototype.move_cell_down = function (index) {
+ console.warn('Notebook.move_cell_down is deprecated as of v4.1 and will be removed in v5.0');
+
+ if(index === undefined){
+ this.move_selection_down();
+ return this;
+ }
+
+ var i = this.index_or_selected(index);
+ if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
+ var pivot = this.get_cell_element(i+1);
+ var tomove = this.get_cell_element(i);
+ if (pivot !== null && tomove !== null) {
+ tomove.detach();
+ pivot.after(tomove);
+ this.select(i+1);
+ var cell = this.get_selected_cell();
+ cell.focus_cell();
+ }
+ }
+ this.set_dirty();
+ return this;
+ };
+
+
+ // Insertion, deletion.
+
+ /**
+ * Delete a cell from the notebook without any precautions
+ * Needed to reload checkpoints and other things like that.
+ *
+ * @param {integer} [index] - cell's numeric index
+ * @return {Notebook} This notebook
+ */
+ Notebook.prototype._unsafe_delete_cell = function (index) {
+ var i = this.index_or_selected(index);
+ var cell = this.get_cell(i);
+
+ $('#undelete_cell').addClass('disabled');
+ if (this.is_valid_cell_index(i)) {
+ var old_ncells = this.ncells();
+ var ce = this.get_cell_element(i);
+ ce.remove();
+ this.set_dirty(true);
+ }
+ return this;
+ };
+
+
+ /**
+ * Delete cells from the notebook
+ *
+ * @param {Array} [indices] - the numeric indices of cells to delete.
+ * @return {Notebook} This notebook
+ */
+ Notebook.prototype.delete_cells = function(indices) {
+ if (indices === undefined) {
+ indices = this.get_selected_cells_indices();
+ }
+
+ this.undelete_backup = [];
+
+ var cursor_ix_before = this.get_selected_index();
+ var deleting_before_cursor = 0;
+ for (var i=0; i < indices.length; i++) {
+ if (!this.get_cell(indices[i]).is_deletable()) {
+ // If any cell is marked undeletable, cancel
+ return this;
+ }
+
+ if (indices[i] < cursor_ix_before) {
+ deleting_before_cursor++;
+ }
+ }
+
+ // If we started deleting cells from the top, the later indices would
+ // get offset. We sort them into descending order to avoid that.
+ indices.sort(function(a, b) {return b-a;});
+ for (i=0; i < indices.length; i++) {
+ var cell = this.get_cell(indices[i]);
+ this.undelete_backup.push(cell.toJSON());
+ this.get_cell_element(indices[i]).remove();
+ this.events.trigger('delete.Cell', {'cell': cell, 'index': indices[i]});
+ }
+
+ // Flip the backup copy of cells back to first-to-last order
+ this.undelete_backup.reverse();
+
+ var new_ncells = this.ncells();
+ // Always make sure we have at least one cell.
+ if (new_ncells === 0) {
+ this.insert_cell_below('code');
+ new_ncells = 1;
+ }
+
+ this.undelete_below = false;
+ var cursor_ix_after = this.get_selected_index();
+ if (cursor_ix_after === null) {
+ // Selected cell was deleted
+ cursor_ix_after = cursor_ix_before - deleting_before_cursor;
+ if (cursor_ix_after >= new_ncells) {
+ cursor_ix_after = new_ncells - 1;
+ this.undelete_below = true;
+ }
+ this.select(cursor_ix_after);
+ }
+
+ // Check if the cells were after the cursor
+ for (i=0; i < indices.length; i++) {
+ if (indices[i] > cursor_ix_before) {
+ this.undelete_below = true;
+ }
+ }
+
+ // This will put all the deleted cells back in one location, rather than
+ // where they came from. It will do until we have proper undo support.
+ this.undelete_index = cursor_ix_after;
+ $('#undelete_cell').removeClass('disabled');
+
+ this.set_dirty(true);
+
+ return this;
+ };
+
+ /**
+ * Delete a cell from the notebook.
+ *
+ * @param {integer} [index] - cell's numeric index
+ * @return {Notebook} This notebook
+ */
+ Notebook.prototype.delete_cell = function (index) {
+ if (index === undefined) {
+ return this.delete_cells();
+ } else {
+ return this.delete_cells([index]);
+ }
+ };
+
+ /**
+ * Restore the most recently deleted cells.
+ */
+ Notebook.prototype.undelete_cell = function() {
+ if (this.undelete_backup !== null && this.undelete_index !== null) {
+ var i, cell_data, new_cell;
+ if (this.undelete_below) {
+ for (i = this.undelete_backup.length-1; i >= 0; i--) {
+ cell_data = this.undelete_backup[i];
+ new_cell = this.insert_cell_below(cell_data.cell_type,
+ this.undelete_index);
+ new_cell.fromJSON(cell_data);
+ }
+ } else {
+ for (i=0; i < this.undelete_backup.length; i++) {
+ cell_data = this.undelete_backup[i];
+ new_cell = this.insert_cell_above(cell_data.cell_type,
+ this.undelete_index);
+ new_cell.fromJSON(cell_data);
+ }
+ }
+
+ this.set_dirty(true);
+ this.undelete_backup = null;
+ this.undelete_index = null;
+ }
+ $('#undelete_cell').addClass('disabled');
+ };
+
+ /**
+ * Insert a cell so that after insertion the cell is at given index.
+ *
+ * If cell type is not provided, it will default to the type of the
+ * currently active cell.
+ *
+ * Similar to insert_above, but index parameter is mandatory.
+ *
+ * Index will be brought back into the accessible range [0,n].
+ *
+ * @param {string} [type] - in ['code','markdown', 'raw'], defaults to 'code'
+ * @param {integer} [index] - a valid index where to insert cell
+ * @return {Cell|null} created cell or null
+ */
+ Notebook.prototype.insert_cell_at_index = function(type, index){
+
+ var ncells = this.ncells();
+ index = Math.min(index, ncells);
+ index = Math.max(index, 0);
+ var cell = null;
+ type = type || this.class_config.get_sync('default_cell_type');
+ if (type === 'above') {
+ if (index > 0) {
+ type = this.get_cell(index-1).cell_type;
+ } else {
+ type = 'code';
+ }
+ } else if (type === 'below') {
+ if (index < ncells) {
+ type = this.get_cell(index).cell_type;
+ } else {
+ type = 'code';
+ }
+ } else if (type === 'selected') {
+ type = this.get_selected_cell().cell_type;
+ }
+
+ if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
+ var cell_options = {
+ events: this.events,
+ config: this.config,
+ keyboard_manager: this.keyboard_manager,
+ notebook: this,
+ tooltip: this.tooltip
+ };
+ switch(type) {
+ case 'code':
+ cell = new codecell.CodeCell(this.kernel, cell_options);
+ cell.set_input_prompt();
+ break;
+ case 'markdown':
+ cell = new textcell.MarkdownCell(cell_options);
+ break;
+ case 'raw':
+ cell = new textcell.RawCell(cell_options);
+ break;
+ default:
+ console.log("Unrecognized cell type: ", type, cellmod);
+ cell = new cellmod.UnrecognizedCell(cell_options);
+ }
+
+ if(this._insert_element_at_index(cell.element,index)) {
+ cell.render();
+ this.events.trigger('create.Cell', {'cell': cell, 'index': index});
+ cell.refresh();
+ // We used to select the cell after we refresh it, but there
+ // are now cases were this method is called where select is
+ // not appropriate. The selection logic should be handled by the
+ // caller of the the top level insert_cell methods.
+ this.set_dirty(true);
+ }
+ }
+ return cell;
+
+ };
+
+ /**
+ * Insert an element at given cell index.
+ *
+ * @param {HTMLElement} element - a cell element
+ * @param {integer} [index] - a valid index where to inser cell
+ * @returns {boolean} success
+ */
+ Notebook.prototype._insert_element_at_index = function(element, index){
+ if (element === undefined){
+ return false;
+ }
+
+ var ncells = this.ncells();
+
+ if (ncells === 0) {
+ // special case append if empty
+ this.container.append(element);
+ } else if ( ncells === index ) {
+ // special case append it the end, but not empty
+ this.get_cell_element(index-1).after(element);
+ } else if (this.is_valid_cell_index(index)) {
+ // otherwise always somewhere to append to
+ this.get_cell_element(index).before(element);
+ } else {
+ return false;
+ }
+
+ if (this.undelete_index !== null && index <= this.undelete_index) {
+ this.undelete_index = this.undelete_index + 1;
+ this.set_dirty(true);
+ }
+ return true;
+ };
+
+ /**
+ * Insert a cell of given type above given index, or at top
+ * of notebook if index smaller than 0.
+ *
+ * @param {string} [type] - cell type
+ * @param {integer} [index] - defaults to the currently selected cell
+ * @return {Cell|null} handle to created cell or null
+ */
+ Notebook.prototype.insert_cell_above = function (type, index) {
+ if (index === null || index === undefined) {
+ index = Math.min(this.get_selected_index(index), this.get_anchor_index());
+ }
+ return this.insert_cell_at_index(type, index);
+ };
+
+ /**
+ * Insert a cell of given type below given index, or at bottom
+ * of notebook if index greater than number of cells
+ *
+ * @param {string} [type] - cell type
+ * @param {integer} [index] - defaults to the currently selected cell
+ * @return {Cell|null} handle to created cell or null
+ */
+ Notebook.prototype.insert_cell_below = function (type, index) {
+ if (index === null || index === undefined) {
+ index = Math.max(this.get_selected_index(index), this.get_anchor_index());
+ }
+ return this.insert_cell_at_index(type, index+1);
+ };
+
+
+ /**
+ * Insert cell at end of notebook
+ *
+ * @param {string} type - cell type
+ * @return {Cell|null} handle to created cell or null
+ */
+ Notebook.prototype.insert_cell_at_bottom = function (type){
+ var len = this.ncells();
+ return this.insert_cell_below(type,len-1);
+ };
+
+ /**
+ * Turn one or more cells into code.
+ *
+ * @param {Array} indices - cell indices to convert
+ */
+ Notebook.prototype.cells_to_code = function (indices) {
+ if (indices === undefined){
+ indices = this.get_selected_cells_indices();
+ }
+
+ for (var i=0; i <indices.length; i++){
+ this.to_code(indices[i]);
+ }
+ };
+
+ /**
+ * Turn a cell into a code cell.
+ *
+ * @param {integer} [index] - cell index
+ */
+ Notebook.prototype.to_code = function (index) {
+ var i = this.index_or_selected(index);
+ if (this.is_valid_cell_index(i)) {
+ var source_cell = this.get_cell(i);
+ if (!(source_cell instanceof codecell.CodeCell)) {
+ var target_cell = this.insert_cell_below('code',i);
+ var text = source_cell.get_text();
+ if (text === source_cell.placeholder) {
+ text = '';
+ }
+ //metadata
+ target_cell.metadata = source_cell.metadata;
+
+ target_cell.set_text(text);
+ // make this value the starting point, so that we can only undo
+ // to this state, instead of a blank cell
+ target_cell.code_mirror.clearHistory();
+ source_cell.element.remove();
+ this.select(i);
+ var cursor = source_cell.code_mirror.getCursor();
+ target_cell.code_mirror.setCursor(cursor);
+ this.set_dirty(true);
+ }
+ }
+ };
+
+ /**
+ * Turn one or more cells into Markdown.
+ *
+ * @param {Array} indices - cell indices to convert
+ */
+ Notebook.prototype.cells_to_markdown = function (indices) {
+ if (indices === undefined) {
+ indices = this.get_selected_cells_indices();
+ }
+
+ for(var i=0; i < indices.length; i++) {
+ this.to_markdown(indices[i]);
+ }
+ };
+
+ /**
+ * Turn a cell into a Markdown cell.
+ *
+ * @param {integer} [index] - cell index
+ */
+ Notebook.prototype.to_markdown = function (index) {
+ var i = this.index_or_selected(index);
+ if (this.is_valid_cell_index(i)) {
+ var source_cell = this.get_cell(i);
+
+ if (!(source_cell instanceof textcell.MarkdownCell)) {
+ var target_cell = this.insert_cell_below('markdown',i);
+ var text = source_cell.get_text();
+
+ if (text === source_cell.placeholder) {
+ text = '';
+ }
+ // metadata
+ target_cell.metadata = source_cell.metadata;
+ // We must show the editor before setting its contents
+ target_cell.unrender();
+ target_cell.set_text(text);
+ // make this value the starting point, so that we can only undo
+ // to this state, instead of a blank cell
+ target_cell.code_mirror.clearHistory();
+ source_cell.element.remove();
+ this.select(i);
+ if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
+ target_cell.render();
+ }
+ var cursor = source_cell.code_mirror.getCursor();
+ target_cell.code_mirror.setCursor(cursor);
+ this.set_dirty(true);
+ }
+ }
+ };
+
+ /**
+ * Turn one or more cells into a raw text cell.
+ *
+ * @param {Array} indices - cell indices to convert
+ */
+ Notebook.prototype.cells_to_raw = function (indices) {
+ if (indices === undefined) {
+ indices = this.get_selected_cells_indices();
+ }
+
+ for(var i=0; i < indices.length; i++) {
+ this.to_raw(indices[i]);
+ }
+ };
+
+ /**
+ * Turn a cell into a raw text cell.
+ *
+ * @param {integer} [index] - cell index
+ */
+ Notebook.prototype.to_raw = function (index) {
+ var i = this.index_or_selected(index);
+ if (this.is_valid_cell_index(i)) {
+ var target_cell = null;
+ var source_cell = this.get_cell(i);
+
+ if (!(source_cell instanceof textcell.RawCell)) {
+ target_cell = this.insert_cell_below('raw',i);
+ var text = source_cell.get_text();
+ if (text === source_cell.placeholder) {
+ text = '';
+ }
+ //metadata
+ target_cell.metadata = source_cell.metadata;
+ // We must show the editor before setting its contents
+ target_cell.unrender();
+ target_cell.set_text(text);
+ // make this value the starting point, so that we can only undo
+ // to this state, instead of a blank cell
+ target_cell.code_mirror.clearHistory();
+ source_cell.element.remove();
+ this.select(i);
+ var cursor = source_cell.code_mirror.getCursor();
+ target_cell.code_mirror.setCursor(cursor);
+ this.set_dirty(true);
+ }
+ }
+ };
+
+ /**
+ * Warn about heading cell support removal.
+ */
+ Notebook.prototype._warn_heading = function () {
+ dialog.modal({
+ notebook: this,
+ keyboard_manager: this.keyboard_manager,
+ title : "Use markdown headings",
+ body : $("<p/>").text(
+ 'Jupyter no longer uses special heading cells. ' +
+ 'Instead, write your headings in Markdown cells using # characters:'
+ ).append($('<pre/>').text(
+ '## This is a level 2 heading'
+ )),
+ buttons : {
+ "OK" : {}
+ }
+ });
+ };
+
+ /**
+ * Turn a cell into a heading containing markdown cell.
+ *
+ * @param {integer} [index] - cell index
+ * @param {integer} [level] - heading level (e.g., 1 for h1)
+ */
+ Notebook.prototype.to_heading = function (index, level) {
+ this.to_markdown(index);
+ level = level || 1;
+ var i = this.index_or_selected(index);
+ if (this.is_valid_cell_index(i)) {
+ var cell = this.get_cell(i);
+ cell.set_heading_level(level);
+ this.set_dirty(true);
+ }
+ };
+
+
+ // Cut/Copy/Paste
+
+ /**
+ * Enable the UI elements for pasting cells.
+ */
+ Notebook.prototype.enable_paste = function () {
+ var that = this;
+ if (!this.paste_enabled) {
+ $('#paste_cell_replace').removeClass('disabled')
+ .on('click', function () {that.paste_cell_replace();});
+ $('#paste_cell_above').removeClass('disabled')
+ .on('click', function () {that.paste_cell_above();});
+ $('#paste_cell_below').removeClass('disabled')
+ .on('click', function () {that.paste_cell_below();});
+ this.paste_enabled = true;
+ }
+ };
+
+ /**
+ * Disable the UI elements for pasting cells.
+ */
+ Notebook.prototype.disable_paste = function () {
+ if (this.paste_enabled) {
+ $('#paste_cell_replace').addClass('disabled').off('click');
+ $('#paste_cell_above').addClass('disabled').off('click');
+ $('#paste_cell_below').addClass('disabled').off('click');
+ this.paste_enabled = false;
+ }
+ };
+
+ /**
+ * Cut a cell.
+ */
+ Notebook.prototype.cut_cell = function () {
+ this.copy_cell();
+ this.delete_cell();
+ };
+
+ /**
+ * Copy cells.
+ */
+ Notebook.prototype.copy_cell = function () {
+ var cells = this.get_selected_cells();
+ if (cells.length === 0) {
+ cells = [this.get_selected_cell()];
+ }
+
+ this.clipboard = [];
+ var cell_json;
+ for (var i=0; i < cells.length; i++) {
+ cell_json = cells[i].toJSON();
+ if (cell_json.metadata.deletable !== undefined) {
+ delete cell_json.metadata.deletable;
+ }
+ this.clipboard.push(cell_json);
+ }
+ this.enable_paste();
+ };
+
+ /**
+ * Replace the selected cell with the cells in the clipboard.
+ */
+ Notebook.prototype.paste_cell_replace = function () {
+
+ if (!(this.clipboard !== null && this.paste_enabled)) {
+ return;
+ }
+
+ var selected = this.get_selected_cells_indices();
+ var insertion_index = selected[0];
+ this.delete_cells(selected);
+
+ for (var i=this.clipboard.length-1; i >= 0; i--) {
+ var cell_data = this.clipboard[i];
+ var new_cell = this.insert_cell_at_index(cell_data.cell_type, insertion_index);
+ new_cell.fromJSON(cell_data);
+ }
+
+ this.select(insertion_index+this.clipboard.length-1);
+ };
+
+ /**
+ * Paste cells from the clipboard above the selected cell.
+ */
+ Notebook.prototype.paste_cell_above = function () {
+ if (this.clipboard !== null && this.paste_enabled) {
+ var first_inserted = null;
+ for (var i=0; i < this.clipboard.length; i++) {
+ var cell_data = this.clipboard[i];
+ var new_cell = this.insert_cell_above(cell_data.cell_type);
+ new_cell.fromJSON(cell_data);
+ if (first_inserted === null) {
+ first_inserted = new_cell;
+ }
+ }
+ first_inserted.focus_cell();
+ }
+ };
+
+ /**
+ * Paste cells from the clipboard below the selected cell.
+ */
+ Notebook.prototype.paste_cell_below = function () {
+ if (this.clipboard !== null && this.paste_enabled) {
+ var first_inserted = null;
+ for (var i = this.clipboard.length-1; i >= 0; i--) {
+ var cell_data = this.clipboard[i];
+ var new_cell = this.insert_cell_below(cell_data.cell_type);
+ new_cell.fromJSON(cell_data);
+ if (first_inserted === null) {
+ first_inserted = new_cell;
+ }
+ }
+ first_inserted.focus_cell();
+ }
+ };
+
+ // Split/merge
+
+ /**
+ * Split the selected cell into two cells.
+ */
+ Notebook.prototype.split_cell = function () {
+ var cell = this.get_selected_cell();
+ if (cell.is_splittable()) {
+ var texta = cell.get_pre_cursor();
+ var textb = cell.get_post_cursor();
+ cell.set_text(textb);
+ var new_cell = this.insert_cell_above(cell.cell_type);
+ // Unrender the new cell so we can call set_text.
+ new_cell.unrender();
+ new_cell.set_text(texta);
+ }
+ };
+
+ /**
+ * Merge a series of cells into one
+ *
+ * @param {Array} indices - the numeric indices of the cells to be merged
+ * @param {bool} into_last - merge into the last cell instead of the first
+ */
+ Notebook.prototype.merge_cells = function(indices, into_last) {
+ if (indices.length <= 1) {
+ return;
+ }
+
+ // Check if trying to merge above on topmost cell or wrap around
+ // when merging above, see #330
+ if (indices.filter(function(item) {return item < 0;}).length > 0) {
+ return;
+ }
+
+ for (var i=0; i < indices.length; i++) {
+ if (!this.get_cell(indices[i]).is_mergeable()) {
+ return;
+ }
+ }
+ var target = this.get_cell(into_last ? indices.pop() : indices.shift());
+
+ // Get all the cells' contents
+ var contents = [];
+ for (i=0; i < indices.length; i++) {
+ contents.push(this.get_cell(indices[i]).get_text());
+ }
+ if (into_last) {
+ contents.push(target.get_text());
+ } else {
+ contents.unshift(target.get_text());
+ }
+
+ // Update the contents of the target cell
+ if (target instanceof codecell.CodeCell) {
+ target.set_text(contents.join('\n\n'));
+ } else {
+ var was_rendered = target.rendered;
+ target.unrender(); // Must unrender before we set_text.
+ target.set_text(contents.join('\n\n'));
+ if (was_rendered) {
+ // The rendered state of the final cell should match
+ // that of the original selected cell;
+ target.render();
+ }
+ }
+
+ // Delete the other cells
+ this.delete_cells(indices);
+
+ this.select(this.find_cell_index(target));
+ };
+
+ /**
+ * Merge the selected range of cells
+ */
+ Notebook.prototype.merge_selected_cells = function() {
+ this.merge_cells(this.get_selected_cells_indices());
+ };
+
+ /**
+ * Merge the selected cell into the cell above it.
+ */
+ Notebook.prototype.merge_cell_above = function () {
+ var index = this.get_selected_index();
+ this.merge_cells([index-1, index], true);
+ };
+
+ /**
+ * Merge the selected cell into the cell below it.
+ */
+ Notebook.prototype.merge_cell_below = function () {
+ var index = this.get_selected_index();
+ this.merge_cells([index, index+1], false);
+ };
+
+
+ // Cell collapsing and output clearing
+
+ /**
+ * Hide a cell's output.
+ *
+ * @param {integer} index - cell index
+ */
+ Notebook.prototype.collapse_output = function (index) {
+ var i = this.index_or_selected(index);
+ var cell = this.get_cell(i);
+ if (cell !== null && (cell instanceof codecell.CodeCell)) {
+ cell.collapse_output();
+ this.set_dirty(true);
+ }
+ };
+
+ /**
+ * Hide each code cell's output area.
+ */
+ Notebook.prototype.collapse_all_output = function () {
+ this.get_cells().map(function (cell, i) {
+ if (cell instanceof codecell.CodeCell) {
+ cell.collapse_output();
+ }
+ });
+ // this should not be set if the `collapse` key is removed from nbformat
+ this.set_dirty(true);
+ };
+
+ /**
+ * Show a cell's output.
+ *
+ * @param {integer} index - cell index
+ */
+ Notebook.prototype.expand_output = function (index) {
+ var i = this.index_or_selected(index);
+ var cell = this.get_cell(i);
+ if (cell !== null && (cell instanceof codecell.CodeCell)) {
+ cell.expand_output();
+ this.set_dirty(true);
+ }
+ };
+
+ /**
+ * Expand each code cell's output area, and remove scrollbars.
+ */
+ Notebook.prototype.expand_all_output = function () {
+ this.get_cells().map(function (cell, i) {
+ if (cell instanceof codecell.CodeCell) {
+ cell.expand_output();
+ }
+ });
+ // this should not be set if the `collapse` key is removed from nbformat
+ this.set_dirty(true);
+ };
+
+ /**
+ * Clear the selected CodeCell's output area.
+ *
+ * @param {integer} index - cell index
+ */
+ Notebook.prototype.clear_output = function (index) {
+ var i = this.index_or_selected(index);
+ var cell = this.get_cell(i);
+ if (cell !== null && (cell instanceof codecell.CodeCell)) {
+ cell.clear_output();
+ this.set_dirty(true);
+ }
+ };
+
+ /**
+ * Clear multiple selected CodeCells' output areas.
+ *
+ */
+ Notebook.prototype.clear_cells_outputs = function(indices) {
+ if (!indices) {
+ indices = this.get_selected_cells_indices();
+ }
+
+ for (var i = 0; i < indices.length; i++){
+ this.clear_output(indices[i]);
+ }
+ };
+
+ /**
+ * Clear each code cell's output area.
+ */
+ Notebook.prototype.clear_all_output = function () {
+ this.get_cells().map(function (cell, i) {
+ if (cell instanceof codecell.CodeCell) {
+ cell.clear_output();
+ }
+ });
+ this.set_dirty(true);
+ };
+
+ /**
+ * Scroll the selected CodeCell's output area.
+ *
+ * @param {integer} index - cell index
+ */
+ Notebook.prototype.scroll_output = function (index) {
+ var i = this.index_or_selected(index);
+ var cell = this.get_cell(i);
+ if (cell !== null && (cell instanceof codecell.CodeCell)) {
+ cell.scroll_output();
+ this.set_dirty(true);
+ }
+ };
+
+ /**
+ * Expand each code cell's output area and add a scrollbar for long output.
+ */
+ Notebook.prototype.scroll_all_output = function () {
+ this.get_cells().map(function (cell, i) {
+ if (cell instanceof codecell.CodeCell) {
+ cell.scroll_output();
+ }
+ });
+ // this should not be set if the `collapse` key is removed from nbformat
+ this.set_dirty(true);
+ };
+
+ /**
+ * Toggle whether a cell's output is collapsed or expanded.
+ *
+ * @param {integer} index - cell index
+ */
+ Notebook.prototype.toggle_output = function (index) {
+ var i = this.index_or_selected(index);
+ var cell = this.get_cell(i);
+ if (cell !== null && (cell instanceof codecell.CodeCell)) {
+ cell.toggle_output();
+ this.set_dirty(true);
+ }
+ };
+
+ /**
+ * Toggle whether all selected cells' outputs are collapsed or expanded.
+ *
+ * @param {integer} indices - the indices of the cells to toggle
+ */
+ Notebook.prototype.toggle_cells_outputs = function(indices) {
+ if (!indices) {
+ indices = this.get_selected_cells_indices();
+ }
+
+ for (var i = 0; i < indices.length; i++){
+ this.toggle_output(indices[i]);
+ }
+ };
+
+ /**
+ * Toggle the output of all cells.
+ */
+ Notebook.prototype.toggle_all_output = function () {
+ this.get_cells().map(function (cell, i) {
+ if (cell instanceof codecell.CodeCell) {
+ cell.toggle_output();
+ }
+ });
+ // this should not be set if the `collapse` key is removed from nbformat
+ this.set_dirty(true);
+ };
+
+ /**
+ * Toggle a scrollbar for long cell outputs.
+ *
+ * @param {integer} index - cell index
+ */
+ Notebook.prototype.toggle_output_scroll = function (index) {
+ var i = this.index_or_selected(index);
+ var cell = this.get_cell(i);
+ if (cell !== null && (cell instanceof codecell.CodeCell)) {
+ cell.toggle_output_scroll();
+ this.set_dirty(true);
+ }
+ };
+
+ /**
+ * Toggle a scrollbar for selected long cells' outputs.
+ *
+ * @param {integer} indices - the indices of the cells to toggle
+ */
+ Notebook.prototype.toggle_cells_outputs_scroll = function(indices) {
+ if (!indices) {
+ indices = this.get_selected_cells_indices();
+ }
+
+ for (var i = 0; i < indices.length; i++){
+ this.toggle_output_scroll(indices[i]);
+ }
+ };
+
+ /**
+ * Toggle the scrolling of long output on all cells.
+ */
+ Notebook.prototype.toggle_all_output_scroll = function () {
+ this.get_cells().map(function (cell, i) {
+ if (cell instanceof codecell.CodeCell) {
+ cell.toggle_output_scroll();
+ }
+ });
+ // this should not be set if the `collapse` key is removed from nbformat
+ this.set_dirty(true);
+ };
+
+ // Other cell functions: line numbers, ...
+
+ /**
+ * Toggle line numbers in the selected cell's input area.
+ */
+ Notebook.prototype.cell_toggle_line_numbers = function() {
+ this.get_selected_cells().map(function(cell, i){cell.toggle_line_numbers();});
+ };
+
+
+ //dispatch codemirror mode to all cells.
+ Notebook.prototype._dispatch_mode = function(spec, newmode){
+ this.codemirror_mode = newmode;
+ codecell.CodeCell.options_default.cm_config.mode = newmode;
+ this.get_cells().map(function(cell, i) {
+ if (cell.cell_type === 'code'){
+ cell.code_mirror.setOption('mode', spec);
+ // This is currently redundant, because cm_config ends up as
+ // codemirror's own .options object, but I don't want to
+ // rely on that.
+ cell._options.cm_config.mode = spec;
+ }
+ });
+
+ };
+
+ // roughly try to check mode equality
+ var _mode_equal = function(mode1, mode2){
+ return ((mode1||{}).name||mode1)===((mode2||{}).name||mode2);
+ };
+
+ /**
+ * Set the codemirror mode for all code cells, including the default for
+ * new code cells.
+ * Set the mode to 'null' (no highlighting) if it can't be found.
+ */
+ Notebook.prototype.set_codemirror_mode = function(newmode){
+ // if mode is the same don't reset,
+ // to avoid n-time re-highlighting.
+ if (_mode_equal(newmode, this.codemirror_mode)) {
+ return;
+ }
+
+ var that = this;
+ utils.requireCodeMirrorMode(newmode, function (spec) {
+ that._dispatch_mode(spec, newmode);
+ }, function(){
+ // on error don't dispatch the new mode as re-setting it later will not work.
+ // don't either set to null mode if it has been changed in the meantime
+ if( _mode_equal(newmode, this.codemirror_mode) ){
+ that._dispatch_mode('null','null');
+ }
+ });
+ };
+
+ // Session related things
+
+ /**
+ * Start a new session and set it on each code cell.
+ */
+ Notebook.prototype.start_session = function (kernel_name) {
+ if (this._session_starting) {
+ throw new session.SessionAlreadyStarting();
+ }
+ this._session_starting = true;
+
+ var options = {
+ base_url: this.base_url,
+ ws_url: this.ws_url,
+ notebook_path: this.notebook_path,
+ notebook_name: this.notebook_name,
+ kernel_name: kernel_name,
+ notebook: this
+ };
+
+ var success = $.proxy(this._session_started, this);
+ var failure = $.proxy(this._session_start_failed, this);
+
+ if (this.session !== null) {
+ this.session.restart(options, success, failure);
+ } else {
+ this.session = new session.Session(options);
+ this.session.start(success, failure);
+ }
+ };
+
+
+ /**
+ * Once a session is started, link the code cells to the kernel and pass the
+ * comm manager to the widget manager.
+ */
+ Notebook.prototype._session_started = function (){
+ this._session_starting = false;
+ this.kernel = this.session.kernel;
+ var ncells = this.ncells();
+ for (var i=0; i<ncells; i++) {
+ var cell = this.get_cell(i);
+ if (cell instanceof codecell.CodeCell) {
+ cell.set_kernel(this.session.kernel);
+ }
+ }
+ };
+
+ /**
+ * Called when the session fails to start.
+ */
+ Notebook.prototype._session_start_failed = function(jqxhr, status, error){
+ this._session_starting = false;
+ utils.log_ajax_error(jqxhr, status, error);
+ };
+
+ /**
+ * Prompt the user to restart the kernel and re-run everything.
+ * if options.confirm === false, no confirmation dialog is shown.
+ */
+ Notebook.prototype.restart_run_all = function (options) {
+ var that = this;
+ var restart_options = {};
+ restart_options.confirm = (options || {}).confirm;
+ restart_options.dialog = {
+ notebook: that,
+ keyboard_manager: that.keyboard_manager,
+ title : "Restart kernel and re-run the whole notebook?",
+ body : $("<p/>").text(
+ 'Are you sure you want to restart the current kernel and re-execute the whole notebook? All variables and outputs will be lost.'
+ ),
+ buttons : {
+ "Restart & run all cells" : {
+ "class" : "btn-danger",
+ "click" : function () {
+ that.execute_all_cells();
+ },
+ },
+ }
+ };
+ return this._restart_kernel(restart_options);
+ };
+
+ /**
+ * Prompt the user to restart the kernel and clear output.
+ * if options.confirm === false, no confirmation dialog is shown.
+ */
+ Notebook.prototype.restart_clear_output = function (options) {
+ var that = this;
+ var restart_options = {};
+ restart_options.confirm = (options || {}).confirm;
+ restart_options.dialog = {
+ notebook: that,
+ keyboard_manager: that.keyboard_manager,
+ title : "Restart kernel and clear all output?",
+ body : $("<p/>").text(
+ 'Do you want to restart the current kernel and clear all output? All variables and outputs will be lost.'
+ ),
+ buttons : {
+ "Restart & clear all outputs" : {
+ "class" : "btn-danger",
+ "click" : function (){
+ that.clear_all_output();
+ },
+ },
+ }
+ };
+ return this._restart_kernel(restart_options);
+ };
+
+ /**
+ * Prompt the user to restart the kernel.
+ * if options.confirm === false, no confirmation dialog is shown.
+ */
+ Notebook.prototype.restart_kernel = function (options) {
+ var that = this;
+ var restart_options = {};
+ restart_options.confirm = (options || {}).confirm;
+ restart_options.dialog = {
+ title : "Restart kernel?",
+ body : $("<p/>").text(
+ 'Do you want to restart the current kernel? All variables will be lost.'
+ ),
+ buttons : {
+ "Restart" : {
+ "class" : "btn-danger",
+ "click" : function () {},
+ },
+ }
+ };
+ return this._restart_kernel(restart_options);
+ };
+
+ // inner implementation of restart dialog & promise
+ Notebook.prototype._restart_kernel = function (options) {
+ var that = this;
+ options = options || {};
+ var resolve_promise, reject_promise;
+ var promise = new Promise(function (resolve, reject){
+ resolve_promise = resolve;
+ reject_promise = reject;
+ });
+
+ function restart_and_resolve () {
+ that.kernel.restart(function () {
+ // resolve when the kernel is *ready* not just started
+ that.events.one('kernel_ready.Kernel', resolve_promise);
+ }, reject_promise);
+ }
+
+ if (options.confirm === false) {
+ var default_button = options.dialog.buttons[Object.keys(options.dialog.buttons)[0]];
+ promise.then(default_button.click);
+ restart_and_resolve();
+ return promise;
+ }
+ options.dialog.notebook = this;
+ options.dialog.keyboard_manager = this.keyboard_manager;
+ // add 'Continue running' cancel button
+ var buttons = {
+ "Continue running": {},
+ };
+ // hook up button.click actions after restart promise resolves
+ Object.keys(options.dialog.buttons).map(function (key) {
+ var button = buttons[key] = options.dialog.buttons[key];
+ var click = button.click;
+ button.click = function () {
+ promise.then(click);
+ restart_and_resolve();
+ };
+ });
+ options.dialog.buttons = buttons;
+ dialog.modal(options.dialog);
+ return promise;
+ };
+
+ /**
+ * Execute cells corresponding to the given indices.
+ *
+ * @param {list} indices - indices of the cells to execute
+ */
+ Notebook.prototype.execute_cells = function (indices) {
+ if (indices.length === 0) {
+ return;
+ }
+
+ var cell;
+ for (var i = 0; i < indices.length; i++) {
+ cell = this.get_cell(indices[i]);
+ cell.execute();
+ }
+
+ this.select(indices[indices.length - 1]);
+ this.command_mode();
+ this.set_dirty(true);
+ };
+
+ /**
+ * Execute or render cell outputs and go into command mode.
+ */
+ Notebook.prototype.execute_selected_cells = function () {
+ this.execute_cells(this.get_selected_cells_indices());
+ };
+
+
+ /**
+ * Alias for execute_selected_cells, for backwards compatibility --
+ * previously, doing "Run Cell" would only ever run a single cell (hence
+ * `execute_cell`), but now it runs all marked cells, so that's the
+ * preferable function to use. But it is good to keep this function to avoid
+ * breaking existing extensions, etc.
+ */
+ Notebook.prototype.execute_cell = function () {
+ this.execute_selected_cells();
+ };
+
+ /**
+ * Execute or render cell outputs and insert a new cell below.
+ */
+ Notebook.prototype.execute_cell_and_insert_below = function () {
+ var indices = this.get_selected_cells_indices();
+ var cell_index;
+ if (indices.length > 1) {
+ this.execute_cells(indices);
+ cell_index = Math.max.apply(Math, indices);
+ } else {
+ var cell = this.get_selected_cell();
+ cell_index = this.find_cell_index(cell);
+ cell.execute();
+ }
+
+ // If we are at the end always insert a new cell and return
+ if (cell_index === (this.ncells()-1)) {
+ this.command_mode();
+ this.insert_cell_below();
+ this.select(cell_index+1);
+ this.edit_mode();
+ this.scroll_to_bottom();
+ this.set_dirty(true);
+ return;
+ }
+
+ this.command_mode();
+ this.insert_cell_below();
+ this.select(cell_index+1);
+ this.edit_mode();
+ this.set_dirty(true);
+ };
+
+ /**
+ * Execute or render cell outputs and select the next cell.
+ */
+ Notebook.prototype.execute_cell_and_select_below = function () {
+ var indices = this.get_selected_cells_indices();
+ var cell_index;
+ if (indices.length > 1) {
+ this.execute_cells(indices);
+ cell_index = Math.max.apply(Math, indices);
+ } else {
+ var cell = this.get_selected_cell();
+ cell_index = this.find_cell_index(cell);
+ cell.execute();
+ }
+
+ // If we are at the end always insert a new cell and return
+ if (cell_index === (this.ncells()-1)) {
+ this.command_mode();
+ this.insert_cell_below();
+ this.select(cell_index+1);
+ this.edit_mode();
+ this.scroll_to_bottom();
+ this.set_dirty(true);
+ return;
+ }
+
+ this.command_mode();
+ this.select(cell_index+1);
+ this.focus_cell();
+ this.set_dirty(true);
+ };
+
+ /**
+ * Execute all cells below the selected cell.
+ */
+ Notebook.prototype.execute_cells_below = function () {
+ this.execute_cell_range(this.get_selected_index(), this.ncells());
+ this.scroll_to_bottom();
+ };
+
+ /**
+ * Execute all cells above the selected cell.
+ */
+ Notebook.prototype.execute_cells_above = function () {
+ this.execute_cell_range(0, this.get_selected_index());
+ };
+
+ /**
+ * Execute all cells.
+ */
+ Notebook.prototype.execute_all_cells = function () {
+ this.execute_cell_range(0, this.ncells());
+ this.scroll_to_bottom();
+ };
+
+ /**
+ * Execute a contiguous range of cells.
+ *
+ * @param {integer} start - index of the first cell to execute (inclusive)
+ * @param {integer} end - index of the last cell to execute (exclusive)
+ */
+ Notebook.prototype.execute_cell_range = function (start, end) {
+ this.command_mode();
+ var indices = [];
+ for (var i=start; i<end; i++) {
+ indices.push(i);
+ }
+ this.execute_cells(indices);
+ };
+
+ // Persistance and loading
+
+ /**
+ * Getter method for this notebook's name.
+ *
+ * @return {string} This notebook's name (excluding file extension)
+ */
+ Notebook.prototype.get_notebook_name = function () {
+ var nbname = utils.splitext(this.notebook_name)[0];
+ return nbname;
+ };
+
+ /**
+ * Setter method for this notebook's name.
+ *
+ * @param {string} name
+ */
+ Notebook.prototype.set_notebook_name = function (name) {
+ var parent = utils.url_path_split(this.notebook_path)[0];
+ this.notebook_name = name;
+ this.notebook_path = utils.url_path_join(parent, name);
+ };
+
+ /**
+ * Check that a notebook's name is valid.
+ *
+ * @param {string} nbname - A name for this notebook
+ * @return {boolean} True if the name is valid, false if invalid
+ */
+ Notebook.prototype.test_notebook_name = function (nbname) {
+ nbname = nbname || '';
+ if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * Load a notebook from JSON (.ipynb).
+ *
+ * @param {object} data - JSON representation of a notebook
+ */
+ Notebook.prototype.fromJSON = function (data) {
+
+ var content = data.content;
+ var ncells = this.ncells();
+ var i;
+ for (i=0; i<ncells; i++) {
+ // Always delete cell 0 as they get renumbered as they are deleted.
+ this._unsafe_delete_cell(0);
+ }
+ // Save the metadata and name.
+ this.metadata = content.metadata;
+ this.notebook_name = data.name;
+ this.notebook_path = data.path;
+ var trusted = true;
+
+ // Set the codemirror mode from language_info metadata
+ if (this.metadata.language_info !== undefined) {
+ var langinfo = this.metadata.language_info;
+ // Mode 'null' should be plain, unhighlighted text.
+ var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
+ this.set_codemirror_mode(cm_mode);
+ }
+
+ var new_cells = content.cells;
+ ncells = new_cells.length;
+ var cell_data = null;
+ var new_cell = null;
+ for (i=0; i<ncells; i++) {
+ cell_data = new_cells[i];
+ new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
+ new_cell.fromJSON(cell_data);
+ if (new_cell.cell_type === 'code' && !new_cell.output_area.trusted) {
+ trusted = false;
+ }
+ }
+ if (trusted !== this.trusted) {
+ this.trusted = trusted;
+ this.events.trigger("trust_changed.Notebook", trusted);
+ }
+ };
+
+ /**
+ * Dump this notebook into a JSON-friendly object.
+ *
+ * @return {object} A JSON-friendly representation of this notebook.
+ */
+ Notebook.prototype.toJSON = function () {
+ // remove the conversion indicator, which only belongs in-memory
+ delete this.metadata.orig_nbformat;
+ delete this.metadata.orig_nbformat_minor;
+
+ var cells = this.get_cells();
+ var ncells = cells.length;
+ var cell_array = new Array(ncells);
+ var trusted = true;
+ for (var i=0; i<ncells; i++) {
+ var cell = cells[i];
+ if (cell.cell_type === 'code' && !cell.output_area.trusted) {
+ trusted = false;
+ }
+ cell_array[i] = cell.toJSON();
+ }
+ var data = {
+ cells: cell_array,
+ metadata: this.metadata,
+ nbformat: this.nbformat,
+ nbformat_minor: this.nbformat_minor
+ };
+ if (trusted !== this.trusted) {
+ this.trusted = trusted;
+ this.events.trigger("trust_changed.Notebook", trusted);
+ }
+ return data;
+ };
+
+ /**
+ * Start an autosave timer which periodically saves the notebook.
+ *
+ * @param {integer} interval - the autosave interval in milliseconds
+ */
+ Notebook.prototype.set_autosave_interval = function (interval) {
+ var that = this;
+ // clear previous interval, so we don't get simultaneous timers
+ if (this.autosave_timer) {
+ clearInterval(this.autosave_timer);
+ }
+ if (!this.writable) {
+ // disable autosave if not writable
+ interval = 0;
+ }
+
+ this.autosave_interval = this.minimum_autosave_interval = interval;
+ if (interval) {
+ this.autosave_timer = setInterval(function() {
+ if (that.dirty) {
+ that.save_notebook();
+ }
+ }, interval);
+ this.events.trigger("autosave_enabled.Notebook", interval);
+ } else {
+ this.autosave_timer = null;
+ this.events.trigger("autosave_disabled.Notebook");
+ }
+ };
+
+ /**
+ * Save this notebook on the server. This becomes a notebook instance's
+ * .save_notebook method *after* the entire notebook has been loaded.
+ */
+ Notebook.prototype.save_notebook = function (check_last_modified) {
+ if (check_last_modified === undefined) {
+ check_last_modified = true;
+ }
+
+ var error;
+ if (!this._fully_loaded) {
+ error = new Error("Load failed, save is disabled");
+ this.events.trigger('notebook_save_failed.Notebook', error);
+ return Promise.reject(error);
+ } else if (!this.writable) {
+ error = new Error("Notebook is read-only");
+ this.events.trigger('notebook_save_failed.Notebook', error);
+ return Promise.reject(error);
+ }
+
+ // Trigger an event before save, which allows listeners to modify
+ // the notebook as needed.
+ this.events.trigger('before_save.Notebook');
+
+ // Create a JSON model to be sent to the server.
+ var model = {
+ type : "notebook",
+ content : this.toJSON()
+ };
+ // time the ajax call for autosave tuning purposes.
+ var start = new Date().getTime();
+
+ var that = this;
+ var _save = function () {
+ return that.contents.save(that.notebook_path, model).then(
+ $.proxy(that.save_notebook_success, that, start),
+ function (error) {
+ that.events.trigger('notebook_save_failed.Notebook', error);
+ }
+ );
+ };
+
+ if (check_last_modified) {
+ return this.contents.get(this.notebook_path, {content: false}).then(
+ function (data) {
+ var last_modified = new Date(data.last_modified);
+ if (last_modified > that.last_modified) {
+ console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+
+ "while the current file seem to have been saved on `"+data.last_modified+"`");
+ dialog.modal({
+ notebook: that,
+ keyboard_manager: that.keyboard_manager,
+ title: "Notebook changed",
+ body: "The notebook file has changed on disk since the last time we opened or saved it. "+
+ "Do you want to overwrite the file on disk with the version open here, or load "+
+ "the version on disk (reload the page) ?",
+ buttons: {
+ Reload: {
+ class: 'btn-warning',
+ click: function() {
+ window.location.reload();
+ }
+ },
+ Cancel: {},
+ Overwrite: {
+ class: 'btn-danger',
+ click: function () {
+ _save();
+ }
+ },
+ }
+ });
+ } else {
+ return _save();
+ }
+ }, function (error) {
+ // maybe it has been deleted or renamed? Go ahead and save.
+ return _save();
+ }
+ );
+ } else {
+ return _save();
+ }
+ };
+
+ /**
+ * Success callback for saving a notebook.
+ *
+ * @param {integer} start - Time when the save request start
+ * @param {object} data - JSON representation of a notebook
+ */
+ Notebook.prototype.save_notebook_success = function (start, data) {
+ this.set_dirty(false);
+ this.last_modified = new Date(data.last_modified);
+ // debug 484
+ this._last_modified = 'save-success:'+data.last_modified;
+ if (data.message) {
+ // save succeeded, but validation failed.
+ var body = $("<div>");
+ var title = "Notebook validation failed";
+
+ body.append($("<p>").text(
+ "The save operation succeeded," +
+ " but the notebook does not appear to be valid." +
+ " The validation error was:"
+ )).append($("<div>").addClass("validation-error").append(
+ $("<pre>").text(data.message)
+ ));
+ dialog.modal({
+ notebook: this,
+ keyboard_manager: this.keyboard_manager,
+ title: title,
+ body: body,
+ buttons : {
+ OK : {
+ "class" : "btn-primary"
+ }
+ }
+ });
+ }
+ this.events.trigger('notebook_saved.Notebook');
+ this._update_autosave_interval(start);
+ if (this._checkpoint_after_save) {
+ this.create_checkpoint();
+ this._checkpoint_after_save = false;
+ }
+ };
+
+ /**
+ * Update the autosave interval based on the duration of the last save.
+ *
+ * @param {integer} timestamp - when the save request started
+ */
+ Notebook.prototype._update_autosave_interval = function (start) {
+ var duration = (new Date().getTime() - start);
+ if (this.autosave_interval) {
+ // new save interval: higher of 10x save duration or parameter (default 30 seconds)
+ var interval = Math.max(10 * duration, this.minimum_autosave_interval);
+ // ceil to 10 seconds, otherwise we will be setting a new interval too often
+ // do not round or anything below 5000ms will desactivate saving.
+ interval = 10000 * Math.ceil(interval / 10000);
+ // set new interval, if it's changed
+ if (interval !== this.autosave_interval) {
+ this.set_autosave_interval(interval);
+ }
+ }
+ };
+
+ /**
+ * Explicitly trust the output of this notebook.
+ */
+ Notebook.prototype.trust_notebook = function () {
+ var body = $("<div>").append($("<p>")
+ .text("A trusted Jupyter notebook may execute hidden malicious code ")
+ .append($("<strong>")
+ .append(
+ $("<em>").text("when you open it")
+ )
+ ).append(".").append(
+ " Selecting trust will immediately reload this notebook in a trusted state."
+ ).append(
+ " For more information, see the "
+ ).append($("<a>").attr("href", "https://jupyter-notebook.readthedocs.io/en/latest/security.html")
+ .text("Jupyter security documentation")
+ ).append(".")
+ );
+
+ var nb = this;
+ dialog.modal({
+ notebook: this,
+ keyboard_manager: this.keyboard_manager,
+ title: "Trust this notebook?",
+ body: body,
+
+ buttons: {
+ Cancel : {},
+ Trust : {
+ class : "btn-danger",
+ click : function () {
+ var cells = nb.get_cells();
+ for (var i = 0; i < cells.length; i++) {
+ var cell = cells[i];
+ if (cell.cell_type === 'code') {
+ cell.output_area.trusted = true;
+ }
+ }
+ nb.events.on('notebook_saved.Notebook', function () {
+ window.location.reload();
+ });
+ nb.save_notebook();
+ }
+ }
+ }
+ });
+ };
+
+ /**
+ * Make a copy of the current notebook.
+ * If the notebook has unsaved changes, it is saved first.
+ */
+ Notebook.prototype.copy_notebook = function () {
+ var that = this;
+ var base_url = this.base_url;
+ var w = window.open('', IPython._target);
+ var parent = utils.url_path_split(this.notebook_path)[0];
+ var p;
+ if (this.dirty) {
+ p = this.save_notebook();
+ } else {
+ p = Promise.resolve();
+ }
+ return p.then(function () {
+ return that.contents.copy(that.notebook_path, parent).then(
+ function (data) {
+ w.location = utils.url_path_join(
+ base_url, 'notebooks', utils.encode_uri_components(data.path)
+ );
+ },
+ function(error) {
+ w.close();
+ that.events.trigger('notebook_copy_failed', error);
+ }
+ );
+ });
+ };
+
+ /**
+ * Ensure a filename has the right extension
+ * Returns the filename with the appropriate extension, appending if necessary.
+ */
+ Notebook.prototype.ensure_extension = function (name) {
+ var ext = utils.splitext(this.notebook_path)[1];
+ if (ext.length && name.slice(-ext.length) !== ext) {
+ name = name + ext;
+ }
+ return name;
+ };
+
+ /**
+ * Rename the notebook.
+ * @param {string} new_name
+ * @return {Promise} promise that resolves when the notebook is renamed.
+ */
+ Notebook.prototype.rename = function (new_name) {
+ new_name = this.ensure_extension(new_name);
+
+ var that = this;
+ var parent = utils.url_path_split(this.notebook_path)[0];
+ var new_path = utils.url_path_join(parent, new_name);
+ return this.contents.rename(this.notebook_path, new_path).then(
+ function (json) {
+ that.notebook_name = json.name;
+ that.notebook_path = json.path;
+ that.last_modified = new Date(json.last_modified);
+ // debug 484
+ that._last_modified = json.last_modified;
+ that.session.rename_notebook(json.path);
+ that.events.trigger('notebook_renamed.Notebook', json);
+ }
+ );
+ };
+
+ /**
+ * Delete this notebook
+ */
+ Notebook.prototype.delete = function () {
+ this.contents.delete(this.notebook_path);
+ };
+
+ /**
+ * Request a notebook's data from the server.
+ *
+ * @param {string} notebook_path - A notebook to load
+ */
+ Notebook.prototype.load_notebook = function (notebook_path) {
+ this.notebook_path = notebook_path;
+ this.notebook_name = utils.url_path_split(this.notebook_path)[1];
+ this.events.trigger('notebook_loading.Notebook');
+ this.contents.get(notebook_path, {type: 'notebook'}).then(
+ $.proxy(this.load_notebook_success, this),
+ $.proxy(this.load_notebook_error, this)
+ );
+ };
+
+ /**
+ * Success callback for loading a notebook from the server.
+ *
+ * Load notebook data from the JSON response.
+ *
+ * @param {object} data JSON representation of a notebook
+ */
+ Notebook.prototype.load_notebook_success = function (data) {
+ var failed, msg;
+ try {
+ this.fromJSON(data);
+ } catch (e) {
+ failed = e;
+ console.log("Notebook failed to load from JSON:", e);
+ }
+ if (failed || data.message) {
+ // *either* fromJSON failed or validation failed
+ var body = $("<div>");
+ var title;
+ if (failed) {
+ title = "Notebook failed to load";
+ body.append($("<p>").text(
+ "The error was: "
+ )).append($("<div>").addClass("js-error").text(
+ failed.toString()
+ )).append($("<p>").text(
+ "See the error console for details."
+ ));
+ } else {
+ title = "Notebook validation failed";
+ }
+
+ if (data.message) {
+ if (failed) {
+ msg = "The notebook also failed validation:";
+ } else {
+ msg = "An invalid notebook may not function properly." +
+ " The validation error was:";
+ }
+ body.append($("<p>").text(
+ msg
+ )).append($("<div>").addClass("validation-error").append(
+ $("<pre>").text(data.message)
+ ));
+ }
+
+ dialog.modal({
+ notebook: this,
+ keyboard_manager: this.keyboard_manager,
+ title: title,
+ body: body,
+ buttons : {
+ OK : {
+ "class" : "btn-primary"
+ }
+ }
+ });
+ }
+ if (this.ncells() === 0) {
+ this.insert_cell_below('code');
+ this.edit_mode(0);
+ } else {
+ this.select(0);
+ this.handle_command_mode(this.get_cell(0));
+ }
+ this.set_dirty(false);
+ this.scroll_to_top();
+ this.writable = data.writable || false;
+ this.last_modified = new Date(data.last_modified);
+ // debug 484
+ this._last_modified = 'load-success:'+data.last_modified;
+ var nbmodel = data.content;
+ var orig_nbformat = nbmodel.metadata.orig_nbformat;
+ var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
+ if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
+ var src;
+ if (nbmodel.nbformat > orig_nbformat) {
+ src = " an older notebook format ";
+ } else {
+ src = " a newer notebook format ";
+ }
+
+ msg = "This notebook has been converted from" + src +
+ "(v"+orig_nbformat+") to the current notebook " +
+ "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
+ "current notebook format will be used.";
+
+ if (nbmodel.nbformat > orig_nbformat) {
+ msg += " Older versions of Jupyter may not be able to read the new format.";
+ } else {
+ msg += " Some features of the original notebook may not be available.";
+ }
+ msg += " To preserve the original version, close the " +
+ "notebook without saving it.";
+ dialog.modal({
+ notebook: this,
+ keyboard_manager: this.keyboard_manager,
+ title : "Notebook converted",
+ body : msg,
+ buttons : {
+ OK : {
+ class : "btn-primary"
+ }
+ }
+ });
+ } else if (this.nbformat_minor < nbmodel.nbformat_minor) {
+ this.nbformat_minor = nbmodel.nbformat_minor;
+ }
+
+ if (this.session === null) {
+ var kernel_name = utils.get_url_param('kernel_name');
+ if (kernel_name) {
+ this.kernel_selector.set_kernel(kernel_name);
+ } else if (this.metadata.kernelspec) {
+ this.kernel_selector.set_kernel(this.metadata.kernelspec);
+ } else if (this.metadata.language) {
+ // compat with IJulia, IHaskell, and other early kernels
+ // adopters that where setting a language metadata.
+ this.kernel_selector.set_kernel({
+ name: "(No name)",
+ language: this.metadata.language
+ });
+ // this should be stored in kspec now, delete it.
+ // remove once we do not support notebook v3 anymore.
+ delete this.metadata.language;
+ } else {
+ // setting kernel via set_kernel above triggers start_session,
+ // otherwise start a new session with the server's default kernel
+ // spec_changed events will fire after kernel is loaded
+ this.start_session();
+ }
+ }
+ // load our checkpoint list
+ this.list_checkpoints();
+
+ // load toolbar state
+ if (this.metadata.celltoolbar) {
+ celltoolbar.CellToolbar.global_show();
+ celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
+ } else {
+ celltoolbar.CellToolbar.global_hide();
+ }
+
+ if (!this.writable) {
+ this.set_autosave_interval(0);
+ this.events.trigger('notebook_read_only.Notebook');
+ }
+
+ // now that we're fully loaded, it is safe to restore save functionality
+ this._fully_loaded = true;
+ this.events.trigger('notebook_loaded.Notebook');
+ };
+
+ Notebook.prototype.set_kernelselector = function(k_selector){
+ this.kernel_selector = k_selector;
+ };
+
+ /**
+ * Failure callback for loading a notebook from the server.
+ *
+ * @param {Error} error
+ */
+ Notebook.prototype.load_notebook_error = function (error) {
+ this.events.trigger('notebook_load_failed.Notebook', error);
+ var msg;
+ if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
+ utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
+ msg = "An unknown error occurred while loading this notebook. " +
+ "This version can load notebook formats " +
+ "v" + this.nbformat + " or earlier. See the server log for details.";
+ } else {
+ msg = error.message;
+ console.warn('Error stack trace while loading notebook was:');
+ console.warn(error.stack);
+ }
+ dialog.modal({
+ notebook: this,
+ keyboard_manager: this.keyboard_manager,
+ title: "Error loading notebook",
+ body : msg,
+ buttons : {
+ "OK": {}
+ }
+ });
+ };
+
+ /********************* checkpoint-related ********************/
+
+ /**
+ * Save the notebook then immediately create a checkpoint.
+ */
+ Notebook.prototype.save_checkpoint = function () {
+ this._checkpoint_after_save = true;
+ this.save_notebook();
+ };
+
+ /**
+ * Add a checkpoint for this notebook.
+ */
+ Notebook.prototype.add_checkpoint = function (checkpoint) {
+ var found = false;
+ for (var i = 0; i < this.checkpoints.length; i++) {
+ var existing = this.checkpoints[i];
+ if (existing.id === checkpoint.id) {
+ found = true;
+ this.checkpoints[i] = checkpoint;
+ break;
+ }
+ }
+ if (!found) {
+ this.checkpoints.push(checkpoint);
+ }
+ this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
+ };
+
+ /**
+ * List checkpoints for this notebook.
+ */
+ Notebook.prototype.list_checkpoints = function () {
+ var that = this;
+ this.contents.list_checkpoints(this.notebook_path).then(
+ $.proxy(this.list_checkpoints_success, this),
+ function(error) {
+ that.events.trigger('list_checkpoints_failed.Notebook', error);
+ }
+ );
+ };
+
+ /**
+ * Success callback for listing checkpoints.
+ *
+ * @param {object} data - JSON representation of a checkpoint
+ */
+ Notebook.prototype.list_checkpoints_success = function (data) {
+ this.checkpoints = data;
+ if (data.length) {
+ this.last_checkpoint = data[data.length - 1];
+ } else {
+ this.last_checkpoint = null;
+ }
+ this.events.trigger('checkpoints_listed.Notebook', [data]);
+ };
+
+ /**
+ * Create a checkpoint of this notebook on the server from the most recent save.
+ */
+ Notebook.prototype.create_checkpoint = function () {
+ var that = this;
+ this.contents.create_checkpoint(this.notebook_path).then(
+ $.proxy(this.create_checkpoint_success, this),
+ function (error) {
+ that.events.trigger('checkpoint_failed.Notebook', error);
+ }
+ );
+ };
+
+ /**
+ * Success callback for creating a checkpoint.
+ *
+ * @param {object} data - JSON representation of a checkpoint
+ */
+ Notebook.prototype.create_checkpoint_success = function (data) {
+ this.add_checkpoint(data);
+ this.events.trigger('checkpoint_created.Notebook', data);
+ };
+
+ /**
+ * Display the restore checkpoint dialog
+ * @param {string} checkpoint ID
+ */
+ Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
+ var that = this;
+ checkpoint = checkpoint || this.last_checkpoint;
+ if ( ! checkpoint ) {
+ console.log("restore dialog, but no checkpoint to restore to!");
+ return;
+ }
+ var body = $('<div/>').append(
+ $('<p/>').addClass("p-space").text(
+ "Are you sure you want to revert the notebook to " +
+ "the latest checkpoint?"
+ ).append(
+ $("<strong/>").text(
+ " This cannot be undone."
+ )
+ )
+ ).append(
+ $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
+ ).append(
+ $('<p/>').addClass("p-space").text(
+ moment(checkpoint.last_modified).format('LLLL') +
+ ' ('+moment(checkpoint.last_modified).fromNow()+')'// Long form: Tuesday, January 27, 2015 12:15 PM
+ ).css("text-align", "center")
+ );
+
+ dialog.modal({
+ notebook: this,
+ keyboard_manager: this.keyboard_manager,
+ title : "Revert notebook to checkpoint",
+ body : body,
+ buttons : {
+ Revert : {
+ class : "btn-danger",
+ click : function () {
+ that.restore_checkpoint(checkpoint.id);
+ }
+ },
+ Cancel : {}
+ }
+ });
+ };
+
+ /**
+ * Restore the notebook to a checkpoint state.
+ *
+ * @param {string} checkpoint ID
+ */
+ Notebook.prototype.restore_checkpoint = function (checkpoint) {
+ this.events.trigger('notebook_restoring.Notebook', checkpoint);
+ var that = this;
+ this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
+ $.proxy(this.restore_checkpoint_success, this),
+ function (error) {
+ that.events.trigger('checkpoint_restore_failed.Notebook', error);
+ }
+ );
+ };
+
+ /**
+ * Success callback for restoring a notebook to a checkpoint.
+ */
+ Notebook.prototype.restore_checkpoint_success = function () {
+ this.events.trigger('checkpoint_restored.Notebook');
+ this.load_notebook(this.notebook_path);
+ };
+
+ /**
+ * Delete a notebook checkpoint.
+ *
+ * @param {string} checkpoint ID
+ */
+ Notebook.prototype.delete_checkpoint = function (checkpoint) {
+ this.events.trigger('notebook_restoring.Notebook', checkpoint);
+ var that = this;
+ this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
+ $.proxy(this.delete_checkpoint_success, this),
+ function (error) {
+ that.events.trigger('checkpoint_delete_failed.Notebook', error);
+ }
+ );
+ };
+
+ /**
+ * Success callback for deleting a notebook checkpoint.
+ */
+ Notebook.prototype.delete_checkpoint_success = function () {
+ this.events.trigger('checkpoint_deleted.Notebook');
+ this.load_notebook(this.notebook_path);
+ };
+
+ return {'Notebook': Notebook};
+});
diff --git a/notebook/static/notebook/js/notificationarea.js b/notebook/static/notebook/js/notificationarea.js
new file mode 100644
index 0000000..36e2e55
--- /dev/null
+++ b/notebook/static/notebook/js/notificationarea.js
@@ -0,0 +1,342 @@
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/dialog',
+ 'base/js/notificationarea',
+ 'moment'
+], function($, utils, dialog, notificationarea, moment) {
+ "use strict";
+ var NotificationArea = notificationarea.NotificationArea;
+
+ var NotebookNotificationArea = function(selector, options) {
+ NotificationArea.apply(this, [selector, options]);
+ this.save_widget = options.save_widget;
+ this.notebook = options.notebook;
+ this.keyboard_manager = options.keyboard_manager;
+ };
+
+ NotebookNotificationArea.prototype = Object.create(NotificationArea.prototype);
+
+ /**
+ * Initialize the default set of notification widgets.
+ *
+ * @method init_notification_widgets
+ */
+ NotebookNotificationArea.prototype.init_notification_widgets = function () {
+ this.init_kernel_notification_widget();
+ this.init_notebook_notification_widget();
+ };
+
+ /**
+ * Initialize the notification widget for kernel status messages.
+ *
+ * @method init_kernel_notification_widget
+ */
+ NotebookNotificationArea.prototype.init_kernel_notification_widget = function () {
+ var that = this;
+ var knw = this.widget('kernel');
+ var $kernel_ind_icon = $("#kernel_indicator_icon");
+ var $modal_ind_icon = $("#modal_indicator");
+ var $readonly_ind_icon = $('#readonly-indicator');
+ var $body = $('body');
+
+ // Listen for the notebook loaded event. Set readonly indicator.
+ this.events.on('notebook_loaded.Notebook', function() {
+ if (that.notebook.writable) {
+ $readonly_ind_icon.hide();
+ } else {
+ $readonly_ind_icon.show();
+ }
+ });
+
+ // Command/Edit mode
+ this.events.on('edit_mode.Notebook', function () {
+ that.save_widget.update_document_title();
+ $body.addClass('edit_mode');
+ $body.removeClass('command_mode');
+ $modal_ind_icon.attr('title','Edit Mode');
+ });
+
+ this.events.on('command_mode.Notebook', function () {
+ that.save_widget.update_document_title();
+ $body.removeClass('edit_mode');
+ $body.addClass('command_mode');
+ $modal_ind_icon.attr('title','Command Mode');
+ });
+
+ // Implicitly start off in Command mode, switching to Edit mode will trigger event
+ $modal_ind_icon.addClass('modal_indicator').attr('title','Command Mode');
+ $body.addClass('command_mode');
+
+ // Kernel events
+
+ // this can be either kernel_created.Kernel or kernel_created.Session
+ this.events.on('kernel_created.Kernel kernel_created.Session', function () {
+ knw.info("Kernel Created", 500);
+ });
+
+ this.events.on('kernel_reconnecting.Kernel', function () {
+ knw.warning("Connecting to kernel");
+ });
+
+ this.events.on('kernel_connection_dead.Kernel', function (evt, info) {
+ knw.danger("Not Connected", undefined, function () {
+ // schedule reconnect a short time in the future, don't reconnect immediately
+ setTimeout($.proxy(info.kernel.reconnect, info.kernel), 500);
+ }, {title: 'click to reconnect'});
+ });
+
+ this.events.on('kernel_connected.Kernel', function () {
+ knw.info("Connected", 500);
+ });
+
+ this.events.on('kernel_restarting.Kernel', function () {
+ that.save_widget.update_document_title();
+ knw.set_message("Restarting kernel", 2000);
+ });
+
+ this.events.on('kernel_autorestarting.Kernel', function (evt, info) {
+ // Only show the dialog on the first restart attempt. This
+ // number gets tracked by the `Kernel` object and passed
+ // along here, because we don't want to show the user 5
+ // dialogs saying the same thing (which is the number of
+ // times it tries restarting).
+ if (info.attempt === 1) {
+
+ dialog.kernel_modal({
+ notebook: that.notebook,
+ keyboard_manager: that.keyboard_manager,
+ title: "Kernel Restarting",
+ body: "The kernel appears to have died. It will restart automatically.",
+ buttons: {
+ OK : {
+ class : "btn-primary"
+ }
+ }
+ });
+ }
+
+ that.save_widget.update_document_title();
+ knw.danger("Dead kernel");
+ $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
+ });
+
+ this.events.on('kernel_interrupting.Kernel', function () {
+ knw.set_message("Interrupting kernel", 2000);
+ });
+
+ this.events.on('kernel_disconnected.Kernel', function () {
+ $kernel_ind_icon
+ .attr('class', 'kernel_disconnected_icon')
+ .attr('title', 'No Connection to Kernel');
+ });
+
+ this.events.on('kernel_connection_failed.Kernel', function (evt, info) {
+ // only show the dialog if this is the first failed
+ // connect attempt, because the kernel will continue
+ // trying to reconnect and we don't want to spam the user
+ // with messages
+ if (info.attempt === 1) {
+
+ var msg = "A connection to the notebook server could not be established." +
+ " The notebook will continue trying to reconnect. Check your" +
+ " network connection or notebook server configuration.";
+
+ dialog.kernel_modal({
+ title: "Connection failed",
+ body: msg,
+ keyboard_manager: that.keyboard_manager,
+ notebook: that.notebook,
+ buttons : {
+ "OK": {}
+ }
+ });
+ }
+ });
+
+ this.events.on('kernel_killed.Kernel kernel_killed.Session', function () {
+ that.save_widget.update_document_title();
+ knw.warning("No kernel");
+ $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel is not running');
+ });
+
+ this.events.on('kernel_dead.Kernel', function () {
+
+ var showMsg = function () {
+
+ var msg = 'The kernel has died, and the automatic restart has failed.' +
+ ' It is possible the kernel cannot be restarted.' +
+ ' If you are not able to restart the kernel, you will still be able to save' +
+ ' the notebook, but running code will no longer work until the notebook' +
+ ' is reopened.';
+
+ dialog.kernel_modal({
+ title: "Dead kernel",
+ body : msg,
+ keyboard_manager: that.keyboard_manager,
+ notebook: that.notebook,
+ buttons : {
+ "Try restarting now": {
+ class: "btn-danger",
+ click: function () {
+ that.notebook.start_session();
+ }
+ },
+ "Don't restart": {}
+ }
+ });
+
+ return false;
+ };
+
+ that.save_widget.update_document_title();
+ knw.danger("Dead kernel", undefined, showMsg);
+ $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
+
+ showMsg();
+ });
+
+ this.events.on("no_kernel.Kernel", function (evt, data) {
+ $("#kernel_indicator").find('.kernel_indicator_name').text("No Kernel");
+ });
+
+ this.events.on('kernel_dead.Session', function (evt, info) {
+ var full = info.xhr.responseJSON.message;
+ var short = info.xhr.responseJSON.short_message || 'Kernel error';
+ var traceback = info.xhr.responseJSON.traceback;
+
+ var showMsg = function () {
+ var msg = $('<div/>').append($('<p/>').text(full));
+ var cm, cm_elem, cm_open;
+
+ if (traceback) {
+ cm_elem = $('<div/>')
+ .css('margin-top', '1em')
+ .css('padding', '1em')
+ .addClass('output_scroll');
+ msg.append(cm_elem);
+ cm = new CodeMirror(cm_elem.get(0), {
+ mode: "python",
+ readOnly : true
+ });
+ cm.setValue(traceback);
+ cm_open = $.proxy(cm.refresh, cm);
+ }
+
+ dialog.kernel_modal({
+ title: "Failed to start the kernel",
+ body : msg,
+ keyboard_manager: that.keyboard_manager,
+ notebook: that.notebook,
+ open: cm_open,
+ buttons : {
+ "Ok": { class: 'btn-primary' }
+ }
+ });
+
+ return false;
+ };
+
+ that.save_widget.update_document_title();
+ $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead');
+ knw.danger(short, undefined, showMsg);
+ });
+
+ this.events.on('kernel_starting.Kernel kernel_created.Session', function () {
+ window.document.title='(Starting) '+window.document.title;
+ $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
+ knw.set_message("Kernel starting, please wait...");
+ });
+
+ this.events.on('kernel_ready.Kernel', function () {
+ that.save_widget.update_document_title();
+ $kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
+ knw.info("Kernel ready", 500);
+ });
+
+ this.events.on('kernel_idle.Kernel', function () {
+ that.save_widget.update_document_title();
+ $kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
+ });
+
+ this.events.on('kernel_busy.Kernel', function () {
+ window.document.title='(Busy) '+window.document.title;
+ $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
+ });
+
+ this.events.on('spec_match_found.Kernel', function (evt, data) {
+ that.widget('kernelspec').info("Using kernel: " + data.found.spec.display_name, 3000, undefined, {
+ title: "Only candidate for language: " + data.selected.language + " was " + data.found.spec.display_name
+ });
+ });
+
+
+ // Start the kernel indicator in the busy state, and send a kernel_info request.
+ // When the kernel_info reply arrives, the kernel is idle.
+ $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
+ };
+
+ /**
+ * Initialize the notification widget for notebook status messages.
+ *
+ * @method init_notebook_notification_widget
+ */
+ NotebookNotificationArea.prototype.init_notebook_notification_widget = function () {
+ var nnw = this.widget('notebook');
+
+ // Notebook events
+ this.events.on('notebook_loading.Notebook', function () {
+ nnw.set_message("Loading notebook",500);
+ });
+ this.events.on('notebook_loaded.Notebook', function () {
+ nnw.set_message("Notebook loaded",500);
+ });
+ this.events.on('notebook_saving.Notebook', function () {
+ nnw.set_message("Saving notebook",500);
+ });
+ this.events.on('notebook_saved.Notebook', function () {
+ nnw.set_message("Notebook saved",2000);
+ });
+ this.events.on('notebook_save_failed.Notebook', function (evt, error) {
+ nnw.warning(error.message || "Notebook save failed");
+ });
+ this.events.on('notebook_copy_failed.Notebook', function (evt, error) {
+ nnw.warning(error.message || "Notebook copy failed");
+ });
+
+ // Checkpoint events
+ this.events.on('checkpoint_created.Notebook', function (evt, data) {
+ var msg = "Checkpoint created";
+ if (data.last_modified) {
+ var d = new Date(data.last_modified);
+ msg = msg + ": " + moment(d).format("HH:mm:ss");
+ }
+ nnw.set_message(msg, 2000);
+ });
+ this.events.on('checkpoint_failed.Notebook', function () {
+ nnw.warning("Checkpoint failed");
+ });
+ this.events.on('checkpoint_deleted.Notebook', function () {
+ nnw.set_message("Checkpoint deleted", 500);
+ });
+ this.events.on('checkpoint_delete_failed.Notebook', function () {
+ nnw.warning("Checkpoint delete failed");
+ });
+ this.events.on('checkpoint_restoring.Notebook', function () {
+ nnw.set_message("Restoring to checkpoint...", 500);
+ });
+ this.events.on('checkpoint_restore_failed.Notebook', function () {
+ nnw.warning("Checkpoint restore failed");
+ });
+
+ // Autosave events
+ this.events.on('autosave_disabled.Notebook', function () {
+ nnw.set_message("Autosave disabled", 2000);
+ });
+ this.events.on('autosave_enabled.Notebook', function (evt, interval) {
+ nnw.set_message("Saving every " + interval / 1000 + "s", 1000);
+ });
+ };
+
+ return {'NotebookNotificationArea': NotebookNotificationArea};
+});
diff --git a/notebook/static/notebook/js/outputarea.js b/notebook/static/notebook/js/outputarea.js
new file mode 100644
index 0000000..97d9583
--- /dev/null
+++ b/notebook/static/notebook/js/outputarea.js
@@ -0,0 +1,966 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery-ui',
+ 'base/js/utils',
+ 'base/js/security',
+ 'base/js/keyboard',
+ 'notebook/js/mathjaxutils',
+ 'components/marked/lib/marked',
+], function($, utils, security, keyboard, mathjaxutils, marked) {
+ "use strict";
+
+ /**
+ * @class OutputArea
+ *
+ * @constructor
+ */
+
+ var OutputArea = function (options) {
+ this.selector = options.selector;
+ this.events = options.events;
+ this.keyboard_manager = options.keyboard_manager;
+ this.wrapper = $(options.selector);
+ this.outputs = [];
+ this.collapsed = false;
+ this.scrolled = false;
+ this.scroll_state = 'auto';
+ this.trusted = true;
+ this.clear_queued = null;
+ if (options.prompt_area === undefined) {
+ this.prompt_area = true;
+ } else {
+ this.prompt_area = options.prompt_area;
+ }
+ this.create_elements();
+ this.style();
+ this.bind_events();
+ };
+
+
+ /**
+ * Class prototypes
+ **/
+
+ OutputArea.prototype.create_elements = function () {
+ this.element = $("<div/>");
+ this.collapse_button = $("<div/>");
+ this.prompt_overlay = $("<div/>");
+ this.wrapper.append(this.prompt_overlay);
+ this.wrapper.append(this.element);
+ this.wrapper.append(this.collapse_button);
+ };
+
+
+ OutputArea.prototype.style = function () {
+ this.collapse_button.hide();
+ this.prompt_overlay.hide();
+
+ this.wrapper.addClass('output_wrapper');
+ this.element.addClass('output');
+
+ this.collapse_button.addClass("btn btn-default output_collapsed");
+ this.collapse_button.attr('title', 'click to expand output');
+ this.collapse_button.text('. . .');
+
+ this.prompt_overlay.addClass('out_prompt_overlay prompt');
+ this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
+
+ this.collapse();
+ };
+
+ /**
+ * Should the OutputArea scroll?
+ * Returns whether the height (in lines) exceeds the current threshold.
+ * Threshold will be OutputArea.minimum_scroll_threshold if scroll_state=true (manually requested)
+ * or OutputArea.auto_scroll_threshold if scroll_state='auto'.
+ * This will always return false if scroll_state=false (scroll disabled).
+ *
+ */
+ OutputArea.prototype._should_scroll = function () {
+ var threshold;
+ if (this.scroll_state === false) {
+ return false;
+ } else if (this.scroll_state === true) {
+ threshold = OutputArea.minimum_scroll_threshold;
+ } else {
+ threshold = OutputArea.auto_scroll_threshold;
+ }
+ if (threshold <=0) {
+ return false;
+ }
+ // line-height from http://stackoverflow.com/questions/1185151
+ var fontSize = this.element.css('font-size');
+ var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
+ return (this.element.height() > threshold * lineHeight);
+ };
+
+
+ OutputArea.prototype.bind_events = function () {
+ var that = this;
+ this.prompt_overlay.dblclick(function () { that.toggle_output(); });
+ this.prompt_overlay.click(function () { that.toggle_scroll(); });
+
+ this.element.resize(function () {
+ // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
+ if ( utils.browser[0] === "Firefox" ) {
+ return;
+ }
+ // maybe scroll output,
+ // if it's grown large enough and hasn't already been scrolled.
+ if (!that.scrolled && that._should_scroll()) {
+ that.scroll_area();
+ }
+ });
+ this.collapse_button.click(function () {
+ that.expand();
+ });
+ };
+
+
+ OutputArea.prototype.collapse = function () {
+ if (!this.collapsed) {
+ this.element.hide();
+ this.prompt_overlay.hide();
+ if (this.element.html()){
+ this.collapse_button.show();
+ }
+ this.collapsed = true;
+ // collapsing output clears scroll state
+ this.scroll_state = 'auto';
+ }
+ };
+
+
+ OutputArea.prototype.expand = function () {
+ if (this.collapsed) {
+ this.collapse_button.hide();
+ this.element.show();
+ if (this.prompt_area) {
+ this.prompt_overlay.show();
+ }
+ this.collapsed = false;
+ this.scroll_if_long();
+ }
+ };
+
+
+ OutputArea.prototype.toggle_output = function () {
+ if (this.collapsed) {
+ this.expand();
+ } else {
+ this.collapse();
+ }
+ };
+
+
+ OutputArea.prototype.scroll_area = function () {
+ this.element.addClass('output_scroll');
+ this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
+ this.scrolled = true;
+ };
+
+
+ OutputArea.prototype.unscroll_area = function () {
+ this.element.removeClass('output_scroll');
+ this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
+ this.scrolled = false;
+ };
+
+ /**
+ * Scroll OutputArea if height exceeds a threshold.
+ *
+ * Threshold is OutputArea.minimum_scroll_threshold if scroll_state = true,
+ * OutputArea.auto_scroll_threshold if scroll_state='auto'.
+ *
+ **/
+ OutputArea.prototype.scroll_if_long = function () {
+ var should_scroll = this._should_scroll();
+ if (!this.scrolled && should_scroll) {
+ // only allow scrolling long-enough output
+ this.scroll_area();
+ } else if (this.scrolled && !should_scroll) {
+ // scrolled and shouldn't be
+ this.unscroll_area();
+ }
+ };
+
+
+ OutputArea.prototype.toggle_scroll = function () {
+ if (this.scroll_state == 'auto') {
+ this.scroll_state = !this.scrolled;
+ } else {
+ this.scroll_state = !this.scroll_state;
+ }
+ if (this.scrolled) {
+ this.unscroll_area();
+ } else {
+ // only allow scrolling long-enough output
+ this.scroll_if_long();
+ }
+ };
+
+
+ // typeset with MathJax if MathJax is available
+ OutputArea.prototype.typeset = function () {
+ utils.typeset(this.element);
+ };
+
+
+ OutputArea.prototype.handle_output = function (msg) {
+ var json = {};
+ var msg_type = json.output_type = msg.header.msg_type;
+ var content = msg.content;
+ if (msg_type === "stream") {
+ json.text = content.text;
+ json.name = content.name;
+ } else if (msg_type === "display_data") {
+ json.data = content.data;
+ json.metadata = content.metadata;
+ } else if (msg_type === "execute_result") {
+ json.data = content.data;
+ json.metadata = content.metadata;
+ json.execution_count = content.execution_count;
+ } else if (msg_type === "error") {
+ json.ename = content.ename;
+ json.evalue = content.evalue;
+ json.traceback = content.traceback;
+ } else {
+ console.log("unhandled output message", msg);
+ return;
+ }
+ this.append_output(json);
+ };
+
+
+ OutputArea.output_types = [
+ 'application/javascript',
+ 'text/html',
+ 'text/markdown',
+ 'text/latex',
+ 'image/svg+xml',
+ 'image/png',
+ 'image/jpeg',
+ 'application/pdf',
+ 'text/plain'
+ ];
+
+ OutputArea.prototype.validate_mimebundle = function (bundle) {
+ /** scrub invalid outputs */
+ if (typeof bundle.data !== 'object') {
+ console.warn("mimebundle missing data", bundle);
+ bundle.data = {};
+ }
+ if (typeof bundle.metadata !== 'object') {
+ console.warn("mimebundle missing metadata", bundle);
+ bundle.metadata = {};
+ }
+ var data = bundle.data;
+ $.map(OutputArea.output_types, function(key){
+ if (key !== 'application/json' &&
+ data[key] !== undefined &&
+ typeof data[key] !== 'string'
+ ) {
+ console.log("Invalid type for " + key, data[key]);
+ delete data[key];
+ }
+ });
+ return bundle;
+ };
+
+ OutputArea.prototype.append_output = function (json) {
+ this.expand();
+
+ // Clear the output if clear is queued.
+ var needs_height_reset = false;
+ if (this.clear_queued) {
+ this.clear_output(false);
+ needs_height_reset = true;
+ }
+
+ var record_output = true;
+ switch(json.output_type) {
+ case 'execute_result':
+ json = this.validate_mimebundle(json);
+ this.append_execute_result(json);
+ break;
+ case 'stream':
+ // append_stream might have merged the output with earlier stream output
+ record_output = this.append_stream(json);
+ break;
+ case 'error':
+ this.append_error(json);
+ break;
+ case 'display_data':
+ // append handled below
+ json = this.validate_mimebundle(json);
+ break;
+ default:
+ console.log("unrecognized output type: " + json.output_type);
+ this.append_unrecognized(json);
+ }
+
+ // We must release the animation fixed height in a callback since Gecko
+ // (FireFox) doesn't render the image immediately as the data is
+ // available.
+ var that = this;
+ var handle_appended = function ($el) {
+ /**
+ * Only reset the height to automatic if the height is currently
+ * fixed (done by wait=True flag on clear_output).
+ */
+ if (needs_height_reset) {
+ that.element.height('');
+ }
+ that.element.trigger('resize');
+ };
+ if (json.output_type === 'display_data') {
+ this.append_display_data(json, handle_appended);
+ } else {
+ handle_appended();
+ }
+
+ if (record_output) {
+ this.outputs.push(json);
+ }
+ };
+
+
+ OutputArea.prototype.create_output_area = function () {
+ var oa = $("<div/>").addClass("output_area");
+ if (this.prompt_area) {
+ oa.append($('<div/>').addClass('prompt'));
+ }
+ return oa;
+ };
+
+
+ function _get_metadata_key(metadata, key, mime) {
+ var mime_md = metadata[mime];
+ // mime-specific higher priority
+ if (mime_md && mime_md[key] !== undefined) {
+ return mime_md[key];
+ }
+ // fallback on global
+ return metadata[key];
+ }
+
+ OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
+ var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
+ if (_get_metadata_key(md, 'isolated', mime)) {
+ // Create an iframe to isolate the subarea from the rest of the
+ // document
+ var iframe = $('<iframe/>').addClass('box-flex1');
+ iframe.css({'height':1, 'width':'100%', 'display':'block'});
+ iframe.attr('frameborder', 0);
+ iframe.attr('scrolling', 'auto');
+
+ // Once the iframe is loaded, the subarea is dynamically inserted
+ iframe.on('load', function() {
+ // Workaround needed by Firefox, to properly render svg inside
+ // iframes, see http://stackoverflow.com/questions/10177190/
+ // svg-dynamically-added-to-iframe-does-not-render-correctly
+ this.contentDocument.open();
+
+ // Insert the subarea into the iframe
+ // We must directly write the html. When using Jquery's append
+ // method, javascript is evaluated in the parent document and
+ // not in the iframe document. At this point, subarea doesn't
+ // contain any user content.
+ this.contentDocument.write(subarea.html());
+
+ this.contentDocument.close();
+
+ var body = this.contentDocument.body;
+ // Adjust the iframe height automatically
+ iframe.height(body.scrollHeight + 'px');
+ });
+
+ // Elements should be appended to the inner subarea and not to the
+ // iframe
+ iframe.append = function(that) {
+ subarea.append(that);
+ };
+
+ return iframe;
+ } else {
+ return subarea;
+ }
+ };
+
+
+ OutputArea.prototype._append_javascript_error = function (err, element) {
+ /**
+ * display a message when a javascript error occurs in display output
+ */
+ var msg = "Javascript error adding output!";
+ if ( element === undefined ) return;
+ element
+ .append($('<div/>').text(msg).addClass('js-error'))
+ .append($('<div/>').text(err.toString()).addClass('js-error'))
+ .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
+ };
+
+ OutputArea.prototype._safe_append = function (toinsert) {
+ /**
+ * safely append an item to the document
+ * this is an object created by user code,
+ * and may have errors, which should not be raised
+ * under any circumstances.
+ */
+ try {
+ this.element.append(toinsert);
+ } catch(err) {
+ console.log(err);
+ // Create an actual output_area and output_subarea, which creates
+ // the prompt area and the proper indentation.
+ var toinsert = this.create_output_area();
+ var subarea = $('<div/>').addClass('output_subarea');
+ toinsert.append(subarea);
+ this._append_javascript_error(err, subarea);
+ this.element.append(toinsert);
+ }
+
+ // Notify others of changes.
+ this.element.trigger('changed');
+ };
+
+
+ OutputArea.prototype.append_execute_result = function (json) {
+ var n = json.execution_count || ' ';
+ var toinsert = this.create_output_area();
+ if (this.prompt_area) {
+ toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
+ }
+ var inserted = this.append_mime_type(json, toinsert);
+ if (inserted) {
+ inserted.addClass('output_result');
+ }
+ this._safe_append(toinsert);
+ // If we just output latex, typeset it.
+ if ((json.data['text/latex'] !== undefined) ||
+ (json.data['text/html'] !== undefined) ||
+ (json.data['text/markdown'] !== undefined)) {
+ this.typeset();
+ }
+ };
+
+
+ OutputArea.prototype.append_error = function (json) {
+ var tb = json.traceback;
+ if (tb !== undefined && tb.length > 0) {
+ var s = '';
+ var len = tb.length;
+ for (var i=0; i<len; i++) {
+ s = s + tb[i] + '\n';
+ }
+ s = s + '\n';
+ var toinsert = this.create_output_area();
+ var append_text = OutputArea.append_map['text/plain'];
+ if (append_text) {
+ append_text.apply(this, [s, {}, toinsert]).addClass('output_error');
+ }
+ this._safe_append(toinsert);
+ }
+ };
+
+
+ OutputArea.prototype.append_stream = function (json) {
+ var text = json.text;
+ if (typeof text !== 'string') {
+ console.error("Stream output is invalid (missing text)", json);
+ return false;
+ }
+ var subclass = "output_"+json.name;
+ if (this.outputs.length > 0){
+ // have at least one output to consider
+ var last = this.outputs[this.outputs.length-1];
+ if (last.output_type == 'stream' && json.name == last.name){
+ // latest output was in the same stream,
+ // so append directly into its pre tag
+ // escape ANSI & HTML specials:
+ last.text = utils.fixCarriageReturn(last.text + json.text);
+ var pre = this.element.find('div.'+subclass).last().find('pre');
+ var html = utils.fixConsole(last.text);
+ html = utils.autoLinkUrls(html);
+ // The only user content injected with this HTML call is
+ // escaped by the fixConsole() method.
+ pre.html(html);
+ // return false signals that we merged this output with the previous one,
+ // and the new output shouldn't be recorded.
+ return false;
+ }
+ }
+
+ if (!text.replace("\r", "")) {
+ // text is nothing (empty string, \r, etc.)
+ // so don't append any elements, which might add undesirable space
+ // return true to indicate the output should be recorded.
+ return true;
+ }
+
+ // If we got here, attach a new div
+ var toinsert = this.create_output_area();
+ var append_text = OutputArea.append_map['text/plain'];
+ if (append_text) {
+ append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass);
+ }
+ this._safe_append(toinsert);
+ return true;
+ };
+
+
+ OutputArea.prototype.append_unrecognized = function (json) {
+ var that = this;
+ var toinsert = this.create_output_area();
+ var subarea = $('<div/>').addClass('output_subarea output_unrecognized');
+ toinsert.append(subarea);
+ subarea.append(
+ $("<a>")
+ .attr("href", "#")
+ .text("Unrecognized output: " + json.output_type)
+ .click(function () {
+ that.events.trigger('unrecognized_output.OutputArea', {output: json});
+ })
+ );
+ this._safe_append(toinsert);
+ };
+
+
+ OutputArea.prototype.append_display_data = function (json, handle_inserted) {
+ var toinsert = this.create_output_area();
+ if (this.append_mime_type(json, toinsert, handle_inserted)) {
+ this._safe_append(toinsert);
+ // If we just output latex, typeset it.
+ if ((json.data['text/latex'] !== undefined) ||
+ (json.data['text/html'] !== undefined) ||
+ (json.data['text/markdown'] !== undefined)) {
+ this.typeset();
+ }
+ }
+ };
+
+
+ OutputArea.safe_outputs = {
+ 'text/plain' : true,
+ 'text/latex' : true,
+ 'image/png' : true,
+ 'image/jpeg' : true
+ };
+
+ OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) {
+ for (var i=0; i < OutputArea.display_order.length; i++) {
+ var type = OutputArea.display_order[i];
+ var append = OutputArea.append_map[type];
+ if ((json.data[type] !== undefined) && append) {
+ var value = json.data[type];
+ if (!this.trusted && !OutputArea.safe_outputs[type]) {
+ // not trusted, sanitize HTML
+ if (type==='text/html' || type==='text/svg') {
+ value = security.sanitize_html(value);
+ } else {
+ // don't display if we don't know how to sanitize it
+ console.log("Ignoring untrusted " + type + " output.");
+ continue;
+ }
+ }
+ var md = json.metadata || {};
+ var toinsert = append.apply(this, [value, md, element, handle_inserted]);
+ // Since only the png and jpeg mime types call the inserted
+ // callback, if the mime type is something other we must call the
+ // inserted callback only when the element is actually inserted
+ // into the DOM. Use a timeout of 0 to do this.
+ if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) {
+ setTimeout(handle_inserted, 0);
+ }
+ this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]);
+ return toinsert;
+ }
+ }
+ return null;
+ };
+
+
+ var append_html = function (html, md, element) {
+ var type = 'text/html';
+ var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
+ this.keyboard_manager.register_events(toinsert);
+ toinsert.append(html);
+ dblclick_to_reset_size(toinsert.find('img'));
+ element.append(toinsert);
+ return toinsert;
+ };
+
+
+ var append_markdown = function(markdown, md, element) {
+ var type = 'text/markdown';
+ var toinsert = this.create_output_subarea(md, "output_markdown rendered_html", type);
+ var text_and_math = mathjaxutils.remove_math(markdown);
+ var text = text_and_math[0];
+ var math = text_and_math[1];
+ marked(text, function (err, html) {
+ html = mathjaxutils.replace_math(html, math);
+ toinsert.append(html);
+ });
+ dblclick_to_reset_size(toinsert.find('img'));
+ element.append(toinsert);
+ return toinsert;
+ };
+
+
+ var append_javascript = function (js, md, element) {
+ /**
+ * We just eval the JS code, element appears in the local scope.
+ */
+ var type = 'application/javascript';
+ var toinsert = this.create_output_subarea(md, "output_javascript rendered_html", type);
+ this.keyboard_manager.register_events(toinsert);
+ element.append(toinsert);
+
+ // Fix for ipython/issues/5293, make sure `element` is the area which
+ // output can be inserted into at the time of JS execution.
+ element = toinsert;
+ try {
+ eval(js);
+ } catch(err) {
+ console.log(err);
+ this._append_javascript_error(err, toinsert);
+ }
+ return toinsert;
+ };
+
+
+ var append_text = function (data, md, element) {
+ var type = 'text/plain';
+ var toinsert = this.create_output_subarea(md, "output_text", type);
+ // escape ANSI & HTML specials in plaintext:
+ data = utils.fixConsole(data);
+ data = utils.fixCarriageReturn(data);
+ data = utils.autoLinkUrls(data);
+ // The only user content injected with this HTML call is
+ // escaped by the fixConsole() method.
+ toinsert.append($("<pre/>").html(data));
+ element.append(toinsert);
+ return toinsert;
+ };
+
+
+ var append_svg = function (svg_html, md, element) {
+ var type = 'image/svg+xml';
+ var toinsert = this.create_output_subarea(md, "output_svg", type);
+
+ // Get the svg element from within the HTML.
+ // One svg is supposed, but could embed other nested svgs
+ var svg = $($('<div \>').html(svg_html).find('svg')[0]);
+ var svg_area = $('<div />');
+ var width = svg.attr('width');
+ var height = svg.attr('height');
+ svg
+ .width('100%')
+ .height('100%');
+ svg_area
+ .width(width)
+ .height(height);
+
+ svg_area.append(svg);
+ toinsert.append(svg_area);
+ element.append(toinsert);
+
+ return toinsert;
+ };
+
+ function dblclick_to_reset_size (img) {
+ /**
+ * Double-click on an image toggles confinement to notebook width
+ *
+ * img: jQuery element
+ */
+
+ img.dblclick(function () {
+ // dblclick toggles *raw* size, disabling max-width confinement.
+ if (img.hasClass('unconfined')) {
+ img.removeClass('unconfined');
+ } else {
+ img.addClass('unconfined');
+ }
+ });
+ }
+
+ var set_width_height = function (img, md, mime) {
+ /**
+ * set width and height of an img element from metadata
+ */
+ var height = _get_metadata_key(md, 'height', mime);
+ if (height !== undefined) img.attr('height', height);
+ var width = _get_metadata_key(md, 'width', mime);
+ if (width !== undefined) img.attr('width', width);
+ if (_get_metadata_key(md, 'unconfined', mime)) {
+ img.addClass('unconfined');
+ }
+ };
+
+ var append_png = function (png, md, element, handle_inserted) {
+ var type = 'image/png';
+ var toinsert = this.create_output_subarea(md, "output_png", type);
+ var img = $("<img/>");
+ if (handle_inserted !== undefined) {
+ img.on('load', function(){
+ handle_inserted(img);
+ });
+ }
+ img[0].src = 'data:image/png;base64,'+ png;
+ set_width_height(img, md, 'image/png');
+ dblclick_to_reset_size(img);
+ toinsert.append(img);
+ element.append(toinsert);
+ return toinsert;
+ };
+
+
+ var append_jpeg = function (jpeg, md, element, handle_inserted) {
+ var type = 'image/jpeg';
+ var toinsert = this.create_output_subarea(md, "output_jpeg", type);
+ var img = $("<img/>");
+ if (handle_inserted !== undefined) {
+ img.on('load', function(){
+ handle_inserted(img);
+ });
+ }
+ img[0].src = 'data:image/jpeg;base64,'+ jpeg;
+ set_width_height(img, md, 'image/jpeg');
+ dblclick_to_reset_size(img);
+ toinsert.append(img);
+ element.append(toinsert);
+ return toinsert;
+ };
+
+
+ var append_pdf = function (pdf, md, element) {
+ var type = 'application/pdf';
+ var toinsert = this.create_output_subarea(md, "output_pdf", type);
+ var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
+ a.attr('target', '_blank');
+ a.text('View PDF');
+ toinsert.append(a);
+ element.append(toinsert);
+ return toinsert;
+ };
+
+ var append_latex = function (latex, md, element) {
+ /**
+ * This method cannot do the typesetting because the latex first has to
+ * be on the page.
+ */
+ var type = 'text/latex';
+ var toinsert = this.create_output_subarea(md, "output_latex", type);
+ toinsert.text(latex);
+ element.append(toinsert);
+ return toinsert;
+ };
+
+
+ OutputArea.prototype.append_raw_input = function (msg) {
+ var that = this;
+ this.expand();
+ var content = msg.content;
+ var area = this.create_output_area();
+
+ // disable any other raw_inputs, if they are left around
+ $("div.output_subarea.raw_input_container").remove();
+
+ var input_type = content.password ? 'password' : 'text';
+
+ area.append(
+ $("<div/>")
+ .addClass("box-flex1 output_subarea raw_input_container")
+ .append(
+ $("<pre/>")
+ .addClass("raw_input_prompt")
+ .html(utils.fixConsole(content.prompt))
+ .append(
+ $("<input/>")
+ .addClass("raw_input")
+ .attr('type', input_type)
+ .attr("size", 47)
+ .keydown(function (event, ui) {
+ // make sure we submit on enter,
+ // and don't re-execute the *cell* on shift-enter
+ if (event.which === keyboard.keycodes.enter) {
+ that._submit_raw_input();
+ return false;
+ }
+ })
+ )
+ )
+ );
+
+ this.element.append(area);
+ var raw_input = area.find('input.raw_input');
+ // Register events that enable/disable the keyboard manager while raw
+ // input is focused.
+ this.keyboard_manager.register_events(raw_input);
+ // Note, the following line used to read raw_input.focus().focus().
+ // This seemed to be needed otherwise only the cell would be focused.
+ // But with the modal UI, this seems to work fine with one call to focus().
+ raw_input.focus();
+ };
+
+ OutputArea.prototype._submit_raw_input = function (evt) {
+ var container = this.element.find("div.raw_input_container");
+ var theprompt = container.find("pre.raw_input_prompt");
+ var theinput = container.find("input.raw_input");
+ var value = theinput.val();
+ var echo = value;
+ // don't echo if it's a password
+ if (theinput.attr('type') == 'password') {
+ echo = '········';
+ }
+ var content = {
+ output_type : 'stream',
+ name : 'stdout',
+ text : theprompt.text() + echo + '\n'
+ };
+ // remove form container
+ container.parent().remove();
+ // replace with plaintext version in stdout
+ this.append_output(content, false);
+ this.events.trigger('send_input_reply.Kernel', value);
+ };
+
+
+ OutputArea.prototype.handle_clear_output = function (msg) {
+ /**
+ * msg spec v4 had stdout, stderr, display keys
+ * v4.1 replaced these with just wait
+ * The default behavior is the same (stdout=stderr=display=True, wait=False),
+ * so v4 messages will still be properly handled,
+ * except for the rarely used clearing less than all output.
+ */
+ this.clear_output(msg.content.wait || false);
+ };
+
+
+ OutputArea.prototype.clear_output = function(wait, ignore_que) {
+ if (wait) {
+
+ // If a clear is queued, clear before adding another to the queue.
+ if (this.clear_queued) {
+ this.clear_output(false);
+ }
+
+ this.clear_queued = true;
+ } else {
+
+ // Fix the output div's height if the clear_output is waiting for
+ // new output (it is being used in an animation).
+ if (!ignore_que && this.clear_queued) {
+ var height = this.element.height();
+ this.element.height(height);
+ this.clear_queued = false;
+ }
+
+ // Clear all
+ // Remove load event handlers from img tags because we don't want
+ // them to fire if the image is never added to the page.
+ this.element.find('img').off('load');
+ this.element.html("");
+
+ // Notify others of changes.
+ this.element.trigger('changed');
+
+ this.outputs = [];
+ this.trusted = true;
+ this.unscroll_area();
+ return;
+ }
+ };
+
+
+ // JSON serialization
+
+ OutputArea.prototype.fromJSON = function (outputs, metadata) {
+ var len = outputs.length;
+ metadata = metadata || {};
+
+ for (var i=0; i<len; i++) {
+ this.append_output(outputs[i]);
+ }
+ if (metadata.collapsed !== undefined) {
+ if (metadata.collapsed) {
+ this.collapse();
+ } else {
+ this.expand();
+ }
+ }
+ if (metadata.scrolled !== undefined) {
+ this.scroll_state = metadata.scrolled;
+ if (metadata.scrolled) {
+ this.scroll_if_long();
+ } else {
+ this.unscroll_area();
+ }
+ }
+ };
+
+
+ OutputArea.prototype.toJSON = function () {
+ return this.outputs;
+ };
+
+ /**
+ * Class properties
+ **/
+
+ /**
+ * Threshold to trigger autoscroll when the OutputArea is resized,
+ * typically when new outputs are added.
+ *
+ * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
+ * unless it is < 0, in which case autoscroll will never be triggered
+ *
+ * @property auto_scroll_threshold
+ * @type Number
+ * @default 100
+ *
+ **/
+ OutputArea.auto_scroll_threshold = 100;
+
+ /**
+ * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
+ * shorter than this are never scrolled.
+ *
+ * @property minimum_scroll_threshold
+ * @type Number
+ * @default 20
+ *
+ **/
+ OutputArea.minimum_scroll_threshold = 20;
+
+
+ OutputArea.display_order = [
+ 'application/javascript',
+ 'text/html',
+ 'text/markdown',
+ 'text/latex',
+ 'image/svg+xml',
+ 'image/png',
+ 'image/jpeg',
+ 'application/pdf',
+ 'text/plain'
+ ];
+
+ OutputArea.append_map = {
+ "text/plain" : append_text,
+ "text/html" : append_html,
+ "text/markdown": append_markdown,
+ "image/svg+xml" : append_svg,
+ "image/png" : append_png,
+ "image/jpeg" : append_jpeg,
+ "text/latex" : append_latex,
+ "application/javascript" : append_javascript,
+ "application/pdf" : append_pdf
+ };
+
+ return {'OutputArea': OutputArea};
+});
diff --git a/notebook/static/notebook/js/pager.js b/notebook/static/notebook/js/pager.js
new file mode 100644
index 0000000..cb9bf32
--- /dev/null
+++ b/notebook/static/notebook/js/pager.js
@@ -0,0 +1,185 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery-ui',
+ 'base/js/utils',
+], function($, utils) {
+ "use strict";
+
+ var Pager = function (pager_selector, options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * pager_selector: string
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ */
+ this.events = options.events;
+ this.pager_element = $(pager_selector);
+ this.pager_button_area = $('#pager-button-area');
+ this._default_end_space = 100;
+ this.pager_element.resizable({handles: 'n', resize: $.proxy(this._resize, this)});
+ this.expanded = false;
+ this.create_button_area();
+ this.bind_events();
+ };
+
+ Pager.prototype.create_button_area = function(){
+ var that = this;
+ this.pager_button_area.append(
+ $('<a>').attr('role', "button")
+ .attr('title',"Open the pager in an external window")
+ .addClass('ui-button')
+ .click(function(){that.detach();})
+ .append(
+ $('<span>').addClass("ui-icon ui-icon-extlink")
+ )
+ );
+ this.pager_button_area.append(
+ $('<a>').attr('role', "button")
+ .attr('title',"Close the pager")
+ .addClass('ui-button')
+ .click(function(){that.collapse();})
+ .append(
+ $('<span>').addClass("ui-icon ui-icon-close")
+ )
+ );
+ };
+
+
+ Pager.prototype.bind_events = function () {
+ var that = this;
+
+ this.pager_element.bind('collapse_pager', function (event, extrap) {
+ // Animate hiding of the pager.
+ var time = (extrap && extrap.duration) ? extrap.duration : 'fast';
+ that.pager_element.animate({
+ height: 'toggle'
+ }, {
+ duration: time,
+ done: function() {
+ $('.end_space').css('height', that._default_end_space);
+ }
+ });
+ });
+
+ this.pager_element.bind('expand_pager', function (event, extrap) {
+ // Clear the pager's height attr if it's set. This allows the
+ // pager to size itself according to its contents.
+ that.pager_element.height('initial');
+
+ // Animate the showing of the pager
+ var time = (extrap && extrap.duration) ? extrap.duration : 'fast';
+ that.pager_element.show(time, function() {
+ // Explicitly set pager height once the pager has shown itself.
+ // This allows the pager-contents div to use percentage sizing.
+ that.pager_element.height(that.pager_element.height());
+ that._resize();
+
+ // HACK: Less horrible, but still horrible hack to force the
+ // pager to show it's scrollbars on FireFox. ipython/ipython/#8853
+ that.pager_element.css('position', 'relative');
+ window.requestAnimationFrame(function() { /* Wait one frame */
+ that.pager_element.css('position', '');
+ });
+ });
+ });
+
+ this.events.on('open_with_text.Pager', function (event, payload) {
+ // FIXME: support other mime types with generic mimebundle display
+ // mechanism
+ if (payload.data['text/html'] && payload.data['text/html'] !== "") {
+ that.clear();
+ that.expand();
+ that.append(payload.data['text/html']);
+ } else if (payload.data['text/plain'] && payload.data['text/plain'] !== "") {
+ that.clear();
+ that.expand();
+ that.append_text(payload.data['text/plain']);
+ }
+ });
+ };
+
+
+ Pager.prototype.collapse = function (extrap) {
+ if (this.expanded === true) {
+ this.expanded = false;
+ this.pager_element.trigger('collapse_pager', extrap);
+ }
+ };
+
+
+ Pager.prototype.expand = function (extrap) {
+ if (this.expanded !== true) {
+ this.expanded = true;
+ this.pager_element.trigger('expand_pager', extrap);
+ }
+ };
+
+
+ Pager.prototype.toggle = function () {
+ if (this.expanded === true) {
+ this.collapse();
+ } else {
+ this.expand();
+ }
+ };
+
+
+ Pager.prototype.clear = function (text) {
+ this.pager_element.find(".container").empty();
+ };
+
+ Pager.prototype.detach = function(){
+ var w = window.open("","_blank");
+ $(w.document.head)
+ .append(
+ $('<link>')
+ .attr('rel',"stylesheet")
+ .attr('href',"/static/css/notebook.css")
+ .attr('type',"text/css")
+ )
+ .append(
+ $('<title>').text("Jupyter Pager")
+ );
+ var pager_body = $(w.document.body);
+ pager_body.css('overflow','scroll');
+
+ pager_body.append(this.pager_element.clone().children());
+ w.document.close();
+ this.collapse();
+ };
+
+ Pager.prototype.append_text = function (text) {
+ /**
+ * The only user content injected with this HTML call is escaped by
+ * the fixConsole() method.
+ */
+ this.pager_element.find(".container").append($('<pre/>').html(utils.fixCarriageReturn(utils.fixConsole(text))));
+ };
+
+ Pager.prototype.append = function (htm) {
+ /**
+ * The only user content injected with this HTML call is escaped by
+ * the fixConsole() method.
+ */
+ this.pager_element.find(".container").append(htm);
+ };
+
+
+ Pager.prototype._resize = function() {
+ /**
+ * Update document based on pager size.
+ */
+
+ // Make sure the padding at the end of the notebook is large
+ // enough that the user can scroll to the bottom of the
+ // notebook.
+ $('.end_space').css('height', Math.max(this.pager_element.height(), this._default_end_space));
+ };
+
+ return {'Pager': Pager};
+});
diff --git a/notebook/static/notebook/js/quickhelp.js b/notebook/static/notebook/js/quickhelp.js
new file mode 100644
index 0000000..744aae6
--- /dev/null
+++ b/notebook/static/notebook/js/quickhelp.js
@@ -0,0 +1,306 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/dialog',
+], function($, utils, dialog) {
+ "use strict";
+ var platform = utils.platform;
+
+ var QuickHelp = function (options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * keyboard_manager: KeyboardManager instance
+ * notebook: Notebook instance
+ */
+ this.keyboard_manager = options.keyboard_manager;
+ this.notebook = options.notebook;
+ this.keyboard_manager.quick_help = this;
+ this.events = options.events;
+ };
+
+ var cmd_ctrl = 'Ctrl-';
+ var platform_specific;
+
+ if (platform === 'MacOS') {
+ // Mac OS X specific
+ cmd_ctrl = 'Cmd-';
+ platform_specific = [
+ { shortcut: "Cmd-Up", help:"go to cell start" },
+ { shortcut: "Cmd-Down", help:"go to cell end" },
+ { shortcut: "Alt-Left", help:"go one word left" },
+ { shortcut: "Alt-Right", help:"go one word right" },
+ { shortcut: "Alt-Backspace", help:"delete word before" },
+ { shortcut: "Alt-Delete", help:"delete word after" },
+ ];
+ } else {
+ // PC specific
+ platform_specific = [
+ { shortcut: "Ctrl-Home", help:"go to cell start" },
+ { shortcut: "Ctrl-Up", help:"go to cell start" },
+ { shortcut: "Ctrl-End", help:"go to cell end" },
+ { shortcut: "Ctrl-Down", help:"go to cell end" },
+ { shortcut: "Ctrl-Left", help:"go one word left" },
+ { shortcut: "Ctrl-Right", help:"go one word right" },
+ { shortcut: "Ctrl-Backspace", help:"delete word before" },
+ { shortcut: "Ctrl-Delete", help:"delete word after" },
+ ];
+ }
+
+ var cm_shortcuts = [
+ { shortcut:"Tab", help:"code completion or indent" },
+ { shortcut:"Shift-Tab", help:"tooltip" },
+ { shortcut: cmd_ctrl + "]", help:"indent" },
+ { shortcut: cmd_ctrl + "[", help:"dedent" },
+ { shortcut: cmd_ctrl + "a", help:"select all" },
+ { shortcut: cmd_ctrl + "z", help:"undo" },
+ { shortcut: cmd_ctrl + "Shift-z", help:"redo" },
+ { shortcut: cmd_ctrl + "y", help:"redo" },
+ ].concat( platform_specific );
+
+ var mac_humanize_map = {
+ // all these are unicode, will probably display badly on anything except macs.
+ // these are the standard symbol that are used in MacOS native menus
+ // cf http://apple.stackexchange.com/questions/55727/
+ // for htmlentities and/or unicode value
+ 'cmd':'⌘',
+ 'shift':'⇧',
+ 'alt':'⌥',
+ 'up':'↑',
+ 'down':'↓',
+ 'left':'←',
+ 'right':'→',
+ 'eject':'⏏',
+ 'tab':'⇥',
+ 'backtab':'⇤',
+ 'capslock':'⇪',
+ 'esc':'esc',
+ 'ctrl':'⌃',
+ 'enter':'↩',
+ 'pageup':'⇞',
+ 'pagedown':'⇟',
+ 'home':'↖',
+ 'end':'↘',
+ 'altenter':'⌤',
+ 'space':'␣',
+ 'delete':'⌦',
+ 'backspace':'⌫',
+ 'apple':'',
+ };
+
+ var default_humanize_map = {
+ 'shift':'Shift',
+ 'alt':'Alt',
+ 'up':'Up',
+ 'down':'Down',
+ 'left':'Left',
+ 'right':'Right',
+ 'tab':'Tab',
+ 'capslock':'Caps Lock',
+ 'esc':'Esc',
+ 'ctrl':'Ctrl',
+ 'enter':'Enter',
+ 'pageup':'Page Up',
+ 'pagedown':'Page Down',
+ 'home':'Home',
+ 'end':'End',
+ 'space':'Space',
+ 'backspace':'Backspace',
+ '-':'Minus'
+ };
+
+ var humanize_map;
+
+ if (platform === 'MacOS'){
+ humanize_map = mac_humanize_map;
+ } else {
+ humanize_map = default_humanize_map;
+ }
+
+ var special_case = { pageup: "PageUp", pagedown: "Page Down" };
+
+ function humanize_key(key){
+ if (key.length === 1){
+ return key.toUpperCase();
+ }
+
+ key = humanize_map[key.toLowerCase()]||key;
+
+ if (key.indexOf(',') === -1){
+ return ( special_case[key] ? special_case[key] : key.charAt(0).toUpperCase() + key.slice(1) );
+ }
+ }
+
+ // return an **html** string of the keyboard shortcut
+ // for human eyes consumption.
+ // the sequence is a string, comma sepparated linkt of shortcut,
+ // where the shortcut is a list of dash-joined keys.
+ // Each shortcut will be wrapped in <kbd> tag, and joined by comma is in a
+ // sequence.
+ //
+ // Depending on the platform each shortcut will be normalized, with or without dashes.
+ // and replace with the corresponding unicode symbol for modifier if necessary.
+ function humanize_sequence(sequence){
+ var joinchar = ',';
+ var hum = _.map(sequence.replace(/meta/g, 'cmd').split(','), humanize_shortcut).join(joinchar);
+ return hum;
+ }
+
+ function humanize_shortcut(shortcut){
+ var joinchar = '-';
+ if (platform === 'MacOS'){
+ joinchar = '';
+ }
+ var sh = _.map(shortcut.split('-'), humanize_key ).join(joinchar);
+ return '<kbd>'+sh+'</kbd>';
+ }
+
+
+ QuickHelp.prototype.show_keyboard_shortcuts = function () {
+ /**
+ * toggles display of keyboard shortcut dialog
+ */
+ var that = this;
+ if ( this.force_rebuild ) {
+ this.shortcut_dialog.remove();
+ delete(this.shortcut_dialog);
+ this.force_rebuild = false;
+ }
+ if ( this.shortcut_dialog ){
+ // if dialog is already shown, close it
+ $(this.shortcut_dialog).modal("toggle");
+ return;
+ }
+ var command_shortcuts = this.keyboard_manager.command_shortcuts.help();
+ var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help();
+ var help, shortcut;
+ var i, half, n;
+ var element = $('<div/>');
+
+ // The documentation
+ var doc = $('<div/>').addClass('alert alert-info');
+ doc.append(
+ 'The Jupyter Notebook has two different keyboard input modes. <b>Edit mode</b> '+
+ 'allows you to type code/text into a cell and is indicated by a green cell '+
+ 'border. <b>Command mode</b> binds the keyboard to notebook level actions '+
+ 'and is indicated by a grey cell border with a blue left margin.'
+ );
+ element.append(doc);
+ if (platform === 'MacOS') {
+ doc = $('<div/>').addClass('alert alert-info');
+ var key_div = this.build_key_names();
+ doc.append(key_div);
+ element.append(doc);
+ }
+
+ // Command mode
+ var cmd_div = this.build_command_help();
+ element.append(cmd_div);
+
+ // Edit mode
+ var edit_div = this.build_edit_help(cm_shortcuts);
+ element.append(edit_div);
+
+ this.shortcut_dialog = dialog.modal({
+ title : "Keyboard shortcuts",
+ body : element,
+ destroy : false,
+ buttons : {
+ Close : {}
+ },
+ notebook: this.notebook,
+ keyboard_manager: this.keyboard_manager,
+ });
+ this.shortcut_dialog.addClass("modal_stretch");
+
+ this.events.on('rebuild.QuickHelp', function() { that.force_rebuild = true;});
+ };
+
+ QuickHelp.prototype.build_key_names = function () {
+ var key_names_mac = [{ shortcut:"⌘", help:"Command" },
+ { shortcut:"⌃", help:"Control" },
+ { shortcut:"⌥", help:"Option" },
+ { shortcut:"⇧", help:"Shift" },
+ { shortcut:"↩", help:"Return" },
+ { shortcut:"␣", help:"Space" },
+ { shortcut:"⇥", help:"Tab" }];
+ var i, half, n;
+ var div = $('<div/>').append('Mac OS X modifier keys:');
+ var sub_div = $('<div/>').addClass('container-fluid');
+ var col1 = $('<div/>').addClass('col-md-6');
+ var col2 = $('<div/>').addClass('col-md-6');
+ n = key_names_mac.length;
+ half = ~~(n/2);
+ for (i=0; i<half; i++) { col1.append(
+ build_one(key_names_mac[i])
+ ); }
+ for (i=half; i<n; i++) { col2.append(
+ build_one(key_names_mac[i])
+ ); }
+ sub_div.append(col1).append(col2);
+ div.append(sub_div);
+ return div;
+ };
+
+
+ QuickHelp.prototype.build_command_help = function () {
+ var command_shortcuts = this.keyboard_manager.command_shortcuts.help();
+ return build_div('<h4>Command Mode (press <kbd>Esc</kbd> to enable)</h4>', command_shortcuts);
+ };
+
+
+ QuickHelp.prototype.build_edit_help = function (cm_shortcuts) {
+ var edit_shortcuts = this.keyboard_manager.edit_shortcuts.help();
+ edit_shortcuts = jQuery.merge(jQuery.merge([], cm_shortcuts), edit_shortcuts);
+ return build_div('<h4>Edit Mode (press <kbd>Enter</kbd> to enable)</h4>', edit_shortcuts);
+ };
+
+ var build_one = function (s) {
+ var help = s.help;
+ var shortcut = '';
+ if(s.shortcut){
+ shortcut = humanize_sequence(s.shortcut);
+ }
+ return $('<div>').addClass('quickhelp').
+ append($('<span/>').addClass('shortcut_key').append($(shortcut))).
+ append($('<span/>').addClass('shortcut_descr').text(' : ' + help));
+
+ };
+
+ var build_div = function (title, shortcuts) {
+
+ // Remove jupyter-notebook:ignore shortcuts.
+ shortcuts = shortcuts.filter(function(shortcut) {
+ if (shortcut.help === 'ignore') {
+ return false;
+ } else {
+ return true;
+ }
+ });
+
+ var i, half, n;
+ var div = $('<div/>').append($(title));
+ var sub_div = $('<div/>').addClass('container-fluid');
+ var col1 = $('<div/>').addClass('col-md-6');
+ var col2 = $('<div/>').addClass('col-md-6');
+ n = shortcuts.length;
+ half = ~~(n/2); // Truncate :)
+ for (i=0; i<half; i++) { col1.append( build_one(shortcuts[i]) ); }
+ for (i=half; i<n; i++) { col2.append( build_one(shortcuts[i]) ); }
+ sub_div.append(col1).append(col2);
+ div.append(sub_div);
+ return div;
+ };
+
+ return {'QuickHelp': QuickHelp,
+ humanize_shortcut: humanize_shortcut,
+ humanize_sequence: humanize_sequence
+ };
+});
diff --git a/notebook/static/notebook/js/savewidget.js b/notebook/static/notebook/js/savewidget.js
new file mode 100644
index 0000000..8a18c58
--- /dev/null
+++ b/notebook/static/notebook/js/savewidget.js
@@ -0,0 +1,221 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/dialog',
+ 'base/js/keyboard',
+ 'moment',
+], function($, utils, dialog, keyboard, moment) {
+ "use strict";
+
+ var SaveWidget = function (selector, options) {
+ /**
+ * TODO: Remove circular ref.
+ */
+ this.notebook = undefined;
+ this.selector = selector;
+ this.events = options.events;
+ this._checkpoint_date = undefined;
+ this.keyboard_manager = options.keyboard_manager;
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.bind_events();
+ }
+ };
+
+
+ SaveWidget.prototype.bind_events = function () {
+ var that = this;
+ this.element.find('span.filename').click(function () {
+ that.rename_notebook({notebook: that.notebook});
+ });
+ this.events.on('notebook_loaded.Notebook', function () {
+ that.update_notebook_name();
+ that.update_document_title();
+ });
+ this.events.on('notebook_saved.Notebook', function () {
+ that.update_notebook_name();
+ that.update_document_title();
+ });
+ this.events.on('notebook_renamed.Notebook', function () {
+ that.update_notebook_name();
+ that.update_document_title();
+ that.update_address_bar();
+ });
+ this.events.on('notebook_save_failed.Notebook', function () {
+ that.set_save_status('Autosave Failed!');
+ });
+ this.events.on('notebook_read_only.Notebook', function () {
+ that.set_save_status('(read only)');
+ // disable future set_save_status
+ that.set_save_status = function () {};
+ });
+ this.events.on('checkpoints_listed.Notebook', function (event, data) {
+ that._set_last_checkpoint(data[0]);
+ });
+
+ this.events.on('checkpoint_created.Notebook', function (event, data) {
+ that._set_last_checkpoint(data);
+ });
+ this.events.on('set_dirty.Notebook', function (event, data) {
+ that.set_autosaved(data.value);
+ });
+ };
+
+
+ SaveWidget.prototype.rename_notebook = function (options) {
+ options = options || {};
+ var that = this;
+ var dialog_body = $('<div/>').append(
+ $("<p/>").addClass("rename-message")
+ .text('Enter a new notebook name:')
+ ).append(
+ $("<br/>")
+ ).append(
+ $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
+ .val(options.notebook.get_notebook_name())
+ );
+ var d = dialog.modal({
+ title: "Rename Notebook",
+ body: dialog_body,
+ notebook: options.notebook,
+ keyboard_manager: this.keyboard_manager,
+ buttons : {
+ "OK": {
+ class: "btn-primary",
+ click: function () {
+ var new_name = d.find('input').val();
+ if (!options.notebook.test_notebook_name(new_name)) {
+ d.find('.rename-message').text(
+ "Invalid notebook name. Notebook names must "+
+ "have 1 or more characters and can contain any characters " +
+ "except :/\\. Please enter a new notebook name:"
+ );
+ return false;
+ } else {
+ d.find('.rename-message').text("Renaming...");
+ d.find('input[type="text"]').prop('disabled', true);
+ that.notebook.rename(new_name).then(
+ function () {
+ d.modal('hide');
+ }, function (error) {
+ d.find('.rename-message').text(error.message || 'Unknown error');
+ d.find('input[type="text"]').prop('disabled', false).focus().select();
+ }
+ );
+ return false;
+ }
+ }
+ },
+ "Cancel": {}
+ },
+ open : function () {
+ /**
+ * Upon ENTER, click the OK button.
+ */
+ d.find('input[type="text"]').keydown(function (event) {
+ if (event.which === keyboard.keycodes.enter) {
+ d.find('.btn-primary').first().click();
+ return false;
+ }
+ });
+ d.find('input[type="text"]').focus().select();
+ }
+ });
+ };
+
+
+ SaveWidget.prototype.update_notebook_name = function () {
+ var nbname = this.notebook.get_notebook_name();
+ this.element.find('span.filename').text(nbname);
+ };
+
+
+ SaveWidget.prototype.update_document_title = function () {
+ var nbname = this.notebook.get_notebook_name();
+ document.title = nbname;
+ };
+
+ SaveWidget.prototype.update_address_bar = function(){
+ var base_url = this.notebook.base_url;
+ var path = this.notebook.notebook_path;
+ var state = {path : path};
+ window.history.replaceState(state, "", utils.url_path_join(
+ base_url,
+ "notebooks",
+ utils.encode_uri_components(path))
+ );
+ };
+
+
+ SaveWidget.prototype.set_save_status = function (msg) {
+ this.element.find('span.autosave_status').text(msg);
+ };
+
+ SaveWidget.prototype._set_last_checkpoint = function (checkpoint) {
+ if (checkpoint) {
+ this._checkpoint_date = new Date(checkpoint.last_modified);
+ } else {
+ this._checkpoint_date = null;
+ }
+ this._render_checkpoint();
+ };
+
+ SaveWidget.prototype._render_checkpoint = function () {
+ /** actually set the text in the element, from our _checkpoint value
+
+ called directly, and periodically in timeouts.
+ */
+ this._schedule_render_checkpoint();
+ var el = this.element.find('span.checkpoint_status');
+ if (!this._checkpoint_date) {
+ el.text('').attr('title', 'no checkpoint');
+ return;
+ }
+ var chkd = moment(this._checkpoint_date);
+ var long_date = chkd.format('llll');
+ var human_date;
+ var tdelta = Math.ceil(new Date() - this._checkpoint_date);
+ if (tdelta < utils.time.milliseconds.d){
+ // less than 24 hours old, use relative date
+ human_date = chkd.fromNow();
+ } else {
+ // otherwise show calendar
+ // <Today | yesterday|...> at hh,mm,ss
+ human_date = chkd.calendar();
+ }
+ el.text('Last Checkpoint: ' + human_date).attr('title', long_date);
+ };
+
+
+ SaveWidget.prototype._schedule_render_checkpoint = function () {
+ /** schedule the next update to relative date
+
+ periodically updated, so short values like 'a few seconds ago' don't get stale.
+ */
+ if (!this._checkpoint_date) {
+ return;
+ }
+ if ((this._checkpoint_timeout)) {
+ clearTimeout(this._checkpoint_timeout);
+ }
+ var dt = Math.ceil(new Date() - this._checkpoint_date);
+ this._checkpoint_timeout = setTimeout(
+ $.proxy(this._render_checkpoint, this),
+ utils.time.timeout_from_dt(dt)
+ );
+ };
+
+ SaveWidget.prototype.set_autosaved = function (dirty) {
+ if (dirty) {
+ this.set_save_status("(unsaved changes)");
+ } else {
+ this.set_save_status("(autosaved)");
+ }
+ };
+
+ return {'SaveWidget': SaveWidget};
+
+});
diff --git a/notebook/static/notebook/js/scrollmanager.js b/notebook/static/notebook/js/scrollmanager.js
new file mode 100644
index 0000000..89144cc
--- /dev/null
+++ b/notebook/static/notebook/js/scrollmanager.js
@@ -0,0 +1,232 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+define(['jquery'], function($){
+ "use strict";
+
+ var ScrollManager = function(notebook, options) {
+ /**
+ * Public constructor.
+ */
+ this.notebook = notebook;
+ this.element = $('#site');
+ options = options || {};
+ this.animation_speed = options.animation_speed || 250; //ms
+ };
+
+ ScrollManager.prototype.onScroll = function (func, rate) {
+ /**
+ * Register a function to be called when the page is scrolled, throttled
+ * at a particular rate limit.
+ */
+ rate = rate || 100; // default rate limit
+ this.element.scroll(function () {
+ clearTimeout(func._timeout);
+ func._timeout = setTimeout(func, rate);
+ });
+ };
+
+ ScrollManager.prototype.scroll = function (delta) {
+ /**
+ * Scroll the document.
+ *
+ * Parameters
+ * ----------
+ * delta: integer
+ * direction to scroll the document. Positive is downwards.
+ * Unit is one page length.
+ */
+ this.scroll_some(delta);
+ return false;
+ };
+
+ ScrollManager.prototype.scroll_to = function(selector) {
+ /**
+ * Scroll to an element in the notebook.
+ */
+ this.element.animate({'scrollTop': $(selector).offset().top + this.element.scrollTop() - this.element.offset().top}, this.animation_speed);
+ };
+
+ ScrollManager.prototype.scroll_some = function(pages) {
+ /**
+ * Scroll up or down a given number of pages.
+ *
+ * Parameters
+ * ----------
+ * pages: integer
+ * number of pages to scroll the document, may be positive or negative.
+ */
+ this.element.animate({'scrollTop': this.element.scrollTop() + pages * this.element.height()}, this.animation_speed);
+ };
+
+ ScrollManager.prototype.get_first_visible_cell = function() {
+ /**
+ * Gets the index of the first visible cell in the document.
+ *
+ * First, attempt to be smart by guessing the index of the cell we are
+ * scrolled to. Then, walk from there up or down until the right cell
+ * is found. To guess the index, get the top of the last cell, and
+ * divide that by the number of cells to get an average cell height.
+ * Then divide the scroll height by the average cell height.
+ */
+ var cell_count = this.notebook.ncells();
+ var first_cell_top = this.notebook.get_cell(0).element.offset().top;
+ var last_cell_top = this.notebook.get_cell(cell_count-1).element.offset().top;
+ var avg_cell_height = (last_cell_top - first_cell_top) / cell_count;
+ var i = Math.ceil(this.element.scrollTop() / avg_cell_height);
+ i = Math.min(Math.max(i , 0), cell_count - 1);
+
+ while (this.notebook.get_cell(i).element.offset().top - first_cell_top < this.element.scrollTop() && i < cell_count - 1) {
+ i += 1;
+ }
+
+ while (this.notebook.get_cell(i).element.offset().top - first_cell_top > this.element.scrollTop() - 50 && i >= 0) {
+ i -= 1;
+ }
+ return Math.min(i + 1, cell_count - 1);
+ };
+
+ ScrollManager.prototype.is_cell_visible = function (cell) {
+ var cell_rect = cell.element[0].getBoundingClientRect();
+ var scroll_rect = this.element[0].getBoundingClientRect();
+ return ((cell_rect.top <= scroll_rect.bottom) && (cell_rect.bottom >= scroll_rect.top));
+ };
+
+
+ var TargetScrollManager = function(notebook, options) {
+ /**
+ * Public constructor.
+ */
+ ScrollManager.apply(this, [notebook, options]);
+ };
+ TargetScrollManager.prototype = Object.create(ScrollManager.prototype);
+
+ TargetScrollManager.prototype.is_target = function (index) {
+ /**
+ * Check if a cell should be a scroll stop.
+ *
+ * Returns `true` if the cell is a cell that the scroll manager
+ * should scroll to. Otherwise, false is returned.
+ *
+ * Parameters
+ * ----------
+ * index: integer
+ * index of the cell to test.
+ */
+ return false;
+ };
+
+ TargetScrollManager.prototype.scroll = function (delta) {
+ /**
+ * Scroll the document.
+ *
+ * Parameters
+ * ----------
+ * delta: integer
+ * direction to scroll the document. Positive is downwards.
+ * Units are targets.
+ *
+ * Try to scroll to the next slide.
+ */
+ var cell_count = this.notebook.ncells();
+ var selected_index = this.get_first_visible_cell() + delta;
+ while (0 <= selected_index && selected_index < cell_count && !this.is_target(selected_index)) {
+ selected_index += delta;
+ }
+
+ if (selected_index < 0 || cell_count <= selected_index) {
+ return ScrollManager.prototype.scroll.apply(this, [delta]);
+ } else {
+ this.scroll_to(this.notebook.get_cell(selected_index).element);
+
+ // Cancel browser keyboard scroll.
+ return false;
+ }
+ };
+
+
+ var SlideScrollManager = function(notebook, options) {
+ /**
+ * Public constructor.
+ */
+ TargetScrollManager.apply(this, [notebook, options]);
+ };
+ SlideScrollManager.prototype = Object.create(TargetScrollManager.prototype);
+
+ SlideScrollManager.prototype.is_target = function (index) {
+ var cell = this.notebook.get_cell(index);
+ return cell.metadata && cell.metadata.slideshow &&
+ cell.metadata.slideshow.slide_type &&
+ (cell.metadata.slideshow.slide_type === "slide" ||
+ cell.metadata.slideshow.slide_type === "subslide");
+ };
+
+
+ var HeadingScrollManager = function(notebook, options) {
+ /**
+ * Public constructor.
+ */
+ ScrollManager.apply(this, [notebook, options]);
+ options = options || {};
+ this._level = options.heading_level || 1;
+ };
+ HeadingScrollManager.prototype = Object.create(ScrollManager.prototype);
+
+ HeadingScrollManager.prototype.scroll = function (delta) {
+ /**
+ * Scroll the document.
+ *
+ * Parameters
+ * ----------
+ * delta: integer
+ * direction to scroll the document. Positive is downwards.
+ * Units are headers.
+ *
+ * Get all of the header elements that match the heading level or are of
+ * greater magnitude (a smaller header number).
+ */
+ var headers = $();
+ var i;
+ for (i = 1; i <= this._level; i++) {
+ headers = headers.add('#notebook-container h' + i);
+ }
+
+ // Find the header the user is on or below.
+ var first_cell_top = this.notebook.get_cell(0).element.offset().top;
+ var current_scroll = this.element.scrollTop();
+ var header_scroll = 0;
+ i = -1;
+ while (current_scroll >= header_scroll && i < headers.length) {
+ if (++i < headers.length) {
+ header_scroll = $(headers[i]).offset().top - first_cell_top;
+ }
+ }
+ i--;
+
+ // Check if the user is below the header.
+ if (i < 0 || current_scroll > $(headers[i]).offset().top - first_cell_top + 30) {
+ // Below the header, count the header as a target.
+ if (delta < 0) {
+ delta += 1;
+ }
+ }
+ i += delta;
+
+ // Scroll!
+ if (0 <= i && i < headers.length) {
+ this.scroll_to(headers[i]);
+ return false;
+ } else {
+ // Default to the base's scroll behavior when target header doesn't
+ // exist.
+ return ScrollManager.prototype.scroll.apply(this, [delta]);
+ }
+ };
+
+ // Return naemspace for require.js loads
+ return {
+ 'ScrollManager': ScrollManager,
+ 'SlideScrollManager': SlideScrollManager,
+ 'HeadingScrollManager': HeadingScrollManager,
+ 'TargetScrollManager': TargetScrollManager
+ };
+});
diff --git a/notebook/static/notebook/js/searchandreplace.js b/notebook/static/notebook/js/searchandreplace.js
new file mode 100644
index 0000000..5e0d373
--- /dev/null
+++ b/notebook/static/notebook/js/searchandreplace.js
@@ -0,0 +1,384 @@
+define(function(require){
+ "use strict";
+
+ var dialog = require('base/js/dialog');
+
+ /**
+ * escape a Regular expression to act as a pure search string.
+ * though it will still have the case sensitivity options and all
+ * the benefits
+ **/
+ function escapeRegExp(string){
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ }
+
+ /**
+ * Compute the model of the preview for the search and replace.
+ * It might not be perfectly accurate if matches overlap...
+ * Parameter:
+ * sre: the string that will become the Search Regular Expression
+ * arr: a list of string on which the match will be applied.
+ * isCaseSensitive: should the match be CaseSensitive
+ * RegExOrNot: a `RegExOrNot` object.
+ * replace: the replacement string for the matching `sre`
+ * Return: a tuple of 2 value:
+ * 1) array of [before match, match, replacement, after match]
+ * where before and after match are cut to a reasonable length after the match.
+ * 2) Boolean, whether the matching has been aborted because one of the element of
+ * arr have too many matches.
+ **/
+ var compute_preview_model = function(sre, arr, isCaseSensitive, RegExpOrNot, replace){
+ var html = [];
+ // and create an array of
+ // before_match, match , replacement, after_match
+ var aborted = false;
+ var replacer_reg = new RegExpOrNot(sre);
+ for(var r=0; r < arr.length; r++){
+ var current_line = arr[r];
+ var match_abort = getMatches(sre, current_line, isCaseSensitive, RegExpOrNot);
+ aborted = aborted || match_abort[1];
+ var matches = match_abort[0];
+ for(var mindex=0; mindex < matches.length ; mindex++){
+ var start = matches[mindex][0];
+ var stop = matches[mindex][1];
+ var initial = current_line.slice(start, stop);
+ var replaced = initial.replace(replacer_reg, replace);
+ // that might be better as a dictionary
+ html.push([cutBefore(current_line.slice(0, start)),
+ initial,
+ replaced,
+ cutAfter(current_line.slice(stop), 30-(stop-start))]);
+ }
+ }
+ return [html, aborted];
+ };
+
+ /**
+ * Build the preview model where things matched and their replacement values
+ * are wrapped in tags with correct CSS classes.
+ * Parameter:
+ * body: jQuery element into which the preview will be build
+ * aborted : have the model been aborted (Boolean) use to tell the user
+ * that the preview might not show all the replacements
+ * html: array of model created by compute_preview_model
+ * replace: Boolean: whether we are actually replacing with something or just matching.
+ **/
+ var build_preview = function(body, aborted, html, replace){
+ body.empty();
+ if(aborted){
+ body.append($('<p/>').addClass('bg-warning').text("Warning, too many matches ("+html.length+"+), some changes might not be shown or applied"));
+ } else {
+ body.append($('<p/>').text(html.length+" match"+(html.length==1?'':'es')));
+ }
+ for(var rindex=0; rindex<html.length; rindex++){
+ var pre = $('<pre/>')
+ .append(html[rindex][0])
+ .append($('<span/>').addClass('match').text(html[rindex][1]));
+ if(replace){
+ pre.append($('<span/>').addClass('insert').text(html[rindex][2]));
+ pre.addClass('replace');
+ }
+ pre.append(html[rindex][3]);
+ body.append(pre);
+ }
+ };
+
+ /**
+ * Given a string, return only the beginning, with potentially an ellipsis
+ * at the end.
+ **/
+ var cutAfter = function(string, n){
+ n=n||10;
+ while(n<10){
+ n+=15;
+ }
+ if(string.length > n+3){
+ return string.slice(0, n)+'...';
+ }
+ return string;
+ };
+
+ /**
+ * Given a string, return only the end, with potentially an ellipsis
+ * at the beginning.
+ **/
+ var cutBefore = function(string){
+ if(string.length > 33){
+ return '...'+string.slice(-30);
+ }
+ return string;
+ };
+
+ /**
+ * Find all occurrences of `re` in `string`, match in a `caseSensitive`
+ * manner or not, and determine whether `re` is a RegExp or not depending of
+ * the type of object passed as `r`.
+ *
+ * Return a tuple
+ * 1) list of matches [start, stop] indexes in the string.
+ * 2) abort Boolean, if more that 100 matches and the matches were aborted.
+ **/
+ var getMatches = function(re, string, caseSensitive, r){
+ var extra = caseSensitive ? '':'i';
+ extra = '';
+ try {
+ re = r(re, 'g'+extra);// have to global or infinite loop
+ } catch (e){
+ return [[], false];
+ }
+ var res = [];
+ var match;
+ var escape_hatch = 0;
+ var abort = false;
+ while((match = re.exec(string)) !== null) {
+ res.push([match.index, match.index+match[0].length]);
+ escape_hatch++;
+ if(escape_hatch > 100){
+ console.warn("More than 100 matches, aborting");
+ abort = true;
+ break;
+ }
+ }
+ return [res, abort];
+ };
+
+ // main function
+ /**
+ * Search N' Replace action handler.
+ **/
+ var snr = function(env, event) {
+
+ var isRegExpButton = $('<button/>')
+ .attr('type', 'button')
+ .attr('id', 'isreg')
+ .addClass("btn btn-default btn-sm")
+ .attr('data-toggle','button')
+ .css('font-weight', 'bold')
+ .attr('title', 'Use regex (JavaScript regex syntax)')
+ .text('.*');
+
+ var onlySelectedButton = $('<button/>')
+ .append($('<i/>').addClass('fa fa-align-left'))
+ .attr('type', 'button')
+ .addClass("btn btn-default btn-sm")
+ .attr('data-toggle','button')
+ .attr('title', 'Replace in selected cells');
+
+ var isCaseSensitiveButton = $('<button/>')
+ .attr('type', 'button')
+ .addClass("btn btn-default btn-sm")
+ .attr('data-toggle','button')
+ .attr('tabindex', '0')
+ .attr('title', 'Match case')
+ .css('font-weight', 'bold')
+ .text('Aa');
+
+ var search = $("<input/>")
+ .addClass('form-control input-sm')
+ .attr('placeholder','Find');
+
+ var findFormGroup = $('<div/>').addClass('form-group');
+ findFormGroup.append(
+ $('<div/>').addClass('input-group')
+ .append(
+ $('<div/>').addClass('input-group-btn')
+ .append(isCaseSensitiveButton)
+ .append(isRegExpButton)
+ .append(onlySelectedButton)
+ )
+ .append(search)
+ )
+
+ var replace = $("<input/>")
+ .addClass('form-control input-sm')
+ .attr('placeholder','Replace');
+ var replaceFormGroup = $('<div/>').addClass('form-group');
+ replaceFormGroup.append(replace);
+
+ var body = $('<div/>').attr('id', 'replace-preview');
+
+ var form = $('<form/>').attr('id', 'find-and-replace')
+ form.append(findFormGroup);
+ form.append(replaceFormGroup);
+ form.append(body);
+
+ // return whether the search is case sensitive
+ var isCaseSensitive = function(){
+ var value = isCaseSensitiveButton.attr('aria-pressed') == 'true';
+ return value;
+ };
+
+ // return whether the search is RegExp based, or
+ // plain string matching.
+ var isReg = function(){
+ var value = isRegExpButton.attr('aria-pressed') == 'true';
+ return value;
+ };
+
+ var onlySelected = function(){
+ return (onlySelectedButton.attr('aria-pressed') == 'true');
+ };
+
+
+ // return a Pseudo RegExp object that acts
+ // either as a plain RegExp Object, or as a pure string matching.
+ // automatically set the flags for case sensitivity from the UI
+ var RegExpOrNot = function(str, flags){
+ if (!isCaseSensitive()){
+ flags = (flags || '')+'i';
+ }
+ if (isRegExpButton.attr('aria-pressed') === 'true'){
+ return new RegExp(str, flags);
+ } else {
+ return new RegExp(escapeRegExp(str), flags);
+ }
+ };
+
+
+ var onError = function(body){
+ body.empty();
+ body.append($('<p/>').text('No matches, invalid or empty regular expression'));
+ };
+
+ var get_cells = function(env){
+ if(onlySelected()){
+ return env.notebook.get_selected_cells();
+ } else {
+ return env.notebook.get_cells();
+ }
+ };
+
+ var get_all_text = function(cells) {
+ var arr = [];
+ for (var c = 0; c < cells.length; c++) {
+ arr = arr.concat(cells[c].code_mirror.getValue().split('\n'));
+ }
+ return arr;
+ };
+ /**
+ * callback triggered anytime a change is made to the
+ * request, case sensitivity, isregex, search or replace
+ * modification.
+ **/
+ var onChange = function(){
+
+ var sre = search.val();
+ // abort on invalid RE
+ if (!sre) {
+ return onError(body);
+ }
+ try {
+ new RegExpOrNot(sre);
+ } catch (e) {
+ return onError(body);
+ }
+
+ // might want to warn if replace is empty
+ var replaceValue = replace.val();
+ var lines = get_all_text(get_cells(env));
+
+ var _hb = compute_preview_model(sre, lines, isCaseSensitive(), RegExpOrNot, replaceValue);
+ var html = _hb[0];
+ var aborted = _hb[1];
+
+ build_preview(body, aborted, html, replaceValue);
+
+ // done on type return false not to submit form
+ return false;
+ };
+
+ var onsubmit = function(event) {
+ var sre = search.val();
+ var replaceValue = replace.val();
+ if (!sre) {
+ return false;
+ }
+ // should abort on invalid RegExp.
+
+ // need to be multi line if we want to directly replace in codemirror.
+ // or need to split/replace/join
+ var reg = RegExpOrNot(sre, 'gm');
+ var cells = get_cells(env);
+ for (var c = 0; c < cells.length; c++) {
+ var cell = cells[c];
+ var oldvalue = cell.code_mirror.getValue();
+ var newvalue = oldvalue.replace(reg , replaceValue);
+ cell.code_mirror.setValue(newvalue);
+ if (cell.cell_type === 'markdown') {
+ cell.rendered = false;
+ cell.render();
+ }
+ }
+ };
+
+ // wire-up the UI
+
+ isRegExpButton.click(function(){
+ search.focus();
+ setTimeout(function(){onChange();}, 100);
+ });
+
+ isCaseSensitiveButton.click(function(){
+ search.focus();
+ setTimeout(function(){onChange();}, 100);
+ });
+
+ onlySelectedButton.click(function(){
+ replace.focus();
+ setTimeout(function(){onChange();}, 100);
+ });
+
+
+ search.keypress(function (e) {
+ if (e.which == 13) {//enter
+ replace.focus();
+ }
+ });
+
+ search.on('input', onChange);
+ replace.on('input', onChange);
+
+
+ var mod = dialog.modal({
+ show: false,
+ title: "Find and Replace",
+ body:form,
+ keyboard_manager: env.notebook.keyboard_manager,
+ buttons:{
+ 'Replace All':{ class: "btn-primary",
+ click: function(event){onsubmit(event); return true;}
+ }
+ },
+ open: function(){
+ search.focus();
+ }
+ });
+
+ replace.keypress(function (e) {
+ if (e.which == 13) {//enter
+ onsubmit();
+ mod.modal('hide');
+ }
+ });
+ mod.modal('show');
+ };
+
+
+ var load = function(keyboard_manager){
+ var action_all = {
+ help: 'find and replace',
+ handler: function(env, event){
+ snr(env, event);
+ }
+ };
+
+ var act_all = keyboard_manager.actions.register(action_all, 'find-and-replace', 'jupyter-notebook');
+
+ keyboard_manager.command_shortcuts.add_shortcuts({
+ 'f': 'jupyter-notebook:find-and-replace'
+ });
+ };
+
+
+ return {load:load};
+});
diff --git a/notebook/static/notebook/js/textcell.js b/notebook/static/notebook/js/textcell.js
new file mode 100644
index 0000000..d955630
--- /dev/null
+++ b/notebook/static/notebook/js/textcell.js
@@ -0,0 +1,383 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'base/js/utils',
+ 'jquery',
+ 'notebook/js/cell',
+ 'base/js/security',
+ 'services/config',
+ 'notebook/js/mathjaxutils',
+ 'notebook/js/celltoolbar',
+ 'components/marked/lib/marked',
+ 'codemirror/lib/codemirror',
+ 'codemirror/mode/gfm/gfm',
+ 'notebook/js/codemirror-ipythongfm'
+], function(
+ utils,
+ $,
+ cell,
+ security,
+ configmod,
+ mathjaxutils,
+ celltoolbar,
+ marked,
+ CodeMirror,
+ gfm,
+ ipgfm
+ ) {
+ "use strict";
+ var Cell = cell.Cell;
+
+ var TextCell = function (options) {
+ /**
+ * Constructor
+ *
+ * Construct a new TextCell, codemirror mode is by default 'htmlmixed',
+ * and cell type is 'text' cell start as not redered.
+ *
+ * Parameters:
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * config: dictionary
+ * keyboard_manager: KeyboardManager instance
+ * notebook: Notebook instance
+ */
+ options = options || {};
+
+ // in all TextCell/Cell subclasses
+ // do not assign most of members here, just pass it down
+ // in the options dict potentially overwriting what you wish.
+ // they will be assigned in the base class.
+ this.notebook = options.notebook;
+ this.events = options.events;
+ this.config = options.config;
+
+ // we cannot put this as a class key as it has handle to "this".
+ var config = utils.mergeopt(TextCell, this.config);
+ Cell.apply(this, [{
+ config: config,
+ keyboard_manager: options.keyboard_manager,
+ events: this.events}]);
+
+ this.cell_type = this.cell_type || 'text';
+ mathjaxutils = mathjaxutils;
+ this.rendered = false;
+ };
+
+ TextCell.prototype = Object.create(Cell.prototype);
+
+ TextCell.options_default = {
+ cm_config : {
+ extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
+ mode: 'htmlmixed',
+ lineWrapping : true,
+ }
+ };
+
+
+ /**
+ * Create the DOM element of the TextCell
+ * @method create_element
+ * @private
+ */
+ TextCell.prototype.create_element = function () {
+ Cell.prototype.create_element.apply(this, arguments);
+ var that = this;
+
+ var cell = $("<div>").addClass('cell text_cell');
+ cell.attr('tabindex','2');
+
+ var prompt = $('<div/>').addClass('prompt input_prompt');
+ cell.append(prompt);
+ var inner_cell = $('<div/>').addClass('inner_cell');
+ this.celltoolbar = new celltoolbar.CellToolbar({
+ cell: this,
+ notebook: this.notebook});
+ inner_cell.append(this.celltoolbar.element);
+ var input_area = $('<div/>').addClass('input_area');
+ this.code_mirror = new CodeMirror(input_area.get(0), this._options.cm_config);
+ // In case of bugs that put the keyboard manager into an inconsistent state,
+ // ensure KM is enabled when CodeMirror is focused:
+ this.code_mirror.on('focus', function () {
+ if (that.keyboard_manager) {
+ that.keyboard_manager.enable();
+ }
+ });
+ this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
+ // The tabindex=-1 makes this div focusable.
+ var render_area = $('<div/>').addClass('text_cell_render rendered_html')
+ .attr('tabindex','-1');
+ inner_cell.append(input_area).append(render_area);
+ cell.append(inner_cell);
+ this.element = cell;
+ };
+
+
+ // Cell level actions
+
+ TextCell.prototype.select = function () {
+ var cont = Cell.prototype.select.apply(this, arguments);
+ if (cont) {
+ if (this.mode === 'edit') {
+ this.code_mirror.refresh();
+ }
+ }
+ return cont;
+ };
+
+ TextCell.prototype.unrender = function () {
+ var cont = Cell.prototype.unrender.apply(this);
+ if (cont) {
+ var text_cell = this.element;
+ if (this.get_text() === this.placeholder) {
+ this.set_text('');
+ }
+ this.refresh();
+ }
+ return cont;
+ };
+
+ TextCell.prototype.execute = function () {
+ this.render();
+ };
+
+ /**
+ * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
+ * @method get_text
+ * @retrun {string} CodeMirror current text value
+ */
+ TextCell.prototype.get_text = function() {
+ return this.code_mirror.getValue();
+ };
+
+ /**
+ * @param {string} text - Codemiror text value
+ * @see TextCell#get_text
+ * @method set_text
+ * */
+ TextCell.prototype.set_text = function(text) {
+ this.code_mirror.setValue(text);
+ this.unrender();
+ this.code_mirror.refresh();
+ };
+
+ /**
+ * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
+ * @method get_rendered
+ * */
+ TextCell.prototype.get_rendered = function() {
+ return this.element.find('div.text_cell_render').html();
+ };
+
+ /**
+ * @method set_rendered
+ */
+ TextCell.prototype.set_rendered = function(text) {
+ this.element.find('div.text_cell_render').html(text);
+ };
+
+
+ /**
+ * Create Text cell from JSON
+ * @param {json} data - JSON serialized text-cell
+ * @method fromJSON
+ */
+ TextCell.prototype.fromJSON = function (data) {
+ Cell.prototype.fromJSON.apply(this, arguments);
+ if (data.cell_type === this.cell_type) {
+ if (data.source !== undefined) {
+ this.set_text(data.source);
+ // make this value the starting point, so that we can only undo
+ // to this state, instead of a blank cell
+ this.code_mirror.clearHistory();
+ // TODO: This HTML needs to be treated as potentially dangerous
+ // user input and should be handled before set_rendered.
+ this.set_rendered(data.rendered || '');
+ this.rendered = false;
+ this.render();
+ }
+ }
+ };
+
+ /** Generate JSON from cell
+ * @return {object} cell data serialised to json
+ */
+ TextCell.prototype.toJSON = function () {
+ var data = Cell.prototype.toJSON.apply(this);
+ data.source = this.get_text();
+ if (data.source == this.placeholder) {
+ data.source = "";
+ }
+ return data;
+ };
+
+
+ var MarkdownCell = function (options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * config: ConfigSection instance
+ * keyboard_manager: KeyboardManager instance
+ * notebook: Notebook instance
+ */
+ options = options || {};
+ var config = utils.mergeopt(MarkdownCell, {});
+ this.class_config = new configmod.ConfigWithDefaults(options.config,
+ {}, 'MarkdownCell');
+ TextCell.apply(this, [$.extend({}, options, {config: config})]);
+
+ this.cell_type = 'markdown';
+ };
+
+ MarkdownCell.options_default = {
+ cm_config: {
+ mode: 'ipythongfm'
+ },
+ placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
+ };
+
+ MarkdownCell.prototype = Object.create(TextCell.prototype);
+
+ MarkdownCell.prototype.set_heading_level = function (level) {
+ /**
+ * make a markdown cell a heading
+ */
+ level = level || 1;
+ var source = this.get_text();
+ source = source.replace(/^(#*)\s?/,
+ new Array(level + 1).join('#') + ' ');
+ this.set_text(source);
+ this.refresh();
+ if (this.rendered) {
+ this.render();
+ }
+ };
+
+ /**
+ * @method render
+ */
+ MarkdownCell.prototype.render = function () {
+ var cont = TextCell.prototype.render.apply(this);
+ if (cont) {
+ var that = this;
+ var text = this.get_text();
+ var math = null;
+ if (text === "") { text = this.placeholder; }
+ var text_and_math = mathjaxutils.remove_math(text);
+ text = text_and_math[0];
+ math = text_and_math[1];
+ marked(text, function (err, html) {
+ html = mathjaxutils.replace_math(html, math);
+ html = security.sanitize_html(html);
+ html = $($.parseHTML(html));
+ // add anchors to headings
+ html.find(":header").addBack(":header").each(function (i, h) {
+ h = $(h);
+ var hash = h.text().replace(/ /g, '-');
+ h.attr('id', hash);
+ h.append(
+ $('<a/>')
+ .addClass('anchor-link')
+ .attr('href', '#' + hash)
+ .text('¶')
+ .on('click',function(){
+ setTimeout(function(){that.unrender(); that.render()}, 100)
+ })
+ );
+ });
+ // links in markdown cells should open in new tabs
+ html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
+ that.set_rendered(html);
+ that.typeset();
+ that.events.trigger("rendered.MarkdownCell", {cell: that});
+ });
+ }
+ return cont;
+ };
+
+ /** @method bind_events **/
+ MarkdownCell.prototype.bind_events = function () {
+ TextCell.prototype.bind_events.apply(this);
+ var that = this;
+
+ this.element.dblclick(function () {
+ var cont = that.unrender();
+ if (cont) {
+ that.focus_editor();
+ }
+ });
+ };
+
+ var RawCell = function (options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * config: ConfigSection instance
+ * keyboard_manager: KeyboardManager instance
+ * notebook: Notebook instance
+ */
+ options = options || {};
+ var config = utils.mergeopt(RawCell, {});
+ TextCell.apply(this, [$.extend({}, options, {config: config})]);
+
+ this.class_config = new configmod.ConfigWithDefaults(options.config,
+ RawCell.config_defaults, 'RawCell');
+ this.cell_type = 'raw';
+ };
+
+ RawCell.options_default = {
+ placeholder : "Write raw LaTeX or other formats here, for use with nbconvert. " +
+ "It will not be rendered in the notebook. " +
+ "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
+ };
+
+ RawCell.config_defaults = {
+ highlight_modes : {
+ 'diff' :{'reg':[/^diff/]}
+ },
+ };
+
+ RawCell.prototype = Object.create(TextCell.prototype);
+
+ /** @method bind_events **/
+ RawCell.prototype.bind_events = function () {
+ TextCell.prototype.bind_events.apply(this);
+ var that = this;
+ this.element.focusout(function() {
+ that.auto_highlight();
+ that.render();
+ });
+
+ this.code_mirror.on('focus', function() { that.unrender(); });
+ };
+
+ /** @method render **/
+ RawCell.prototype.render = function () {
+ var cont = TextCell.prototype.render.apply(this);
+ if (cont){
+ var text = this.get_text();
+ if (text === "") { text = this.placeholder; }
+ this.set_text(text);
+ this.element.removeClass('rendered');
+ this.auto_highlight();
+ }
+ return cont;
+ };
+
+ var textcell = {
+ TextCell: TextCell,
+ MarkdownCell: MarkdownCell,
+ RawCell: RawCell
+ };
+ return textcell;
+});
diff --git a/notebook/static/notebook/js/toolbar.js b/notebook/static/notebook/js/toolbar.js
new file mode 100644
index 0000000..8b85c0b
--- /dev/null
+++ b/notebook/static/notebook/js/toolbar.js
@@ -0,0 +1,137 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery'
+], function($) {
+ "use strict";
+
+ /**
+ * A generic toolbar on which one can add button
+ * @class ToolBar
+ * @constructor
+ * @param {Dom_object} selector
+ */
+ var ToolBar = function (selector, options) {
+ this.selector = selector;
+ this.actions = (options||{}).actions;
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.style();
+ }
+ };
+
+ ToolBar.prototype._pseudo_actions={};
+
+
+ ToolBar.prototype.construct = function (config) {
+ for(var k=0; k<config.length; k++) {
+ this.add_buttons_group(config[k][0],config[k][1]);
+ }
+ };
+
+ /**
+ * Add a group of button into the current toolbar.
+ *
+ * Use a [dict of [list of action name]] to trigger
+ * on click to the button
+ *
+ * @example
+ *
+ * ... todo, maybe use a list of list to keep ordering.
+ *
+ * [
+ * [
+ * [
+ * action_name_1,
+ * action_name_2,
+ * action_name_3,
+ * ],
+ * optional_group_name
+ * ],
+ * ...
+ * ]
+ *
+ * @param list {List}
+ * List of button of the group, with the following paramter for each :
+ * @param list.label {string} text to show on button hover
+ * @param list.icon {string} icon to choose from [Font Awesome](http://fortawesome.github.io/Font-Awesome)
+ * @param list.callback {function} function to be called on button click
+ * @param [list.id] {String} id to give to the button
+ * @param [group_id] {String} optionnal id to give to the group
+ *
+ *
+ * for private usage, the key can also be strings starting with '<' and ending with '>' to inject custom element that cannot
+ * be bound to an action.
+ *
+ */
+ // TODO JUPYTER:
+ // get rid of legacy code that handle things that are not actions.
+ ToolBar.prototype.add_buttons_group = function (list, group_id) {
+ // handle custom call of pseudoaction binding.
+ if(typeof(list) === 'string' && list.slice(0,1) === '<' && list.slice(-1) === '>'){
+ var _pseudo_action;
+ try{
+ _pseudo_action = list.slice(1,-1);
+ this.element.append(this._pseudo_actions[_pseudo_action].call(this));
+ } catch (e) {
+ console.warn('ouch, calling ', _pseudo_action, 'does not seem to work...:', e);
+ }
+ return ;
+ }
+ var that = this;
+ var btn_group = $('<div/>').addClass("btn-group");
+ if( group_id !== undefined ) {
+ btn_group.attr('id',group_id);
+ }
+ for(var i=0; i < list.length; i++) {
+
+ // IIFE because javascript don't have loop scope so
+ // action_name would otherwise be the same on all iteration
+ // of the loop
+ (function(i,list){
+ var el = list[i];
+ var action_name;
+ var action;
+ if(typeof(el) === 'string'){
+ action = that.actions.get(el);
+ action_name = el;
+
+ }
+ var button = $('<button/>')
+ .addClass('btn btn-default')
+ .attr("title", el.label||action.help)
+ .append(
+ $("<i/>").addClass(el.icon||(action||{icon:'fa-exclamation-triangle'}).icon).addClass('fa')
+ );
+ var id = el.id;
+ if( id !== undefined ){
+ button.attr('id',id);
+ }
+ button.attr('data-jupyter-action', action_name);
+ var fun = el.callback|| function(){
+ that.actions.call(action_name);
+ };
+ button.click(fun);
+ btn_group.append(button);
+ })(i,list);
+ // END IIFE
+ }
+ $(this.selector).append(btn_group);
+ return btn_group;
+ };
+
+ ToolBar.prototype.style = function () {
+ this.element.addClass('toolbar');
+ };
+
+ /**
+ * Show and hide toolbar
+ * @method toggle
+ */
+ ToolBar.prototype.toggle = function () {
+ this.element.toggle();
+ };
+
+ return {'ToolBar': ToolBar};
+});
diff --git a/notebook/static/notebook/js/tooltip.js b/notebook/static/notebook/js/tooltip.js
new file mode 100644
index 0000000..d131200
--- /dev/null
+++ b/notebook/static/notebook/js/tooltip.js
@@ -0,0 +1,322 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+], function($, utils) {
+ "use strict";
+
+ // tooltip constructor
+ var Tooltip = function (events) {
+ var that = this;
+ this.events = events;
+ this.time_before_tooltip = 1200;
+
+ // handle to html
+ this.tooltip = $('#tooltip');
+ this._hidden = true;
+
+ // variable for consecutive call
+ this._old_cell = null;
+ this._old_request = null;
+ this._consecutive_counter = 0;
+
+ // 'sticky ?'
+ this._sticky = false;
+
+ // display tooltip if the docstring is empty?
+ this._hide_if_no_docstring = false;
+
+ // contain the button in the upper right corner
+ this.buttons = $('<div/>').addClass('tooltipbuttons');
+
+ // will contain the docstring
+ this.text = $('<div/>').addClass('tooltiptext').addClass('smalltooltip');
+
+ // build the buttons menu on the upper right
+ // expand the tooltip to see more
+ var expandlink = $('<a/>').attr('href', "#").addClass("ui-corner-all") //rounded corner
+ .attr('role', "button").attr('id', 'expanbutton').attr('title', 'Grow the tooltip vertically (press shift-tab twice)').click(function () {
+ that.expand();
+ event.preventDefault();
+ }).append(
+ $('<span/>').text('Expand').addClass('ui-icon').addClass('ui-icon-plus'));
+
+ // open in pager
+ var morelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button').attr('title', 'show the current docstring in pager (press shift-tab 4 times)');
+ var morespan = $('<span/>').text('Open in Pager').addClass('ui-icon').addClass('ui-icon-arrowstop-l-n');
+ morelink.append(morespan);
+ morelink.click(function () {
+ that.showInPager(that._old_cell);
+ event.preventDefault();
+ });
+
+ // close the tooltip
+ var closelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button');
+ var closespan = $('<span/>').text('Close').addClass('ui-icon').addClass('ui-icon-close');
+ closelink.append(closespan);
+ closelink.click(function () {
+ that.remove_and_cancel_tooltip(true);
+ event.preventDefault();
+ });
+
+ this._clocklink = $('<a/>').attr('href', "#");
+ this._clocklink.attr('role', "button");
+ this._clocklink.addClass('ui-button');
+ this._clocklink.attr('title', 'Tooltip will linger for 10 seconds while you type');
+ var clockspan = $('<span/>').text('Close');
+ clockspan.addClass('ui-icon');
+ clockspan.addClass('ui-icon-clock');
+ this._clocklink.append(clockspan);
+ this._clocklink.click(function () {
+ that.cancel_stick();
+ event.preventDefault();
+ });
+
+
+
+
+ //construct the tooltip
+ // add in the reverse order you want them to appear
+ this.buttons.append(closelink);
+ this.buttons.append(expandlink);
+ this.buttons.append(morelink);
+ this.buttons.append(this._clocklink);
+ this._clocklink.hide();
+
+
+ // we need a phony element to make the small arrow
+ // of the tooltip in css
+ // we will move the arrow later
+ this.arrow = $('<div/>').addClass('pretooltiparrow');
+ this.tooltip.append(this.buttons);
+ this.tooltip.append(this.arrow);
+ this.tooltip.append(this.text);
+
+ // function that will be called if you press tab 1, 2, 3... times in a row
+ this.tabs_functions = [function (cell, text, cursor) {
+ that._request_tooltip(cell, text, cursor);
+ }, function () {
+ that.expand();
+ }, function () {
+ that.stick();
+ }, function (cell) {
+ that.cancel_stick();
+ that.showInPager(cell);
+ }];
+ // call after all the tabs function above have bee call to clean their effects
+ // if necessary
+ this.reset_tabs_function = function (cell, text) {
+ this._old_cell = (cell) ? cell : null;
+ this._old_request = (text) ? text : null;
+ this._consecutive_counter = 0;
+ };
+ };
+
+ Tooltip.prototype.is_visible = function () {
+ return !this._hidden;
+ };
+
+ Tooltip.prototype.showInPager = function (cell) {
+ /**
+ * reexecute last call in pager by appending ? to show back in pager
+ */
+ this.events.trigger('open_with_text.Pager', this._reply.content);
+ this.remove_and_cancel_tooltip();
+ };
+
+ // grow the tooltip verticaly
+ Tooltip.prototype.expand = function () {
+ this.text.removeClass('smalltooltip');
+ this.text.addClass('bigtooltip');
+ $('#expanbutton').hide('slow');
+ };
+
+ // deal with all the logic of hiding the tooltip
+ // and reset it's status
+ Tooltip.prototype._hide = function () {
+ this._hidden = true;
+ this.tooltip.fadeOut('fast');
+ $('#expanbutton').show('slow');
+ this.text.removeClass('bigtooltip');
+ this.text.addClass('smalltooltip');
+ // keep scroll top to be sure to always see the first line
+ this.text.scrollTop(0);
+ this.code_mirror = null;
+ };
+
+ // return true on successfully removing a visible tooltip; otherwise return
+ // false.
+ Tooltip.prototype.remove_and_cancel_tooltip = function (force) {
+ /**
+ * note that we don't handle closing directly inside the calltip
+ * as in the completer, because it is not focusable, so won't
+ * get the event.
+ */
+ this.cancel_pending();
+ if (!this._hidden) {
+ if (force || !this._sticky) {
+ this.cancel_stick();
+ this._hide();
+ }
+ this.reset_tabs_function();
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ // cancel autocall done after '(' for example.
+ Tooltip.prototype.cancel_pending = function () {
+ if (this._tooltip_timeout !== null) {
+ clearTimeout(this._tooltip_timeout);
+ this._tooltip_timeout = null;
+ }
+ };
+
+ // will trigger tooltip after timeout
+ Tooltip.prototype.pending = function (cell, hide_if_no_docstring) {
+ var that = this;
+ this._tooltip_timeout = setTimeout(function () {
+ that.request(cell, hide_if_no_docstring);
+ }, that.time_before_tooltip);
+ };
+
+ // easy access for julia monkey patching.
+ Tooltip.last_token_re = /[a-z_][0-9a-z._]*$/gi;
+
+ Tooltip.prototype._request_tooltip = function (cell, text, cursor_pos) {
+ var callbacks = $.proxy(this._show, this);
+ var msg_id = cell.kernel.inspect(text, cursor_pos, callbacks);
+ };
+
+ // make an immediate completion request
+ Tooltip.prototype.request = function (cell, hide_if_no_docstring) {
+ /**
+ * request(codecell)
+ * Deal with extracting the text from the cell and counting
+ * call in a row
+ */
+ this.cancel_pending();
+ var editor = cell.code_mirror;
+ var cursor = editor.getCursor();
+ var cursor_pos = editor.indexFromPos(cursor);
+ var text = cell.get_text();
+
+ this._hide_if_no_docstring = hide_if_no_docstring;
+
+ if(editor.somethingSelected()){
+ // get only the most recent selection.
+ text = editor.getSelection();
+ }
+
+ // need a permanent handle to code_mirror for future auto recall
+ this.code_mirror = editor;
+
+ // now we treat the different number of keypress
+ // first if same cell, same text, increment counter by 1
+ if (this._old_cell == cell && this._old_request == text && this._hidden === false) {
+ this._consecutive_counter++;
+ } else {
+ // else reset
+ this.cancel_stick();
+ this.reset_tabs_function (cell, text);
+ }
+
+ this.tabs_functions[this._consecutive_counter](cell, text, cursor_pos);
+
+ // then if we are at the end of list function, reset
+ if (this._consecutive_counter == this.tabs_functions.length) {
+ this.reset_tabs_function (cell, text, cursor);
+ }
+
+ return;
+ };
+
+ // cancel the option of having the tooltip to stick
+ Tooltip.prototype.cancel_stick = function () {
+ clearTimeout(this._stick_timeout);
+ this._stick_timeout = null;
+ this._clocklink.hide('slow');
+ this._sticky = false;
+ };
+
+ // put the tooltip in a sicky state for 10 seconds
+ // it won't be removed by remove_and_cancell() unless you called with
+ // the first parameter set to true.
+ // remove_and_cancell_tooltip(true)
+ Tooltip.prototype.stick = function (time) {
+ time = (time !== undefined) ? time : 10;
+ var that = this;
+ this._sticky = true;
+ this._clocklink.show('slow');
+ this._stick_timeout = setTimeout(function () {
+ that._sticky = false;
+ that._clocklink.hide('slow');
+ }, time * 1000);
+ };
+
+ // should be called with the kernel reply to actually show the tooltip
+ Tooltip.prototype._show = function (reply) {
+ /**
+ * move the bubble if it is not hidden
+ * otherwise fade it
+ */
+ this._reply = reply;
+ var content = reply.content;
+ if (!content.found) {
+ // object not found, nothing to show
+ return;
+ }
+ this.name = content.name;
+
+ // do some math to have the tooltip arrow on more or less on left or right
+ // position of the editor
+ var cm_pos = $(this.code_mirror.getWrapperElement()).position();
+
+ // anchor and head positions are local within CodeMirror element
+ var anchor = this.code_mirror.cursorCoords(false, 'local');
+ var head = this.code_mirror.cursorCoords(true, 'local');
+ // locate the target at the center of anchor, head
+ var center_left = (head.left + anchor.left) / 2;
+ // locate the left edge of the tooltip, at most 450 px left of the arrow
+ var edge_left = Math.max(center_left - 450, 0);
+ // locate the arrow at the cursor. A 24 px offset seems necessary.
+ var arrow_left = center_left - edge_left - 24;
+
+ // locate left, top within container element
+ var left = (cm_pos.left + edge_left) + 'px';
+ var top = (cm_pos.top + head.bottom + 10) + 'px';
+
+ if (this._hidden === false) {
+ this.tooltip.animate({
+ left: left,
+ top: top
+ });
+ } else {
+ this.tooltip.css({
+ left: left
+ });
+ this.tooltip.css({
+ top: top
+ });
+ }
+ this.arrow.animate({
+ 'left': arrow_left + 'px'
+ });
+
+ this._hidden = false;
+ this.tooltip.fadeIn('fast');
+ this.text.children().remove();
+
+ // This should support rich data types, but only text/plain for now
+ // Any HTML within the docstring is escaped by the fixConsole() method.
+ var pre = $('<pre/>').html(utils.fixConsole(content.data['text/plain']));
+ this.text.append(pre);
+ // keep scroll top to be sure to always see the first line
+ this.text.scrollTop(0);
+ };
+
+ return {'Tooltip': Tooltip};
+});
diff --git a/notebook/static/notebook/js/tour.js b/notebook/static/notebook/js/tour.js
new file mode 100644
index 0000000..9ac56a5
--- /dev/null
+++ b/notebook/static/notebook/js/tour.js
@@ -0,0 +1,172 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'bootstraptour',
+], function($, Tour) {
+ "use strict";
+
+ var tour_style = "<div class='popover tour'>\n" +
+ "<div class='arrow'></div>\n" +
+ "<div style='position:absolute; top:7px; right:7px'>\n" +
+ "<button class='btn btn-default btn-sm fa fa-times' data-role='end'></button>\n" +
+ "</div><h3 class='popover-title'></h3>\n" +
+ "<div class='popover-content'></div>\n" +
+ "<div class='popover-navigation'>\n" +
+ "<button class='btn btn-default fa fa-step-backward' data-role='prev'></button>\n" +
+ "<button class='btn btn-default fa fa-step-forward pull-right' data-role='next'></button>\n" +
+ "<button id='tour-pause' class='btn btn-sm btn-default fa fa-pause' data-resume-text='' data-pause-text='' data-role='pause-resume'></button>\n" +
+ "</div>\n" +
+ "</div>";
+
+ var NotebookTour = function (notebook, events) {
+ var that = this;
+ this.notebook = notebook;
+ this.step_duration = 0;
+ this.events = events;
+ this.tour_steps = [
+ {
+ title: "Welcome to the Notebook Tour",
+ placement: 'bottom',
+ orphan: true,
+ content: "You can use the left and right arrow keys to go backwards and forwards."
+ }, {
+ element: "#notebook_name",
+ title: "Filename",
+ placement: 'bottom',
+ content: "Click here to change the filename for this notebook."
+ }, {
+ element: $("#menus").parent(),
+ placement: 'bottom',
+ title: "Notebook Menubar",
+ content: "The menubar has menus for actions on the notebook, its cells, and the kernel it communicates with."
+ }, {
+ element: "#maintoolbar",
+ placement: 'bottom',
+ title: "Notebook Toolbar",
+ content: "The toolbar has buttons for the most common actions. Hover your mouse over each button for more information."
+ }, {
+ element: "#modal_indicator",
+ title: "Mode Indicator",
+ placement: 'bottom',
+ content: "The Notebook has two modes: Edit Mode and Command Mode. In this area, an indicator can appear to tell you which mode you are in.",
+ onShow: function(tour) { that.command_icon_hack(); }
+ }, {
+ element: "#modal_indicator",
+ title: "Command Mode",
+ placement: 'bottom',
+ onShow: function(tour) { notebook.command_mode(); that.command_icon_hack(); },
+ onNext: function(tour) { that.edit_mode(); },
+ content: "Right now you are in Command Mode, and many keyboard shortcuts are available. In this mode, no icon is displayed in the indicator area."
+ }, {
+ element: "#modal_indicator",
+ title: "Edit Mode",
+ placement: 'bottom',
+ onShow: function(tour) { that.edit_mode(); },
+ content: "Pressing <code>Enter</code> or clicking in the input text area of the cell switches to Edit Mode."
+ }, {
+ element: '.selected',
+ title: "Edit Mode",
+ placement: 'bottom',
+ onShow: function(tour) { that.edit_mode(); },
+ content: "Notice that the border around the currently active cell changed color. Typing will insert text into the currently active cell."
+ }, {
+ element: '.selected',
+ title: "Back to Command Mode",
+ placement: 'bottom',
+ onShow: function(tour) { notebook.command_mode(); },
+ content: "Pressing <code>Esc</code> or clicking outside of the input text area takes you back to Command Mode."
+ }, {
+ element: '#keyboard_shortcuts',
+ title: "Keyboard Shortcuts",
+ placement: 'bottom',
+ onShow: function(tour) {
+ /** need to add `open` and `pulse` classes in 2 calls */
+ $('#help_menu').parent().addClass('open');
+ $('#help_menu').parent().addClass('pulse');
+ $('#keyboard_shortcuts').addClass('pulse');
+ },
+ onHide: function(tour) {
+ $('#help_menu').parent().removeClass('open pulse');
+ $('#keyboard_shortcuts').removeClass('pulse');
+ },
+ content: "You can click here to get a list of all of the keyboard shortcuts."
+ }, {
+ element: "#kernel_indicator_icon",
+ title: "Kernel Indicator",
+ placement: 'bottom',
+ onShow: function(tour) { events.trigger('kernel_idle.Kernel');},
+ content: "This is the Kernel indicator. It looks like this when the Kernel is idle."
+ }, {
+ element: "#kernel_indicator_icon",
+ title: "Kernel Indicator",
+ placement: 'bottom',
+ onShow: function(tour) { events.trigger('kernel_busy.Kernel'); },
+ content: "The Kernel indicator looks like this when the Kernel is busy."
+ }, {
+ element: ".fa-stop",
+ placement: 'bottom',
+ title: "Interrupting the Kernel",
+ onHide: function(tour) { events.trigger('kernel_idle.Kernel'); },
+ content: "To cancel a computation in progress, you can click here."
+ }, {
+ element: "#notification_kernel",
+ placement: 'bottom',
+ onShow: function(tour) { $('.fa-stop').click(); },
+ title: "Notification Area",
+ content: "Messages in response to user actions (Save, Interrupt, etc) appear here."
+ }, {
+ title: "Fin.",
+ placement: 'bottom',
+ orphan: true,
+ content: "This concludes the Jupyter Notebook User Interface Tour. Happy hacking!"
+ }
+ ];
+
+ this.tour = new Tour({
+ storage: false, // start tour from beginning every time
+ debug: true,
+ reflex: true, // click on element to continue tour
+ animation: false,
+ duration: this.step_duration,
+ onStart: function() { console.log('tour started'); },
+ // TODO: remove the onPause/onResume logic once pi's patch has been
+ // merged upstream to make this work via data-resume-class and
+ // data-resume-text attributes.
+ onPause: this.toggle_pause_play,
+ onResume: this.toggle_pause_play,
+ steps: this.tour_steps,
+ template: tour_style,
+ orphan: true
+ });
+
+ };
+
+ NotebookTour.prototype.start = function () {
+ console.log("let's start the tour");
+ this.tour.init();
+ this.tour.start();
+ if (this.tour.ended())
+ {
+ this.tour.restart();
+ }
+ };
+
+ NotebookTour.prototype.command_icon_hack = function() {
+ $('#modal_indicator').css('min-height', '18px');
+ };
+
+ NotebookTour.prototype.toggle_pause_play = function () {
+ $('#tour-pause').toggleClass('fa-pause fa-play');
+ };
+
+ NotebookTour.prototype.edit_mode = function() {
+ this.notebook.focus_cell();
+ this.notebook.edit_mode();
+ };
+
+ return {'Tour': NotebookTour};
+
+});
+
diff --git a/notebook/static/notebook/less/ansicolors.less b/notebook/static/notebook/less/ansicolors.less
new file mode 100644
index 0000000..7f15301
--- /dev/null
+++ b/notebook/static/notebook/less/ansicolors.less
@@ -0,0 +1,23 @@
+/* CSS font colors for translated ANSI colors. */
+
+.ansibold {font-weight: bold;}
+
+/* use dark versions for foreground, to improve visibility */
+.ansiblack {color: black;}
+.ansired {color: darkred;}
+.ansigreen {color: darkgreen;}
+.ansiyellow {color: #c4a000;}
+.ansiblue {color: darkblue;}
+.ansipurple {color: darkviolet;}
+.ansicyan {color: steelblue;}
+.ansigray {color: gray;}
+
+/* and light for background, for the same reason */
+.ansibgblack {background-color: black;}
+.ansibgred {background-color: red;}
+.ansibggreen {background-color: green;}
+.ansibgyellow {background-color: yellow;}
+.ansibgblue {background-color: blue;}
+.ansibgpurple {background-color: magenta;}
+.ansibgcyan {background-color: cyan;}
+.ansibggray {background-color: gray;}
diff --git a/notebook/static/notebook/less/cell.less b/notebook/static/notebook/less/cell.less
new file mode 100644
index 0000000..76fcb7a
--- /dev/null
+++ b/notebook/static/notebook/less/cell.less
@@ -0,0 +1,150 @@
+@_cell_padding_minus_border: @cell_padding - @cell_border_width;
+
+._selected_style(@c1, @c2, @sep:0, @border_width:@cell_border_width) {
+ border-left-width: @border_width;
+ padding-left: @cell_padding - @border_width;
+ background: linear-gradient(to right, @c1 -40px,@c1 @sep,@c2 @sep,@c2 100%);
+}
+
+
+div.cell {
+ .vbox();
+ .corner-all();
+ .border-box-sizing();
+
+ border-width: @cell_border_width;
+ border-style: solid;
+ border-color: transparent;
+
+ width: 100%;
+ padding: @_cell_padding_minus_border;
+ /* This acts as a spacer between cells, that is outside the border */
+ margin: 0px;
+ outline: none;
+
+ ._selected_style(transparent, transparent, @cell_border_width);
+
+ &.jupyter-soft-selected {
+ border-left-color: @selected_border_color_light;
+ border-left-color: @soft_select_color;
+ border-left-width: @cell_border_width;
+ padding-left: @cell_padding - @cell_border_width;
+ border-right-color: @soft_select_color;
+ border-right-width: @cell_border_width;
+ background: @soft_select_color;
+
+ @media print {
+ border-color: transparent;
+ }
+ }
+
+ &.selected {
+ border-color: @border_color;
+ ._selected_style(@selected_border_color, transparent, 5px, 0px);
+
+ @media print {
+ border-color: transparent;
+ }
+ }
+
+ &.selected.jupyter-soft-selected {
+ ._selected_style(@selected_border_color, @soft_select_color, 7px, 0);
+ }
+
+ .edit_mode &.selected {
+ border-color: @edit_mode_border_color;
+ ._selected_style(@edit_mode_border_color, transparent, 5px, 0px);
+
+ @media print {
+ border-color: transparent;
+ }
+ }
+
+}
+
+
+.prompt {
+ /* This needs to be wide enough for 3 digit prompt numbers: In[100]: */
+ min-width: 14ex;
+
+ /* This padding is tuned to match the padding on the CodeMirror editor. */
+ padding: @code_padding;
+ margin: 0px;
+ font-family: @font-family-monospace;
+ text-align: right;
+
+ /* This has to match that of the the CodeMirror class line-height below */
+ line-height: @code_line_height;
+
+ /* Don't highlight prompt number selection */
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ /* Use default cursor */
+ cursor: default;
+}
+
+@media (max-width: @screen-xs-min) {
+ // prompts are in the main column on small screens,
+ // so text should be left-aligned
+ .prompt {
+ text-align: left;
+ }
+}
+
+div.inner_cell {
+ min-width: 0;
+ .vbox();
+ .box-flex1();
+}
+
+/* input_area and input_prompt must match in top border and margin for alignment */
+div.input_area {
+ border: 1px solid @light_border_color;
+ .corner-all;
+ background: @cell_background;
+ line-height: @code_line_height;
+}
+
+/* This is needed so that empty prompt areas can collapse to zero height when there
+ is no content in the output_subarea and the prompt. The main purpose of this is
+ to make sure that empty JavaScript output_subareas have no height. */
+div.prompt:empty {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+div.unrecognized_cell {
+ // from text_cell
+ padding: 5px @_cell_padding_minus_border 5px 0px;
+ .hbox();
+
+ .inner_cell {
+ .border-radius(@border-radius-base);
+ padding: @_cell_padding_minus_border;
+ font-weight: bold;
+ color: red;
+ border: @cell_border_width solid @light_border_color;
+ background: darken(@cell_background, 5%);
+ // remove decoration from link
+ a {
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ color: inherit;
+ text-decoration: none;
+ }
+ }
+ }
+}
+@media (max-width: @screen-xs-min) {
+ // remove prompt indentation on small screens
+ div.unrecognized_cell > div.prompt {
+ display: none;
+ }
+}
diff --git a/notebook/static/notebook/less/celltoolbar.less b/notebook/static/notebook/less/celltoolbar.less
new file mode 100644
index 0000000..2f77f42
--- /dev/null
+++ b/notebook/static/notebook/less/celltoolbar.less
@@ -0,0 +1,70 @@
+/* CSS for the cell toolbar */
+@celltoolbar-height: 29px;
+
+.celltoolbar {
+ border: thin solid #CFCFCF;
+ border-bottom: none;
+ background : #EEE;
+ border-radius : @border-radius-base @border-radius-base 0px 0px;
+ width:100%;
+ -webkit-box-pack: end;
+ height: @celltoolbar-height;
+ padding-right: 4px;
+ .hbox();
+ .end();
+ // safari fix, we cannot use -webkit-flex on hbox
+ // and vbox either or all css get borked
+ // cf https://github.com/jupyter/nbgrader/issues/394
+ display: -webkit-flex;
+ @media print{
+ display: none;
+ }
+}
+
+
+.ctb_hideshow {
+ display:none;
+ vertical-align:bottom;
+}
+
+/* ctb_show is added to the ctb_hideshow div to show the cell toolbar.
+ Cell toolbars are only shown when the ctb_global_show class is also set.
+*/
+.ctb_global_show .ctb_show.ctb_hideshow {
+ display: block;
+}
+
+.ctb_global_show .ctb_show + .input_area,
+.ctb_global_show .ctb_show + div.text_cell_input,
+.ctb_global_show .ctb_show ~ div.text_cell_render {
+ border-top-right-radius: 0px;
+ border-top-left-radius: 0px;
+}
+
+.ctb_global_show .ctb_show ~ div.text_cell_render {
+ // add border to rendered markdown cells
+ border: @border_width solid @light_border_color;
+}
+
+.celltoolbar {
+ font-size: 87%;
+ padding-top: 3px;
+}
+
+.celltoolbar select {
+ .form-control();
+ .input-sm();
+ // undo some of the sizing caused by the above mixins
+ width: inherit;
+ font-size: inherit;
+ height: 22px;
+ padding: 0px;
+
+ display: inline-block;
+
+}
+
+.celltoolbar label {
+ margin-left: 5px;
+ margin-right: 5px;
+}
diff --git a/notebook/static/notebook/less/codecell.less b/notebook/static/notebook/less/codecell.less
new file mode 100644
index 0000000..4256146
--- /dev/null
+++ b/notebook/static/notebook/less/codecell.less
@@ -0,0 +1,48 @@
+div.code_cell {
+ /* avoid page breaking on code cells when printing */
+ @media print {
+ page-break-inside: avoid;
+ }
+}
+
+/* any special styling for code cells that are currently running goes here */
+div.code_cell.running {
+}
+
+div.input {
+ page-break-inside: avoid;
+ .hbox();
+}
+
+@media (max-width: @screen-xs-min) {
+ // move prompts above code on small screens
+ div.input {
+ .vbox();
+ }
+}
+
+/* input_area and input_prompt must match in top border and margin for alignment */
+div.input_prompt {
+ color: @input_prompt_color;
+ border-top: 1px solid transparent;
+}
+
+// The styles related to div.highlight are for nbconvert HTML output only. This works
+// because the .highlight div isn't present in the live notebook. We could put this into
+// nbconvert, but it easily falls out of sync, can't use our less variables and doesn't
+// help the basic template when paired with our CSS.
+
+div.input_area > div.highlight {
+ margin: @code_padding;
+ border: none;
+ padding: 0px;
+ background-color: transparent;
+}
+
+div.input_area > div.highlight > pre {
+ margin: 0px;
+ border: none;
+ padding: 0px;
+ background-color: transparent;
+}
+
diff --git a/notebook/static/notebook/less/codemirror.less b/notebook/static/notebook/less/codemirror.less
new file mode 100644
index 0000000..e423b2f
--- /dev/null
+++ b/notebook/static/notebook/less/codemirror.less
@@ -0,0 +1,52 @@
+/* The following gets added to the <head> if it is detected that the user has a
+ * monospace font with inconsistent normal/bold/italic height. See
+ * notebookmain.js. Such fonts will have keywords vertically offset with
+ * respect to the rest of the text. The user should select a better font.
+ * See: https://github.com/ipython/ipython/issues/1503
+ *
+ * .CodeMirror span {
+ * vertical-align: bottom;
+ * }
+ */
+
+.CodeMirror {
+ line-height: @code_line_height; /* Changed from 1em to our global default */
+ font-size: @notebook_font_size;
+ height: auto; /* Changed to auto to autogrow */
+ background: none; /* Changed from white to allow our bg to show through */
+}
+
+.CodeMirror-scroll {
+ /* The CodeMirror docs are a bit fuzzy on if overflow-y should be hidden or visible.*/
+ /* We have found that if it is visible, vertical scrollbars appear with font size changes.*/
+ overflow-y: hidden;
+ overflow-x: auto;
+}
+
+.CodeMirror-lines {
+ /* In CM2, this used to be 0.4em, but in CM3 it went to 4px. We need the em value because */
+ /* we have set a different line-height and want this to scale with that. */
+ padding: @code_padding;
+}
+
+.CodeMirror-linenumber {
+ // This is needed to fine tune the position of the line numbers because we use the 0.4em in @code_padding
+ // spacing in various places. Fine tuned to look right.
+ padding: 0 8px 0 4px;
+}
+
+.CodeMirror-gutters {
+ // This is needed because our cell has rounded corners, otherwise the gutter area square
+ // corner cuts into the rounded cell border.
+ border-bottom-left-radius: @border-radius-base;
+ border-top-left-radius: @border-radius-base;
+}
+
+.CodeMirror pre {
+ /* In CM3 this went to 4px from 0 in CM2. We need the 0 value because of how we size */
+ /* .CodeMirror-lines */
+ padding: 0;
+ border: 0;
+ .border-radius(0)
+}
+
diff --git a/notebook/static/notebook/less/commandpalette.less b/notebook/static/notebook/less/commandpalette.less
new file mode 100644
index 0000000..f7f4e89
--- /dev/null
+++ b/notebook/static/notebook/less/commandpalette.less
@@ -0,0 +1,45 @@
+ul.typeahead-list i{
+ margin-left: -10px;
+ width: 18px;
+}
+
+ul.typeahead-list {
+ max-height: 80vh;
+ overflow:auto;
+
+ & > li > a {
+ /** Firefox bug **/
+ /* see https://github.com/jupyter/notebook/issues/559 */
+ white-space: normal;
+ }
+}
+
+.cmd-palette {
+ & .modal-body{
+ padding: 7px;
+ }
+
+ & form {
+ background: white;
+ }
+
+ & input {
+ outline:none;
+ }
+}
+
+.no-shortcut{
+ display:none;
+}
+
+.command-shortcut:before{
+ content:"(command)";
+ padding-right:3px;
+ color:@gray-light;
+}
+
+.edit-shortcut:before{
+ content:"(edit)";
+ padding-right:3px;
+ color:@gray-light;
+}
diff --git a/notebook/static/notebook/less/completer.less b/notebook/static/notebook/less/completer.less
new file mode 100644
index 0000000..cc8f605
--- /dev/null
+++ b/notebook/static/notebook/less/completer.less
@@ -0,0 +1,26 @@
+.completions {
+ position: absolute;
+ z-index: 110;
+ overflow: hidden;
+ border: 1px solid @border_color;
+ .corner-all;
+ .box-shadow(0px 6px 10px -1px #adadad);
+ line-height: 1;
+}
+
+.completions select {
+ background: white;
+ outline: none;
+ border: none;
+ padding: 0px;
+ margin: 0px;
+ overflow: auto;
+ font-family: @font-family-monospace;
+ font-size: 110%;
+ color: @text-color;
+ width: auto;
+}
+
+.completions select option.context {
+ color: darken(@brand-primary, 10%);
+}
diff --git a/notebook/static/notebook/less/highlight-refs.less b/notebook/static/notebook/less/highlight-refs.less
new file mode 100644
index 0000000..a2c0ab6
--- /dev/null
+++ b/notebook/static/notebook/less/highlight-refs.less
@@ -0,0 +1,5 @@
+/* load the codemirror defaults as LESS so that highlight.less
+ can load default theme declarations by reference without pulling in the
+ nasty positioning
+*/
+@import (less) "../../components/codemirror/lib/codemirror.css";
diff --git a/notebook/static/notebook/less/highlight.less b/notebook/static/notebook/less/highlight.less
new file mode 100644
index 0000000..d0b2bf2
--- /dev/null
+++ b/notebook/static/notebook/less/highlight.less
@@ -0,0 +1,112 @@
+/*
+
+Original style from softwaremaniacs.org (c) Ivan Sagalaev <Maniac@SoftwareManiacs.Org>
+Adapted from GitHub theme
+
+*/
+
+@import (reference) "highlight-refs.less";
+
+@highlight-base: #000;
+
+.highlight-base{
+ color: @highlight-base;
+}
+
+.highlight-variable{
+ .highlight-base();
+}
+
+.highlight-variable-2{
+ color: lighten(@highlight-base, 10%);
+}
+
+.highlight-variable-3{
+ color: lighten(@highlight-base, 20%);
+}
+
+.highlight-string{
+ color: #BA2121;
+}
+
+.highlight-comment{
+ color: #408080;
+ font-style: italic;
+}
+
+.highlight-number{
+ color: #080;
+}
+
+.highlight-atom{
+ color: #88F;
+}
+
+.highlight-keyword{
+ color: #008000;
+ font-weight: bold;
+}
+
+.highlight-builtin{
+ color: #008000;
+}
+
+.highlight-error{
+ color: #f00;
+}
+
+.highlight-operator{
+ color: #AA22FF;
+ font-weight: bold;
+}
+
+.highlight-meta{
+ color: #AA22FF;
+}
+
+/* previously not defined, copying from default codemirror */
+.highlight-def{ .cm-s-default.cm-def() }
+.highlight-punctuation{ .cm-s-default.cm-punctuation() }
+.highlight-property{ .cm-s-default.cm-property() }
+.highlight-string-2{ .cm-s-default.cm-string-2() }
+.highlight-qualifier{ .cm-s-default.cm-qualifier() }
+.highlight-bracket{ .cm-s-default.cm-bracket() }
+.highlight-tag{ .cm-s-default.cm-tag() }
+.highlight-attribute{ .cm-s-default.cm-attribute() }
+.highlight-header{ .cm-s-default.cm-header() }
+.highlight-quote{ .cm-s-default.cm-quote() }
+.highlight-link{ .cm-s-default.cm-link() }
+
+
+/* apply the same style to codemirror */
+.cm-s-ipython span {
+ &.cm-keyword { .highlight-keyword() }
+ &.cm-atom { .highlight-atom() }
+ &.cm-number { .highlight-number() }
+ &.cm-def { .highlight-def() }
+ &.cm-variable { .highlight-variable() }
+ &.cm-punctuation { .highlight-punctuation() }
+ &.cm-property { .highlight-property() }
+ &.cm-operator { .highlight-operator() }
+ &.cm-variable-2 { .highlight-variable-2() }
+ &.cm-variable-3 { .highlight-variable-3() }
+ &.cm-comment { .highlight-comment() }
+ &.cm-string { .highlight-string() }
+ &.cm-string-2 { .highlight-string-2() }
+ &.cm-meta { .highlight-meta() }
+ &.cm-qualifier { .highlight-qualifier() }
+ &.cm-builtin { .highlight-builtin() }
+ &.cm-bracket { .highlight-bracket() }
+ &.cm-tag { .highlight-tag() }
+ &.cm-attribute { .highlight-attribute() }
+ &.cm-header { .highlight-header() }
+ &.cm-quote { .highlight-quote() }
+ &.cm-link { .highlight-link() }
+ &.cm-error { .highlight-error() }
+
+ &.cm-tab {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);
+ background-position: right;
+ background-repeat: no-repeat;
+ }
+}
diff --git a/notebook/static/notebook/less/kernelselector.less b/notebook/static/notebook/less/kernelselector.less
new file mode 100644
index 0000000..33c68a3
--- /dev/null
+++ b/notebook/static/notebook/less/kernelselector.less
@@ -0,0 +1,10 @@
+#kernel_logo_widget {
+ .pull-right();
+
+ .current_kernel_logo {
+ display: none;
+ .navbar-vertical-align(32px);
+ width: 32px;
+ height: 32px;
+ }
+}
diff --git a/notebook/static/notebook/less/menubar.less b/notebook/static/notebook/less/menubar.less
new file mode 100644
index 0000000..7f776c0
--- /dev/null
+++ b/notebook/static/notebook/less/menubar.less
@@ -0,0 +1,81 @@
+#menubar {
+ .border-box-sizing();
+ margin-top: 1px;
+
+ .navbar {
+ border-top: 1px;
+ border-radius: 0px 0px @border-radius-base @border-radius-base;
+ margin-bottom: 0px;
+ }
+
+ .navbar-toggle {
+ float: left;
+ padding-top:7px;
+ padding-bottom:7px;
+ border:none;
+ }
+ .navbar-collapse {
+ clear: left;
+ }
+
+}
+
+.nav-wrapper {
+ border-bottom: 1px solid @navbar-default-border;
+}
+
+i.menu-icon {
+ // add padding to account for float-right
+ padding-top: 4px;
+}
+
+ul#help_menu li a{
+ overflow: hidden;
+ padding-right: 2.2em;
+ i {
+ margin-right: -1.2em;
+ }
+}
+
+// Make sub menus work in BS3.
+// Credit: http://www.bootply.com/86684
+.dropdown-submenu {
+ position: relative;
+}
+
+.dropdown-submenu>.dropdown-menu {
+ top: 0;
+ left: 100%;
+ margin-top: -6px;
+ margin-left: -1px;
+}
+
+// arrow that indicate presence of submenu
+.dropdown-submenu:hover>.dropdown-menu {
+ display: block;
+}
+
+.dropdown-submenu>a:after {
+ .fa();
+ display: block;
+ content: @fa-var-caret-right;
+ float: right;
+ color: @dropdown-link-color;
+ margin-top: 2px;
+ margin-right: -10px;
+}
+
+.dropdown-submenu:hover>a:after {
+ color: @dropdown-link-hover-color;
+}
+
+.dropdown-submenu.pull-left {
+ float: none;
+}
+
+.dropdown-submenu.pull-left>.dropdown-menu {
+ left: -100%;
+ margin-left: 10px;
+}
+
+//end submenu
diff --git a/notebook/static/notebook/less/notebook.less b/notebook/static/notebook/less/notebook.less
new file mode 100644
index 0000000..2c3e8e6
--- /dev/null
+++ b/notebook/static/notebook/less/notebook.less
@@ -0,0 +1,101 @@
+@media (max-width: @screen-xs-max) {
+ // remove bootstrap-responsive's body padding on small screens
+ .notebook_app {
+ padding-left: 0px;
+ padding-right: 0px;
+ }
+}
+
+#ipython-main-app {
+ .border-box-sizing();
+ height: 100%;
+}
+
+div#notebook_panel {
+ margin: 0px;
+ padding: 0px;
+ .border-box-sizing();
+ height: 100%;
+}
+
+div#notebook {
+ font-size: @notebook_font_size;
+ line-height: @notebook_line_height;
+ overflow-y: hidden;
+ overflow-x: auto;
+ width: 100%;
+ /* This spaces the page away from the edge of the notebook area */
+ padding-top: @page-header-padding;
+ margin: 0px;
+ outline: none;
+ .border-box-sizing();
+ min-height: 100%;
+}
+
+#notebook-container{
+ @media not print{
+ padding: @page-padding;
+ background-color : @page-color;
+ min-height: @page-min-height;
+ .box-shadow(@global-shadow);
+ }
+ @media print {
+ width: 100%;
+ }
+}
+
+div.ui-widget-content {
+ border: 1px solid @border_color;
+ outline: none;
+}
+
+pre.dialog {
+ background-color: @cell_background;
+ border: 1px solid #ddd;
+ .corner-all;
+ padding: 0.4em;
+ padding-left: 2em;
+}
+
+p.dialog {
+ padding : 0.2em;
+}
+
+/* Word-wrap output correctly. This is the CSS3 spelling, though Firefox seems
+ to not honor it correctly. Webkit browsers (Chrome, rekonq, Safari) do.
+ */
+pre, code, kbd, samp { white-space: pre-wrap; }
+
+#fonttest {
+ font-family: @font-family-monospace;
+}
+
+p {
+ margin-bottom:0;
+}
+
+.end_space {
+ min-height: 100px;
+ transition: height .2s ease;
+}
+
+.notebook_app > #header {
+ .box-shadow(@global-shadow);
+}
+
+.notebook_app{
+ @media not print {
+ background-color: @page-backdrop-color;
+ }
+}
+
+kbd {
+ border-style: solid;
+ border-width: 1px;
+ box-shadow: none;
+ margin: 2px;
+ padding-left: 2px;
+ padding-right: 2px;
+ padding-top: 1px;
+ padding-bottom: 1px;
+}
diff --git a/notebook/static/notebook/less/notificationarea.less b/notebook/static/notebook/less/notificationarea.less
new file mode 100644
index 0000000..bfbf5b7
--- /dev/null
+++ b/notebook/static/notebook/less/notificationarea.less
@@ -0,0 +1,74 @@
+#notification_area {
+ .pull-right();
+ z-index: 10;
+}
+
+.indicator_area {
+ .pull-right();
+ color: @navbar-default-link-color;
+ margin-left: 5px;
+ margin-right: 5px;
+ width: 11px;
+ z-index: 10;
+ text-align: center;
+ width: auto;
+}
+
+#kernel_indicator {
+ .indicator_area();
+
+ border-left: 1px solid;
+
+ .kernel_indicator_name {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
+
+#modal_indicator {
+ .pull-right();
+ .indicator_area();
+}
+
+#readonly-indicator {
+ .pull-right();
+ .indicator_area();
+
+ margin-top: 2px;
+ margin-bottom: 0px;
+ margin-left: 0px;
+ margin-right: 0px;
+
+ // Start out hidden.
+ display: none;
+}
+
+.modal_indicator:before {
+ .fa-fw();
+}
+
+.edit_mode .modal_indicator:before {
+ .icon(@fa-var-pencil);
+}
+
+.command_mode .modal_indicator:before {
+ .icon(' ');
+}
+
+.kernel_idle_icon:before {
+ .icon(@fa-var-circle-o);
+}
+
+.kernel_busy_icon:before {
+ .icon(@fa-var-circle);
+}
+
+.kernel_dead_icon:before {
+ .icon(@fa-var-bomb);
+}
+
+.kernel_disconnected_icon:before {
+ .icon(@fa-var-chain-broken);
+}
+
+
diff --git a/notebook/static/notebook/less/notificationwidget.less b/notebook/static/notebook/less/notificationwidget.less
new file mode 100644
index 0000000..c9c376e
--- /dev/null
+++ b/notebook/static/notebook/less/notificationwidget.less
@@ -0,0 +1,21 @@
+.notification_widget {
+ color: @navbar-default-link-color;
+ z-index: 10;
+ background: @notification_widget_bg;
+ margin-right: 4px;
+ .btn-default();
+}
+
+.notification_widget.warning {
+ .btn-warning();
+}
+.notification_widget.success {
+ .btn-success();
+}
+.notification_widget.info {
+ .btn-info();
+}
+.notification_widget.danger {
+ .btn-danger();
+}
+
diff --git a/notebook/static/notebook/less/outputarea.less b/notebook/static/notebook/less/outputarea.less
new file mode 100644
index 0000000..b44cd7c
--- /dev/null
+++ b/notebook/static/notebook/less/outputarea.less
@@ -0,0 +1,209 @@
+div.output_wrapper {
+ /* this position must be relative to enable descendents to be absolute within it */
+ position: relative;
+ .vbox();
+ // avoid scrolled output overlaying input in some strange circumstances
+ z-index: 1;
+}
+
+/* class for the output area when it should be height-limited */
+div.output_scroll {
+ /* ideally, this would be max-height, but FF barfs all over that */
+ height: 24em;
+ /* FF needs this *and the wrapper* to specify full width, or it will shrinkwrap */
+ width: 100%;
+
+ overflow: auto;
+ .corner-all;
+ .box-shadow(inset 0 2px 8px rgba(0, 0, 0, .8));
+ display: block;
+}
+
+/* output div while it is collapsed */
+div.output_collapsed {
+ margin: 0px;
+ padding: 0px;
+ .vbox();
+}
+
+div.out_prompt_overlay {
+ height: 100%;
+ padding: 0px @code_padding;
+ position: absolute;
+ .corner-all;
+}
+
+div.out_prompt_overlay:hover {
+ /* use inner shadow to get border that is computed the same on WebKit/FF */
+ .box-shadow(inset 0 0 1px #000);
+ background: rgba(240, 240, 240, 0.5);
+}
+
+div.output_prompt {
+ color: @output_prompt_color;
+}
+
+/* This class is the outer container of all output sections. */
+div.output_area {
+ padding: 0px;
+ page-break-inside: avoid;
+ .hbox();
+
+ .MathJax_Display {
+ // Inside a CodeCell, elements are left justified
+ text-align: left !important;
+ }
+
+ .rendered_html {
+ // Inside a CodeCell, elements are left justified
+ table {
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ img {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+
+ img, svg {
+ max-width: 100%;
+ height: auto;
+ &.unconfined {
+ max-width: none;
+ }
+ }
+}
+
+/* This is needed to protect the pre formating from global settings such
+ as that of bootstrap */
+.output {
+ .vbox();
+}
+
+@media (max-width: @screen-xs-min) {
+ // move prompts above output on small screens
+ div.output_area {
+ .vbox();
+ }
+}
+
+div.output_area pre {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ vertical-align: baseline;
+ color: @output_pre_color;
+ background-color: transparent;
+ .border-radius(0);
+}
+
+/* This class is for the output subarea inside the output_area and after
+ the prompt div. */
+div.output_subarea {
+ // Don't let contents overflow page area.
+ overflow-x: auto;
+ padding: @code_padding;
+ .box-flex1();
+ // appears to be needed for max-width in children to mean anything on Firefox:
+ max-width: calc(~"100% - 14ex");
+}
+
+div.output_scroll div.output_subarea {
+ // if the output area is scrolling, we don't need scrollbars on the subarea
+ overflow-x: visible;
+}
+
+/* The rest of the output_* classes are for special styling of the different
+ output types */
+
+/* all text output has this class: */
+div.output_text {
+ text-align: left;
+ color: @text-color;
+ /* This has to match that of the the CodeMirror class line-height below */
+ line-height: @code_line_height;
+}
+
+/* stdout/stderr are 'text' as well as 'stream', but execute_result/error are *not* streams */
+div.output_stream {
+}
+
+div.output_stdout {
+}
+
+div.output_stderr {
+ background: #fdd; /* very light red background for stderr */
+}
+
+div.output_latex {
+ text-align: left;
+}
+
+div.output_html {
+}
+
+div.output_png {
+}
+
+div.output_jpeg {
+}
+
+/* Empty output_javascript divs should have no height */
+div.output_javascript:empty {
+ padding: 0;
+}
+
+.js-error {
+ color: darkred;
+}
+
+/* raw_input styles */
+
+div.raw_input_container {
+ line-height: @code_line_height;
+ // for some reason, em padding doesn't compute the same for raw_input
+ // that is not the first input, but px does
+ padding-top: 5px;
+}
+
+pre.raw_input_prompt {
+ /* nothing needed here. */
+}
+
+input.raw_input {
+ font-family: @font-family-monospace;
+ font-size: inherit;
+ color: inherit;
+ width: auto;
+ /* make sure input baseline aligns with prompt */
+ vertical-align: baseline;
+ /* padding + margin = 0.5em between prompt and cursor */
+ padding: 0em 0.25em;
+ margin: 0em 0.25em;
+}
+
+input.raw_input:focus {
+ box-shadow: none;
+}
+
+p.p-space {
+ margin-bottom: 10px;
+}
+
+div.output_unrecognized {
+ padding: 5px;
+ font-weight: bold;
+ color: red;
+ // remove decoration from link
+ a {
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ color: inherit;
+ text-decoration: none;
+ }
+ }
+}
diff --git a/notebook/static/notebook/less/pager.less b/notebook/static/notebook/less/pager.less
new file mode 100644
index 0000000..605d136
--- /dev/null
+++ b/notebook/static/notebook/less/pager.less
@@ -0,0 +1,69 @@
+div#pager {
+ background-color: @body-bg;
+ font-size: @notebook_font_size;
+ line-height: @notebook_line_height;
+ overflow: hidden;
+ display: none;
+ position: fixed;
+ bottom: 0px;
+ width: 100%;
+ max-height: 50%;
+ padding-top: 8px;
+
+ .box-shadow(@global-shadow);
+
+ /* Display over codemirror */
+ z-index: 100;
+
+ /* Hack which prevents jquery ui resizable from changing top. */
+ top: auto !important;
+
+ pre {
+ line-height: @code_line_height;
+ color: @text-color;
+ background-color: @cell_background;
+ padding: @code_padding;
+ }
+
+ #pager-button-area {
+ position: absolute;
+ top: 8px;
+ right: 20px;
+ }
+
+ #pager-contents {
+ position: relative;
+ overflow: auto;
+ width: 100%;
+ height: 100%;
+
+ #pager-container {
+ position: relative;
+ padding: 15px 0px;
+ .border-box-sizing();
+ }
+ }
+
+ .ui-resizable-handle {
+ top: 0px;
+ height: 8px;
+ background: @cell_background;
+ border-top: 1px solid @light_border_color;
+ border-bottom: 1px solid @light_border_color;
+
+ /* This injects handle bars (a short, wide = symbol) for
+ the resize handle. */
+ &::after {
+ content: '';
+
+ top: 2px;
+ left: 50%;
+ height: 3px;
+ width: 30px;
+ margin-left: -15px;
+ position: absolute;
+
+ border-top: 1px solid @light_border_color;
+ }
+ }
+}
diff --git a/notebook/static/notebook/less/quickhelp.less b/notebook/static/notebook/less/quickhelp.less
new file mode 100644
index 0000000..310185c
--- /dev/null
+++ b/notebook/static/notebook/less/quickhelp.less
@@ -0,0 +1,15 @@
+.quickhelp {
+ .hbox();
+ line-height: 1.8em;
+}
+.shortcut_key {
+ display: inline-block;
+ width: 21ex;
+ text-align: right;
+ font-family: @font-family-monospace;
+}
+
+.shortcut_descr {
+ display: inline-block;
+ .box-flex1();
+}
diff --git a/notebook/static/notebook/less/renderedhtml.less b/notebook/static/notebook/less/renderedhtml.less
new file mode 100644
index 0000000..d0ceb8b
--- /dev/null
+++ b/notebook/static/notebook/less/renderedhtml.less
@@ -0,0 +1,93 @@
+.rendered_html {
+
+ color: @text-color;
+ em {font-style: italic;}
+ strong {font-weight: bold;}
+ u {text-decoration: underline;}
+ :link {text-decoration: underline;}
+ :visited {text-decoration: underline;}
+
+ // For a 14px base font size this goes as:
+ // font-size = 26, 22, 18, 14, 12, 12
+ // margin-top = 14, 14, 14, 14, 8, 8
+ h1 {font-size: 185.7%; margin: 1.08em 0 0 0; font-weight: bold; line-height: 1.0;}
+ h2 {font-size: 157.1%; margin: 1.27em 0 0 0; font-weight: bold; line-height: 1.0;}
+ h3 {font-size: 128.6%; margin: 1.55em 0 0 0; font-weight: bold; line-height: 1.0;}
+ h4 {font-size: 100%; margin: 2em 0 0 0; font-weight: bold; line-height: 1.0;}
+ h5 {font-size: 100%; margin: 2em 0 0 0; font-weight: bold; line-height: 1.0; font-style: italic;}
+ h6 {font-size: 100%; margin: 2em 0 0 0; font-weight: bold; line-height: 1.0; font-style: italic;}
+
+ // Reduce the top margins by 14px compared to above
+ h1:first-child {margin-top: 0.538em;}
+ h2:first-child {margin-top: 0.636em;}
+ h3:first-child {margin-top: 0.777em;}
+ h4:first-child {margin-top: 1em;}
+ h5:first-child {margin-top: 1em;}
+ h6:first-child {margin-top: 1em;}
+
+ ul {list-style:disc; margin: 0em 2em; padding-left: 0px;}
+ ul ul {list-style:square; margin: 0em 2em;}
+ ul ul ul {list-style:circle; margin: 0em 2em;}
+ ol {list-style:decimal; margin: 0em 2em; padding-left: 0px;}
+ ol ol {list-style:upper-alpha; margin: 0em 2em;}
+ ol ol ol {list-style:lower-alpha; margin: 0em 2em;}
+ ol ol ol ol {list-style:lower-roman; margin: 0em 2em;}
+ /* any extras will just be numbers: */
+ ol ol ol ol ol {list-style:decimal; margin: 0em 2em;}
+ * + ul {margin-top: 1em;}
+ * + ol {margin-top: 1em;}
+
+ hr {
+ color: @rendered_html_border_color;
+ background-color: @rendered_html_border_color;
+ }
+
+ pre {margin: 1em 2em;}
+
+ pre, code {
+ border: 0;
+ background-color: @body-bg;
+ color: @text-color;
+ font-size: 100%;
+ padding: 0px;
+ }
+
+ blockquote {margin: 1em 2em;}
+
+ table {
+ margin-left: auto;
+ margin-right: auto;
+ border: 1px solid @rendered_html_border_color;
+ border-collapse: collapse;
+ }
+ tr, th, td {
+ border: 1px solid @rendered_html_border_color;
+ border-collapse: collapse;
+ margin: 1em 2em;
+ }
+ td, th {
+ text-align: left;
+ vertical-align: middle;
+ padding: 4px;
+ }
+ th {font-weight: bold;}
+ * + table {margin-top: 1em;}
+
+ p {text-align: left;}
+ * + p {margin-top: 1em;}
+
+ img {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ * + img {margin-top: 1em;}
+
+ img, svg {
+ max-width: 100%;
+ height: auto;
+ &.unconfined {
+ max-width: none;
+ }
+ }
+}
diff --git a/notebook/static/notebook/less/savewidget.less b/notebook/static/notebook/less/savewidget.less
new file mode 100644
index 0000000..889e7db
--- /dev/null
+++ b/notebook/static/notebook/less/savewidget.less
@@ -0,0 +1,43 @@
+span.save_widget {
+ margin-top: 6px;
+
+ span.filename {
+ height: 1em;
+ line-height: 1em;
+ padding: 3px;
+ margin-left: @padding-large-horizontal;
+ border: none;
+ font-size: 146.5%;
+ &:hover{
+ // ensure body is lighter on dark palette,
+ // and vice versa
+ background-color:contrast(@body-bg, lighten(@body-bg,30%), darken(@body-bg,10%));
+ }
+ .corner-all;
+ }
+}
+
+span.checkpoint_status, span.autosave_status {
+ font-size: small;
+}
+
+@media (max-width: @screen-xs-max) {
+ span.save_widget {
+ font-size: small;
+ }
+ span.checkpoint_status, span.autosave_status {
+ display: none;
+ }
+}
+
+@media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {
+ span.checkpoint_status {
+ display: none;
+ }
+ span.autosave_status {
+ font-size: x-small;
+ }
+}
+
+
+
diff --git a/notebook/static/notebook/less/searchandreplace.less b/notebook/static/notebook/less/searchandreplace.less
new file mode 100644
index 0000000..01cedee
--- /dev/null
+++ b/notebook/static/notebook/less/searchandreplace.less
@@ -0,0 +1,37 @@
+
+#find-and-replace {
+
+ #replace-preview .match, #replace-preview .insert {
+ background-color: #BBDEFB; // MD Blue 100
+ border-color: #90CAF9; // MD Blue 200
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 0px;
+ }
+
+ #replace-preview .replace {
+
+ & .match {
+ // text-decoration: line-through;
+ background-color: #FFCDD2; // MD Red 100
+ border-color: #EF9A9A; // MD Red 200
+ border-radius: 0px;
+
+ }
+
+ & .insert {
+ background-color: #C8E6C9; // MD Green 100
+ border-color: #A5D6A7; // MD Green 200
+ border-radius: 0px;
+
+ }
+ }
+
+ #replace-preview {
+ max-height: 60vh;
+ overflow: auto;
+ pre {
+ padding: 5px 10px;
+ }
+ }
+}
diff --git a/notebook/static/notebook/less/style.less b/notebook/static/notebook/less/style.less
new file mode 100644
index 0000000..3d26c90
--- /dev/null
+++ b/notebook/static/notebook/less/style.less
@@ -0,0 +1,19 @@
+@import "style_noapp.less";
+
+/*!
+*
+* IPython notebook webapp
+*
+*/
+@import "notebook.less";
+@import "celltoolbar.less";
+@import "completer.less";
+@import "kernelselector.less";
+@import "menubar.less";
+@import "notificationarea.less";
+@import "notificationwidget.less";
+@import "pager.less";
+@import "quickhelp.less";
+@import "savewidget.less";
+@import "toolbar.less";
+@import "tooltip.less";
diff --git a/notebook/static/notebook/less/style_noapp.less b/notebook/static/notebook/less/style_noapp.less
new file mode 100644
index 0000000..f450328
--- /dev/null
+++ b/notebook/static/notebook/less/style_noapp.less
@@ -0,0 +1,14 @@
+/*!
+*
+* IPython notebook
+*
+*/
+@import "variables.less";
+@import "ansicolors.less";
+@import "cell.less";
+@import "codecell.less";
+@import "codemirror.less";
+@import "highlight.less";
+@import "outputarea.less";
+@import "renderedhtml.less";
+@import "textcell.less";
diff --git a/notebook/static/notebook/less/textcell.less b/notebook/static/notebook/less/textcell.less
new file mode 100644
index 0000000..6e3f6ee
--- /dev/null
+++ b/notebook/static/notebook/less/textcell.less
@@ -0,0 +1,72 @@
+div.text_cell {
+ .hbox();
+}
+@media (max-width: @screen-xs-min) {
+ // remove prompt indentation on small screens
+ div.text_cell > div.prompt {
+ display: none;
+ }
+}
+
+div.text_cell_render {
+ /*font-family: "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;*/
+ outline: none;
+ resize: none;
+ width: inherit;
+ border-style: none;
+ padding: 0.5em 0.5em 0.5em @code_padding;
+ color: @text-color;
+ .border-box-sizing();
+}
+
+a.anchor-link:link {
+ text-decoration: none;
+ padding: 0px 20px;
+ visibility: hidden;
+}
+
+h1,h2,h3,h4,h5,h6 {
+ &:hover .anchor-link {
+ visibility: visible;
+ }
+}
+
+.text_cell.rendered .input_area {
+ display: none;
+}
+
+.text_cell.rendered .rendered_html {
+ overflow-x: auto;
+
+ // Content in the y direction should cause the rendered content to grow,
+ // the overflow-x: auto causes chrome to assume the same of y, so we need
+ // to tell it explicitly otherwise.
+ overflow-y: hidden;
+}
+
+.text_cell.unrendered .text_cell_render {
+ display:none;
+}
+
+.cm-header-1,
+.cm-header-2,
+.cm-header-3,
+.cm-header-4,
+.cm-header-5,
+.cm-header-6 {
+ font-weight: bold;
+ font-family: @font-family-sans-serif;
+}
+
+.cm-header-1 { font-size: 185.7%; }
+.cm-header-2 { font-size: 157.1%; }
+.cm-header-3 { font-size: 128.6%; }
+.cm-header-4 { font-size: 110%; }
+.cm-header-5 {
+ font-size: 100%;
+ font-style: italic;
+}
+.cm-header-6 {
+ font-size: 100%;
+ font-style: italic;
+}
diff --git a/notebook/static/notebook/less/toolbar.less b/notebook/static/notebook/less/toolbar.less
new file mode 100644
index 0000000..7430274
--- /dev/null
+++ b/notebook/static/notebook/less/toolbar.less
@@ -0,0 +1,59 @@
+.toolbar {
+ padding: 0px;
+ margin-left: -5px;
+ margin-top: 2px;
+ margin-bottom: 5px;
+
+ select, label {
+ width: auto;
+ vertical-align:middle;
+ margin-right:2px;
+ margin-bottom:0px;
+ display: inline;
+ font-size: 92%;
+ margin-left:0.3em;
+ margin-right:0.3em;
+ padding: 0px;
+ padding-top: 3px;
+ }
+ .btn {
+ padding: 2px 8px;
+ }
+
+ .border-box-sizing();
+}
+
+.toolbar .btn-group {
+ margin-top: 0px;
+ margin-left: 5px;
+}
+
+#maintoolbar {
+ margin-bottom: -3px;
+ margin-top: -8px;
+ border: 0px;
+ min-height: 27px;
+ margin-left: 0px;
+ padding-top: 11px;
+ padding-bottom: 3px;
+
+ .navbar-text {
+ float: none;
+ vertical-align: middle;
+ text-align: right;
+ margin-left: 5px;
+ margin-right: 0px;
+ margin-top: 0px;
+ }
+}
+
+.select-xs {
+ height: @btn_small_height;
+}
+
+
+// highlight the new menu where celltoolbar is
+.pulse, .dropdown-menu > li > a.pulse, li.pulse > a.dropdown-toggle, li.pulse.open > a.dropdown-toggle {
+ background-color: #F37626;
+ color: white;
+}
diff --git a/notebook/static/notebook/less/tooltip.less b/notebook/static/notebook/less/tooltip.less
new file mode 100644
index 0000000..ee372d6
--- /dev/null
+++ b/notebook/static/notebook/less/tooltip.less
@@ -0,0 +1,158 @@
+/**
+ * Primary styles
+ *
+ * Author: Jupyter Development Team
+ */
+
+/** WARNING IF YOU ARE EDITTING THIS FILE, if this is a .css file, It has a lot
+ * of chance of beeing generated from the ../less/[samename].less file, you can
+ * try to get back the less file by reverting somme commit in history
+ **/
+
+/*
+ * We'll try to get something pretty, so we
+ * have some strange css to have the scroll bar on
+ * the left with fix button on the top right of the tooltip
+ */
+
+// double slash comment are remove by less compilation
+// **
+// * Less mixins
+// **/
+
+// Four color of the background
+@import "variables" ;
+
+.dropshadow(){
+ -moz-box-shadow: 0px 6px 10px -1px #adadad;
+ -webkit-box-shadow: 0px 6px 10px -1px #adadad;
+ box-shadow: 0px 6px 10px -1px #adadad;
+}
+
+// smoth height adaptation
+.smoothheight(@t:500ms) {
+ -webkit-transition-property: height;
+ -webkit-transition-duration: @t;
+ -moz-transition-property: height;
+ -moz-transition-duration: @t;
+ transition-property: height;
+ transition-duration: @t;
+}
+
+@-moz-keyframes fadeOut {
+ from {opacity:1;}
+ to {opacity:0;}
+}
+
+@-webkit-keyframes fadeOut {
+ from {opacity:1;}
+ to {opacity:0;}
+}
+
+@-moz-keyframes fadeIn {
+ from {opacity:0;}
+ to {opacity:1;}
+}
+
+@-webkit-keyframes fadeIn {
+ from {opacity:0;}
+ to {opacity:1;}
+}
+
+/*properties of tooltip after "expand"*/
+.bigtooltip {
+ overflow: auto;
+ height: 200px;
+ .smoothheight();
+}
+
+/*properties of tooltip before "expand"*/
+.smalltooltip{
+ .smoothheight();
+ text-overflow: ellipsis;
+ overflow: hidden;
+ height:80px;
+}
+
+.tooltipbuttons
+{
+ position: absolute;
+ padding-right : 15px;
+ top : 0px;
+ right:0px;
+}
+
+.tooltiptext
+{
+ /*avoid the button to overlap on some docstring*/
+ padding-right:30px
+}
+
+.ipython_tooltip {
+ max-width:700px;
+ /*fade-in animation when inserted*/
+ -webkit-animation: fadeOut 400ms;
+ -moz-animation: fadeOut 400ms;
+ animation: fadeOut 400ms;
+ -webkit-animation: fadeIn 400ms;
+ -moz-animation: fadeIn 400ms;
+ animation: fadeIn 400ms;
+ vertical-align: middle;
+ background-color: @cell_background;
+
+ overflow : visible;
+ border: @border_color @border_width solid;
+ outline: none;
+ padding: 3px;
+ margin: 0px;
+ padding-left:7px;
+ font-family: @font-family-monospace;
+ min-height:50px;
+
+ .dropshadow;
+ .corner-all;
+
+ a {
+ float:right;
+ };
+ position: absolute;
+
+ z-index: 1000;
+
+ .tooltiptext {
+ pre {
+ border: 0;
+ .border-radius(0);
+ font-size: 100%;
+ background-color: @cell_background;
+ }
+ }
+}
+
+.pretooltiparrow {
+ left: 0px;
+ margin: 0px;
+ top: -16px;
+ width: 40px;
+ height: 16px;
+ overflow: hidden;
+ position: absolute;
+
+}
+
+.pretooltiparrow:before {
+ background-color : @cell_background;
+ border : @border_width @border_color solid;
+ z-index:11;
+ content: "";
+ position: absolute;
+ left: 15px;
+ top: 10px;
+ width: 25px;
+ height: 25px;
+ @theta : 45deg;
+ -webkit-transform: rotate(@theta);
+ -moz-transform: rotate(@theta);
+ -ms-transform: rotate(@theta);
+ -o-transform: rotate(@theta);
+}
diff --git a/notebook/static/notebook/less/variables.less b/notebook/static/notebook/less/variables.less
new file mode 100644
index 0000000..c1dad69
--- /dev/null
+++ b/notebook/static/notebook/less/variables.less
@@ -0,0 +1,27 @@
+
+// Our own variables for this page
+
+@cell_selected_background: darken(@body-bg, 2%);
+@cell_background: darken(@body-bg, 3.2%);
+@border_color: darken(@cell_selected_background, 31%);
+@light_border_color: darken(@cell_selected_background, 17%);
+@border_width: 1px;
+@notebook_font_size: 14px;
+@notebook_line_height: 20px;
+@code_line_height: 1.21429em; // changed from 1.231 to get 17px even
+@code_padding: 0.4em; // 5.6 px
+@rendered_html_border_color: black;
+@input_prompt_color: #303F9F;
+@output_prompt_color: #D84315;
+@output_pre_color: black;
+@notification_widget_bg: rgba(240, 240, 240, 0.5);
+
+
+@selected_border_color: #42A5F5;
+@selected_border_color_light: #90CAF9;
+@soft_select_color: #E3F2FD;
+
+
+@edit_mode_border_color: #66BB6A;
+@cell_padding: 6px;
+@cell_border_width: 1px;
diff --git a/notebook/static/services/config.js b/notebook/static/services/config.js
new file mode 100644
index 0000000..fbf0f6b
--- /dev/null
+++ b/notebook/static/services/config.js
@@ -0,0 +1,130 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ ],
+function($, utils) {
+ "use strict";
+ var ConfigSection = function(section_name, options) {
+ this.section_name = section_name;
+ this.base_url = options.base_url;
+ this.data = {};
+
+ var that = this;
+
+ /* .loaded is a promise, fulfilled the first time the config is loaded
+ * from the server. Code can do:
+ * conf.loaded.then(function() { ... using conf.data ... });
+ */
+ this._one_load_finished = false;
+ this.loaded = new Promise(function(resolve, reject) {
+ that._finish_firstload = resolve;
+ });
+ };
+
+ ConfigSection.prototype.api_url = function() {
+ return utils.url_path_join(this.base_url, 'api/config',
+ utils.encode_uri_components(this.section_name));
+ };
+
+ ConfigSection.prototype._load_done = function() {
+ if (!this._one_load_finished) {
+ this._one_load_finished = true;
+ this._finish_firstload();
+ }
+ };
+
+ ConfigSection.prototype.load = function() {
+ var that = this;
+ return utils.promising_ajax(this.api_url(), {
+ cache: false,
+ type: "GET",
+ dataType: "json",
+ }).then(function(data) {
+ that.data = data;
+ that._load_done();
+ return data;
+ });
+ };
+
+ /**
+ * Modify the config values stored. Update the local data immediately,
+ * send the change to the server, and use the updated data from the server
+ * when the reply comes.
+ */
+ ConfigSection.prototype.update = function(newdata) {
+ $.extend(true, this.data, newdata); // true -> recursive update
+
+ var that = this;
+ return utils.promising_ajax(this.api_url(), {
+ processData: false,
+ type : "PATCH",
+ data: JSON.stringify(newdata),
+ dataType : "json",
+ contentType: 'application/json',
+ }).then(function(data) {
+ that.data = data;
+ that._load_done();
+ return data;
+ });
+ };
+
+
+ var ConfigWithDefaults = function(section, defaults, classname) {
+ this.section = section;
+ this.defaults = defaults;
+ this.classname = classname;
+ };
+
+ ConfigWithDefaults.prototype._class_data = function() {
+ if (this.classname) {
+ return this.section.data[this.classname] || {};
+ } else {
+ return this.section.data
+ }
+ };
+
+ /**
+ * Wait for config to have loaded, then get a value or the default.
+ * Returns a promise.
+ */
+ ConfigWithDefaults.prototype.get = function(key) {
+ var that = this;
+ return this.section.loaded.then(function() {
+ return that._class_data()[key] || that.defaults[key]
+ });
+ };
+
+ /**
+ * Return a config value. If config is not yet loaded, return the default
+ * instead of waiting for it to load.
+ */
+ ConfigWithDefaults.prototype.get_sync = function(key) {
+ return this._class_data()[key] || this.defaults[key];
+ };
+
+ /**
+ * Set a config value. Send the update to the server, and change our
+ * local copy of the data immediately.
+ * Returns a promise which is fulfilled when the server replies to the
+ * change.
+ */
+ ConfigWithDefaults.prototype.set = function(key, value) {
+ var d = {};
+ d[key] = value;
+ if (this.classname) {
+ var d2 = {};
+ d2[this.classname] = d;
+ return this.section.update(d2);
+ } else {
+ return this.section.update(d);
+ }
+ };
+
+ return {ConfigSection: ConfigSection,
+ ConfigWithDefaults: ConfigWithDefaults,
+ };
+
+});
diff --git a/notebook/static/services/contents.js b/notebook/static/services/contents.js
new file mode 100644
index 0000000..cffc9d2
--- /dev/null
+++ b/notebook/static/services/contents.js
@@ -0,0 +1,258 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define(function(require) {
+ "use strict";
+
+ var $ = require('jquery');
+ var utils = require('base/js/utils');
+
+ var Contents = function(options) {
+ /**
+ * Constructor
+ *
+ * Preliminary documentation for the REST API is at
+ * https://github.com/ipython/ipython/wiki/IPEP-27%3A-Contents-Service
+ *
+ * A contents handles passing file operations
+ * to the back-end. This includes checkpointing
+ * with the normal file operations.
+ *
+ * Parameters:
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * base_url: string
+ */
+ this.base_url = options.base_url;
+ };
+
+ /** Error type */
+ Contents.DIRECTORY_NOT_EMPTY_ERROR = 'DirectoryNotEmptyError';
+
+ Contents.DirectoryNotEmptyError = function() {
+ // Constructor
+ //
+ // An error representing the result of attempting to delete a non-empty
+ // directory.
+ this.message = 'A directory must be empty before being deleted.';
+ };
+
+ Contents.DirectoryNotEmptyError.prototype = Object.create(Error.prototype);
+ Contents.DirectoryNotEmptyError.prototype.name =
+ Contents.DIRECTORY_NOT_EMPTY_ERROR;
+
+
+ Contents.prototype.api_url = function() {
+ var url_parts = [
+ this.base_url, 'api/contents',
+ utils.url_join_encode.apply(null, arguments),
+ ];
+ return utils.url_path_join.apply(null, url_parts);
+ };
+
+ /**
+ * Creates a basic error handler that wraps a jqXHR error as an Error.
+ *
+ * Takes a callback that accepts an Error, and returns a callback that can
+ * be passed directly to $.ajax, which will wrap the error from jQuery
+ * as an Error, and pass that to the original callback.
+ *
+ * @method create_basic_error_handler
+ * @param{Function} callback
+ * @return{Function}
+ */
+ Contents.prototype.create_basic_error_handler = function(callback) {
+ if (!callback) {
+ return utils.log_ajax_error;
+ }
+ return function(xhr, status, error) {
+ callback(utils.wrap_ajax_error(xhr, status, error));
+ };
+ };
+
+ /**
+ * File Functions (including notebook operations)
+ */
+
+ /**
+ * Get a file.
+ *
+ * @method get
+ * @param {String} path
+ * @param {Object} options
+ * type : 'notebook', 'file', or 'directory'
+ * format: 'text' or 'base64'; only relevant for type: 'file'
+ * content: true or false; // whether to include the content
+ */
+ Contents.prototype.get = function (path, options) {
+ /**
+ * We do the call with settings so we can set cache to false.
+ */
+ var settings = {
+ processData : false,
+ cache : false,
+ type : "GET",
+ dataType : "json",
+ };
+ var url = this.api_url(path);
+ var params = {};
+ if (options.type) { params.type = options.type; }
+ if (options.format) { params.format = options.format; }
+ if (options.content === false) { params.content = '0'; }
+ return utils.promising_ajax(url + '?' + $.param(params), settings);
+ };
+
+
+ /**
+ * Creates a new untitled file or directory in the specified directory path.
+ *
+ * @method new
+ * @param {String} path: the directory in which to create the new file/directory
+ * @param {Object} options:
+ * ext: file extension to use
+ * type: model type to create ('notebook', 'file', or 'directory')
+ */
+ Contents.prototype.new_untitled = function(path, options) {
+ var data = JSON.stringify({
+ ext: options.ext,
+ type: options.type
+ });
+
+ var settings = {
+ processData : false,
+ type : "POST",
+ data: data,
+ contentType: 'application/json',
+ dataType : "json",
+ };
+ return utils.promising_ajax(this.api_url(path), settings);
+ };
+
+ Contents.prototype.delete = function(path) {
+ var settings = {
+ processData : false,
+ type : "DELETE",
+ dataType : "json",
+ };
+ var url = this.api_url(path);
+ return utils.promising_ajax(url, settings).catch(
+ // Translate certain errors to more specific ones.
+ function(error) {
+ // TODO: update IPEP27 to specify errors more precisely, so
+ // that error types can be detected here with certainty.
+ if (error.xhr.status === 400) {
+ throw new Contents.DirectoryNotEmptyError();
+ }
+ throw error;
+ }
+ );
+ };
+
+ Contents.prototype.rename = function(path, new_path) {
+ var data = {path: new_path};
+ var settings = {
+ processData : false,
+ type : "PATCH",
+ data : JSON.stringify(data),
+ dataType: "json",
+ contentType: 'application/json',
+ };
+ var url = this.api_url(path);
+ return utils.promising_ajax(url, settings);
+ };
+
+ Contents.prototype.save = function(path, model) {
+ /**
+ * We do the call with settings so we can set cache to false.
+ */
+ var settings = {
+ processData : false,
+ type : "PUT",
+ dataType: "json",
+ data : JSON.stringify(model),
+ contentType: 'application/json',
+ };
+ var url = this.api_url(path);
+ return utils.promising_ajax(url, settings);
+ };
+
+ Contents.prototype.copy = function(from_file, to_dir) {
+ /**
+ * Copy a file into a given directory via POST
+ * The server will select the name of the copied file
+ */
+ var url = this.api_url(to_dir);
+
+ var settings = {
+ processData : false,
+ type: "POST",
+ data: JSON.stringify({copy_from: from_file}),
+ contentType: 'application/json',
+ dataType : "json",
+ };
+ return utils.promising_ajax(url, settings);
+ };
+
+ /**
+ * Checkpointing Functions
+ */
+
+ Contents.prototype.create_checkpoint = function(path) {
+ var url = this.api_url(path, 'checkpoints');
+ var settings = {
+ type : "POST",
+ contentType: false, // no data
+ dataType : "json",
+ };
+ return utils.promising_ajax(url, settings);
+ };
+
+ Contents.prototype.list_checkpoints = function(path) {
+ var url = this.api_url(path, 'checkpoints');
+ var settings = {
+ type : "GET",
+ cache: false,
+ dataType: "json",
+ };
+ return utils.promising_ajax(url, settings);
+ };
+
+ Contents.prototype.restore_checkpoint = function(path, checkpoint_id) {
+ var url = this.api_url(path, 'checkpoints', checkpoint_id);
+ var settings = {
+ type : "POST",
+ contentType: false, // no data
+ };
+ return utils.promising_ajax(url, settings);
+ };
+
+ Contents.prototype.delete_checkpoint = function(path, checkpoint_id) {
+ var url = this.api_url(path, 'checkpoints', checkpoint_id);
+ var settings = {
+ type : "DELETE",
+ };
+ return utils.promising_ajax(url, settings);
+ };
+
+ /**
+ * File management functions
+ */
+
+ /**
+ * List notebooks and directories at a given path
+ *
+ * On success, load_callback is called with an array of dictionaries
+ * representing individual files or directories. Each dictionary has
+ * the keys:
+ * type: "notebook" or "directory"
+ * created: created date
+ * last_modified: last modified dat
+ * @method list_notebooks
+ * @param {String} path The path to list notebooks in
+ */
+ Contents.prototype.list_contents = function(path) {
+ return this.get(path, {type: 'directory'});
+ };
+
+ return {'Contents': Contents};
+});
diff --git a/notebook/static/services/kernels/comm.js b/notebook/static/services/kernels/comm.js
new file mode 100644
index 0000000..85065dd
--- /dev/null
+++ b/notebook/static/services/kernels/comm.js
@@ -0,0 +1,216 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+], function($, utils) {
+ "use strict";
+
+ //-----------------------------------------------------------------------
+ // CommManager class
+ //-----------------------------------------------------------------------
+
+ var CommManager = function (kernel) {
+ this.comms = {};
+ this.targets = {};
+ if (kernel !== undefined) {
+ this.init_kernel(kernel);
+ }
+ };
+
+ CommManager.prototype.init_kernel = function (kernel) {
+ /**
+ * connect the kernel, and register message handlers
+ */
+ this.kernel = kernel;
+ var msg_types = ['comm_open', 'comm_msg', 'comm_close'];
+ for (var i = 0; i < msg_types.length; i++) {
+ var msg_type = msg_types[i];
+ kernel.register_iopub_handler(msg_type, $.proxy(this[msg_type], this));
+ }
+ };
+
+ CommManager.prototype.new_comm = function (target_name, data, callbacks, metadata, comm_id) {
+ /**
+ * Create a new Comm, register it, and open its Kernel-side counterpart
+ * Mimics the auto-registration in `Comm.__init__` in the Jupyter Comm.
+ *
+ * argument comm_id is optional
+ */
+ var comm = new Comm(target_name, comm_id);
+ this.register_comm(comm);
+ comm.open(data, callbacks, metadata);
+ return comm;
+ };
+
+ CommManager.prototype.register_target = function (target_name, f) {
+ /**
+ * Register a target function for a given target name
+ */
+ this.targets[target_name] = f;
+ };
+
+ CommManager.prototype.unregister_target = function (target_name, f) {
+ /**
+ * Unregister a target function for a given target name
+ */
+ delete this.targets[target_name];
+ };
+
+ CommManager.prototype.register_comm = function (comm) {
+ /**
+ * Register a comm in the mapping
+ */
+ this.comms[comm.comm_id] = Promise.resolve(comm);
+ comm.kernel = this.kernel;
+ return comm.comm_id;
+ };
+
+ CommManager.prototype.unregister_comm = function (comm) {
+ /**
+ * Remove a comm from the mapping
+ */
+ delete this.comms[comm.comm_id];
+ };
+
+ // comm message handlers
+
+ CommManager.prototype.comm_open = function (msg) {
+ var content = msg.content;
+ var that = this;
+ var comm_id = content.comm_id;
+
+ this.comms[comm_id] = utils.load_class(content.target_name, content.target_module,
+ this.targets).then(function(target) {
+ var comm = new Comm(content.target_name, comm_id);
+ comm.kernel = that.kernel;
+ try {
+ var response = target(comm, msg);
+ } catch (e) {
+ comm.close();
+ that.unregister_comm(comm);
+ var wrapped_error = new utils.WrappedError("Exception opening new comm", e);
+ console.error(wrapped_error);
+ return Promise.reject(wrapped_error);
+ }
+ // Regardless of the target return value, we need to
+ // then return the comm
+ return Promise.resolve(response).then(function() {return comm;});
+ }, utils.reject('Could not open comm', true));
+ return this.comms[comm_id];
+ };
+
+ CommManager.prototype.comm_close = function(msg) {
+ var content = msg.content;
+ if (this.comms[content.comm_id] === undefined) {
+ console.error('Comm promise not found for comm id ' + content.comm_id);
+ return;
+ }
+ var that = this;
+ this.comms[content.comm_id] = this.comms[content.comm_id].then(function(comm) {
+ that.unregister_comm(comm);
+ try {
+ comm.handle_close(msg);
+ } catch (e) {
+ console.log("Exception closing comm: ", e, e.stack, msg);
+ }
+ // don't return a comm, so that further .then() functions
+ // get an undefined comm input
+ });
+ return this.comms[content.comm_id];
+ };
+
+ CommManager.prototype.comm_msg = function(msg) {
+ var content = msg.content;
+ if (this.comms[content.comm_id] === undefined) {
+ console.error('Comm promise not found for comm id ' + content.comm_id);
+ return;
+ }
+
+ this.comms[content.comm_id] = this.comms[content.comm_id].then(function(comm) {
+ try {
+ comm.handle_msg(msg);
+ } catch (e) {
+ console.log("Exception handling comm msg: ", e, e.stack, msg);
+ }
+ return comm;
+ });
+ return this.comms[content.comm_id];
+ };
+
+ //-----------------------------------------------------------------------
+ // Comm base class
+ //-----------------------------------------------------------------------
+
+ var Comm = function (target_name, comm_id) {
+ this.target_name = target_name;
+ this.comm_id = comm_id || utils.uuid();
+ this._msg_callback = this._close_callback = null;
+ };
+
+ // methods for sending messages
+ Comm.prototype.open = function (data, callbacks, metadata) {
+ var content = {
+ comm_id : this.comm_id,
+ target_name : this.target_name,
+ data : data || {},
+ };
+ return this.kernel.send_shell_message("comm_open", content, callbacks, metadata);
+ };
+
+ Comm.prototype.send = function (data, callbacks, metadata, buffers) {
+ var content = {
+ comm_id : this.comm_id,
+ data : data || {},
+ };
+ return this.kernel.send_shell_message("comm_msg", content, callbacks, metadata, buffers);
+ };
+
+ Comm.prototype.close = function (data, callbacks, metadata) {
+ var content = {
+ comm_id : this.comm_id,
+ data : data || {},
+ };
+ return this.kernel.send_shell_message("comm_close", content, callbacks, metadata);
+ };
+
+ // methods for registering callbacks for incoming messages
+ Comm.prototype._register_callback = function (key, callback) {
+ this['_' + key + '_callback'] = callback;
+ };
+
+ Comm.prototype.on_msg = function (callback) {
+ this._register_callback('msg', callback);
+ };
+
+ Comm.prototype.on_close = function (callback) {
+ this._register_callback('close', callback);
+ };
+
+ // methods for handling incoming messages
+
+ Comm.prototype._callback = function (key, msg) {
+ var callback = this['_' + key + '_callback'];
+ if (callback) {
+ try {
+ callback(msg);
+ } catch (e) {
+ console.log("Exception in Comm callback", e, e.stack, msg);
+ }
+ }
+ };
+
+ Comm.prototype.handle_msg = function (msg) {
+ this._callback('msg', msg);
+ };
+
+ Comm.prototype.handle_close = function (msg) {
+ this._callback('close', msg);
+ };
+
+ return {
+ 'CommManager': CommManager,
+ 'Comm': Comm
+ };
+});
diff --git a/notebook/static/services/kernels/kernel.js b/notebook/static/services/kernels/kernel.js
new file mode 100644
index 0000000..c8d0c75
--- /dev/null
+++ b/notebook/static/services/kernels/kernel.js
@@ -0,0 +1,1112 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ './comm',
+ './serialize',
+ 'base/js/events'
+], function($, utils, comm, serialize, events) {
+ "use strict";
+
+ /**
+ * A Kernel class to communicate with the Python kernel. This
+ * should generally not be constructed directly, but be created
+ * by. the `Session` object. Once created, this object should be
+ * used to communicate with the kernel.
+ *
+ * Preliminary documentation for the REST API is at
+ * https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#kernels-api
+ *
+ * @class Kernel
+ * @param {string} kernel_service_url - the URL to access the kernel REST api
+ * @param {string} ws_url - the websockets URL
+ * @param {string} name - the kernel type (e.g. python3)
+ */
+ var Kernel = function (kernel_service_url, ws_url, name) {
+ this.events = events;
+
+ this.id = null;
+ this.name = name;
+ this.ws = null;
+
+ this.kernel_service_url = kernel_service_url;
+ this.kernel_url = null;
+ this.ws_url = ws_url || utils.get_body_data("wsUrl");
+ if (!this.ws_url) {
+ // trailing 's' in https will become wss for secure web sockets
+ this.ws_url = location.protocol.replace('http', 'ws') + "//" + location.host;
+ }
+
+ this.username = "username";
+ this.session_id = utils.uuid();
+ this._msg_callbacks = {};
+ this._msg_queue = Promise.resolve();
+ this.info_reply = {}; // kernel_info_reply stored here after starting
+
+ if (typeof(WebSocket) !== 'undefined') {
+ this.WebSocket = WebSocket;
+ } else if (typeof(MozWebSocket) !== 'undefined') {
+ this.WebSocket = MozWebSocket;
+ } else {
+ alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox ≥ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
+ }
+
+ this.bind_events();
+ this.init_iopub_handlers();
+ this.comm_manager = new comm.CommManager(this);
+
+ this.last_msg_id = null;
+ this.last_msg_callbacks = {};
+
+ this._autorestart_attempt = 0;
+ this._reconnect_attempt = 0;
+ this.reconnect_limit = 7;
+
+ this._pending_messages = [];
+ };
+
+ /**
+ * @function _get_msg
+ */
+ Kernel.prototype._get_msg = function (msg_type, content, metadata, buffers) {
+ var msg = {
+ header : {
+ msg_id : utils.uuid(),
+ username : this.username,
+ session : this.session_id,
+ msg_type : msg_type,
+ version : "5.0"
+ },
+ metadata : metadata || {},
+ content : content,
+ buffers : buffers || [],
+ parent_header : {}
+ };
+ return msg;
+ };
+
+ /**
+ * @function bind_events
+ */
+ Kernel.prototype.bind_events = function () {
+ var that = this;
+ this.events.on('send_input_reply.Kernel', function(evt, data) {
+ that.send_input_reply(data);
+ });
+
+ var record_status = function (evt, info) {
+ console.log('Kernel: ' + evt.type + ' (' + info.kernel.id + ')');
+ };
+
+ this.events.on('kernel_created.Kernel', record_status);
+ this.events.on('kernel_reconnecting.Kernel', record_status);
+ this.events.on('kernel_connected.Kernel', record_status);
+ this.events.on('kernel_starting.Kernel', record_status);
+ this.events.on('kernel_restarting.Kernel', record_status);
+ this.events.on('kernel_autorestarting.Kernel', record_status);
+ this.events.on('kernel_interrupting.Kernel', record_status);
+ this.events.on('kernel_disconnected.Kernel', record_status);
+ // these are commented out because they are triggered a lot, but can
+ // be uncommented for debugging purposes
+ //this.events.on('kernel_idle.Kernel', record_status);
+ //this.events.on('kernel_busy.Kernel', record_status);
+ this.events.on('kernel_ready.Kernel', record_status);
+ this.events.on('kernel_killed.Kernel', record_status);
+ this.events.on('kernel_dead.Kernel', record_status);
+
+ this.events.on('kernel_ready.Kernel', function () {
+ that._autorestart_attempt = 0;
+ });
+ this.events.on('kernel_connected.Kernel', function () {
+ that._reconnect_attempt = 0;
+ });
+ };
+
+ /**
+ * Initialize the iopub handlers.
+ *
+ * @function init_iopub_handlers
+ */
+ Kernel.prototype.init_iopub_handlers = function () {
+ var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
+ this._iopub_handlers = {};
+ this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
+ this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
+ this.register_iopub_handler('execute_input', $.proxy(this._handle_input_message, this));
+
+ for (var i=0; i < output_msg_types.length; i++) {
+ this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
+ }
+ };
+
+ /**
+ * GET /api/kernels
+ *
+ * Get the list of running kernels.
+ *
+ * @function list
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Kernel.prototype.list = function (success, error) {
+ $.ajax(this.kernel_service_url, {
+ processData: false,
+ cache: false,
+ type: "GET",
+ dataType: "json",
+ success: success,
+ error: this._on_error(error)
+ });
+ };
+
+ /**
+ * POST /api/kernels
+ *
+ * Start a new kernel.
+ *
+ * In general this shouldn't be used -- the kernel should be
+ * started through the session API. If you use this function and
+ * are also using the session API then your session and kernel
+ * WILL be out of sync!
+ *
+ * @function start
+ * @param {params} [Object] - parameters to include in the query string
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Kernel.prototype.start = function (params, success, error) {
+ var url = this.kernel_service_url;
+ var qs = $.param(params || {}); // query string for sage math stuff
+ if (qs !== "") {
+ url = url + "?" + qs;
+ }
+
+ this.events.trigger('kernel_starting.Kernel', {kernel: this});
+ var that = this;
+ var on_success = function (data, status, xhr) {
+ that.events.trigger('kernel_created.Kernel', {kernel: that});
+ that._kernel_created(data);
+ if (success) {
+ success(data, status, xhr);
+ }
+ };
+
+ $.ajax(url, {
+ processData: false,
+ cache: false,
+ type: "POST",
+ data: JSON.stringify({name: this.name}),
+ contentType: 'application/json',
+ dataType: "json",
+ success: this._on_success(on_success),
+ error: this._on_error(error)
+ });
+
+ return url;
+ };
+
+ /**
+ * GET /api/kernels/[:kernel_id]
+ *
+ * Get information about the kernel.
+ *
+ * @function get_info
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Kernel.prototype.get_info = function (success, error) {
+ $.ajax(this.kernel_url, {
+ processData: false,
+ cache: false,
+ type: "GET",
+ dataType: "json",
+ success: this._on_success(success),
+ error: this._on_error(error)
+ });
+ };
+
+ /**
+ * DELETE /api/kernels/[:kernel_id]
+ *
+ * Shutdown the kernel.
+ *
+ * If you are also using sessions, then this function shoul NOT be
+ * used. Instead, use Session.delete. Otherwise, the session and
+ * kernel WILL be out of sync.
+ *
+ * @function kill
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Kernel.prototype.kill = function (success, error) {
+ this.events.trigger('kernel_killed.Kernel', {kernel: this});
+ this._kernel_dead();
+ $.ajax(this.kernel_url, {
+ processData: false,
+ cache: false,
+ type: "DELETE",
+ dataType: "json",
+ success: this._on_success(success),
+ error: this._on_error(error)
+ });
+ };
+
+ /**
+ * POST /api/kernels/[:kernel_id]/interrupt
+ *
+ * Interrupt the kernel.
+ *
+ * @function interrupt
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Kernel.prototype.interrupt = function (success, error) {
+ this.events.trigger('kernel_interrupting.Kernel', {kernel: this});
+
+ var that = this;
+ var on_success = function (data, status, xhr) {
+ /**
+ * get kernel info so we know what state the kernel is in
+ */
+ that.kernel_info();
+ if (success) {
+ success(data, status, xhr);
+ }
+ };
+
+ var url = utils.url_path_join(this.kernel_url, 'interrupt');
+ $.ajax(url, {
+ processData: false,
+ cache: false,
+ type: "POST",
+ contentType: false, // there's no data with this
+ dataType: "json",
+ success: this._on_success(on_success),
+ error: this._on_error(error)
+ });
+ };
+
+ Kernel.prototype.restart = function (success, error) {
+ /**
+ * POST /api/kernels/[:kernel_id]/restart
+ *
+ * Restart the kernel.
+ *
+ * @function interrupt
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ this.events.trigger('kernel_restarting.Kernel', {kernel: this});
+ this.stop_channels();
+
+ var that = this;
+ var on_success = function (data, status, xhr) {
+ that.events.trigger('kernel_created.Kernel', {kernel: that});
+ that._kernel_created(data);
+ if (success) {
+ success(data, status, xhr);
+ }
+ };
+
+ var on_error = function (xhr, status, err) {
+ that.events.trigger('kernel_dead.Kernel', {kernel: that});
+ that._kernel_dead();
+ if (error) {
+ error(xhr, status, err);
+ }
+ };
+
+ var url = utils.url_path_join(this.kernel_url, 'restart');
+ $.ajax(url, {
+ processData: false,
+ cache: false,
+ type: "POST",
+ contentType: false, // there's no data with this
+ dataType: "json",
+ success: this._on_success(on_success),
+ error: this._on_error(on_error)
+ });
+ };
+
+ Kernel.prototype.reconnect = function () {
+ /**
+ * Reconnect to a disconnected kernel. This is not actually a
+ * standard HTTP request, but useful function nonetheless for
+ * reconnecting to the kernel if the connection is somehow lost.
+ *
+ * @function reconnect
+ */
+ if (this.is_connected()) {
+ this.stop_channels();
+ }
+ this._reconnect_attempt = this._reconnect_attempt + 1;
+ this.events.trigger('kernel_reconnecting.Kernel', {
+ kernel: this,
+ attempt: this._reconnect_attempt,
+ });
+ this.start_channels();
+ };
+
+ Kernel.prototype._on_success = function (success) {
+ /**
+ * Handle a successful AJAX request by updating the kernel id and
+ * name from the response, and then optionally calling a provided
+ * callback.
+ *
+ * @function _on_success
+ * @param {function} success - callback
+ */
+ var that = this;
+ return function (data, status, xhr) {
+ if (data) {
+ that.id = data.id;
+ that.name = data.name;
+ }
+ that.kernel_url = utils.url_path_join(that.kernel_service_url,
+ encodeURIComponent(that.id));
+ if (success) {
+ success(data, status, xhr);
+ }
+ };
+ };
+
+ Kernel.prototype._on_error = function (error) {
+ /**
+ * Handle a failed AJAX request by logging the error message, and
+ * then optionally calling a provided callback.
+ *
+ * @function _on_error
+ * @param {function} error - callback
+ */
+ return function (xhr, status, err) {
+ utils.log_ajax_error(xhr, status, err);
+ if (error) {
+ error(xhr, status, err);
+ }
+ };
+ };
+
+ Kernel.prototype._kernel_created = function (data) {
+ /**
+ * Perform necessary tasks once the kernel has been started,
+ * including actually connecting to the kernel.
+ *
+ * @function _kernel_created
+ * @param {Object} data - information about the kernel including id
+ */
+ this.id = data.id;
+ this.kernel_url = utils.url_path_join(this.kernel_service_url,
+ encodeURIComponent(this.id));
+ this.start_channels();
+ };
+
+ Kernel.prototype._kernel_connected = function () {
+ /**
+ * Perform necessary tasks once the connection to the kernel has
+ * been established. This includes requesting information about
+ * the kernel.
+ *
+ * @function _kernel_connected
+ */
+ this.events.trigger('kernel_connected.Kernel', {kernel: this});
+
+ // Send pending messages. We shift the message off the queue
+ // after the message is sent so that if there is an exception,
+ // the message is still pending.
+ while (this._pending_messages.length > 0) {
+ this.ws.send(this._pending_messages[0]);
+ this._pending_messages.shift();
+ }
+
+ // get kernel info so we know what state the kernel is in
+ var that = this;
+ this.kernel_info(function (reply) {
+ that.info_reply = reply.content;
+ that.events.trigger('kernel_ready.Kernel', {kernel: that});
+ });
+ };
+
+ Kernel.prototype._kernel_dead = function () {
+ /**
+ * Perform necessary tasks after the kernel has died. This closing
+ * communication channels to the kernel if they are still somehow
+ * open.
+ *
+ * @function _kernel_dead
+ */
+ this.stop_channels();
+ };
+
+ Kernel.prototype.start_channels = function () {
+ /**
+ * Start the websocket channels.
+ * Will stop and restart them if they already exist.
+ *
+ * @function start_channels
+ */
+ var that = this;
+ this.stop_channels();
+ var ws_host_url = this.ws_url + this.kernel_url;
+
+ console.log("Starting WebSockets:", ws_host_url);
+
+ this.ws = new this.WebSocket([
+ that.ws_url,
+ utils.url_path_join(that.kernel_url, 'channels'),
+ "?session_id=" + that.session_id
+ ].join('')
+ );
+
+ var already_called_onclose = false; // only alert once
+ var ws_closed_early = function(evt){
+ if (already_called_onclose){
+ return;
+ }
+ already_called_onclose = true;
+ if ( ! evt.wasClean ){
+ // If the websocket was closed early, that could mean
+ // that the kernel is actually dead. Try getting
+ // information about the kernel from the API call --
+ // if that fails, then assume the kernel is dead,
+ // otherwise just follow the typical websocket closed
+ // protocol.
+ that.get_info(function () {
+ that._ws_closed(ws_host_url, false);
+ }, function () {
+ that.events.trigger('kernel_dead.Kernel', {kernel: that});
+ that._kernel_dead();
+ });
+ }
+ };
+ var ws_closed_late = function(evt){
+ if (already_called_onclose){
+ return;
+ }
+ already_called_onclose = true;
+ if ( ! evt.wasClean ){
+ that._ws_closed(ws_host_url, false);
+ }
+ };
+ var ws_error = function(evt){
+ if (already_called_onclose){
+ return;
+ }
+ already_called_onclose = true;
+ that._ws_closed(ws_host_url, true);
+ };
+
+ this.ws.onopen = $.proxy(this._ws_opened, this);
+ this.ws.onclose = ws_closed_early;
+ this.ws.onerror = ws_error;
+ // switch from early-close to late-close message after 1s
+ setTimeout(function() {
+ if (that.ws !== null) {
+ that.ws.onclose = ws_closed_late;
+ }
+ }, 1000);
+ this.ws.onmessage = $.proxy(this._handle_ws_message, this);
+ };
+
+ Kernel.prototype._ws_opened = function (evt) {
+ /**
+ * Handle a websocket entering the open state,
+ * signaling that the kernel is connected when websocket is open.
+ *
+ * @function _ws_opened
+ */
+ if (this.is_connected()) {
+ // all events ready, trigger started event.
+ this._kernel_connected();
+ }
+ };
+
+ Kernel.prototype._ws_closed = function(ws_url, error) {
+ /**
+ * Handle a websocket entering the closed state. If the websocket
+ * was not closed due to an error, try to reconnect to the kernel.
+ *
+ * @function _ws_closed
+ * @param {string} ws_url - the websocket url
+ * @param {bool} error - whether the connection was closed due to an error
+ */
+ this.stop_channels();
+
+ this.events.trigger('kernel_disconnected.Kernel', {kernel: this});
+ if (error) {
+ console.log('WebSocket connection failed: ', ws_url, error);
+ this.events.trigger('kernel_connection_failed.Kernel', {
+ kernel: this,
+ ws_url: ws_url,
+ attempt: this._reconnect_attempt,
+ error: error,
+ });
+ }
+ this._schedule_reconnect();
+ };
+
+ Kernel.prototype._schedule_reconnect = function () {
+ /**
+ * function to call when kernel connection is lost
+ * schedules reconnect, or fires 'connection_dead' if reconnect limit is hit
+ */
+ if (this._reconnect_attempt < this.reconnect_limit) {
+ var timeout = Math.pow(2, this._reconnect_attempt);
+ console.log("Connection lost, reconnecting in " + timeout + " seconds.");
+ setTimeout($.proxy(this.reconnect, this), 1e3 * timeout);
+ } else {
+ this.events.trigger('kernel_connection_dead.Kernel', {
+ kernel: this,
+ reconnect_attempt: this._reconnect_attempt,
+ });
+ console.log("Failed to reconnect, giving up.");
+ }
+ };
+
+ Kernel.prototype.stop_channels = function () {
+ /**
+ * Close the websocket. After successful close, the value
+ * in `this.ws` will be null.
+ *
+ * @function stop_channels
+ */
+ var that = this;
+ var close = function () {
+ if (that.ws && that.ws.readyState === WebSocket.CLOSED) {
+ that.ws = null;
+ }
+ };
+ if (this.ws !== null) {
+ if (this.ws.readyState === WebSocket.OPEN) {
+ this.ws.onclose = close;
+ this.ws.close();
+ } else {
+ close();
+ }
+ }
+ };
+
+ Kernel.prototype.is_connected = function () {
+ /**
+ * Check whether there is a connection to the kernel. This
+ * function only returns true if websocket has been
+ * created and has a state of WebSocket.OPEN.
+ *
+ * @function is_connected
+ * @returns {bool} - whether there is a connection
+ */
+ // if any channel is not ready, then we're not connected
+ if (this.ws === null) {
+ return false;
+ }
+ if (this.ws.readyState !== WebSocket.OPEN) {
+ return false;
+ }
+ return true;
+ };
+
+ Kernel.prototype.is_fully_disconnected = function () {
+ /**
+ * Check whether the connection to the kernel has been completely
+ * severed. This function only returns true if all channel objects
+ * are null.
+ *
+ * @function is_fully_disconnected
+ * @returns {bool} - whether the kernel is fully disconnected
+ */
+ return (this.ws === null);
+ };
+
+ Kernel.prototype._send = function(msg) {
+ /**
+ * Send a message (if the kernel is connected) or queue the message for future delivery
+ *
+ * Pending messages will automatically be sent when a kernel becomes connected.
+ *
+ * @function _send
+ * @param msg
+ */
+ if (this.is_connected()) {
+ this.ws.send(msg);
+ } else {
+ this._pending_messages.push(msg);
+ }
+ }
+
+ Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) {
+ /**
+ * Send a message on the Kernel's shell channel
+ *
+ * If the kernel is not connected, the message will be buffered.
+ *
+ * @function send_shell_message
+ */
+ var msg = this._get_msg(msg_type, content, metadata, buffers);
+ msg.channel = 'shell';
+ this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
+ this._send(serialize.serialize(msg));
+ return msg.header.msg_id;
+ };
+
+ Kernel.prototype.kernel_info = function (callback) {
+ /**
+ * Get kernel info
+ *
+ * @function kernel_info
+ * @param callback {function}
+ *
+ * When calling this method, pass a callback function that expects one argument.
+ * The callback will be passed the complete `kernel_info_reply` message documented
+ * [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#kernel-info)
+ */
+ var callbacks;
+ if (callback) {
+ callbacks = { shell : { reply : callback } };
+ }
+ return this.send_shell_message("kernel_info_request", {}, callbacks);
+ };
+
+ Kernel.prototype.comm_info = function (target_name, callback) {
+ /**
+ * Get comm info
+ *
+ * @function comm_info
+ * @param callback {function}
+ *
+ * When calling this method, pass a callback function that expects one argument.
+ * The callback will be passed the complete `comm_info_reply` message documented
+ * [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#comm_info)
+ */
+ var callbacks;
+ if (callback) {
+ callbacks = { shell : { reply : callback } };
+ }
+ var content = {
+ target_name : target_name,
+ };
+ return this.send_shell_message("comm_info_request", content, callbacks);
+ };
+
+ Kernel.prototype.inspect = function (code, cursor_pos, callback) {
+ /**
+ * Get info on an object
+ *
+ * When calling this method, pass a callback function that expects one argument.
+ * The callback will be passed the complete `inspect_reply` message documented
+ * [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#object-information)
+ *
+ * @function inspect
+ * @param code {string}
+ * @param cursor_pos {integer}
+ * @param callback {function}
+ */
+ var callbacks;
+ if (callback) {
+ callbacks = { shell : { reply : callback } };
+ }
+
+ var content = {
+ code : code,
+ cursor_pos : cursor_pos,
+ detail_level : 0
+ };
+ return this.send_shell_message("inspect_request", content, callbacks);
+ };
+
+ Kernel.prototype.execute = function (code, callbacks, options) {
+ /**
+ * Execute given code into kernel, and pass result to callback.
+ *
+ * @async
+ * @function execute
+ * @param {string} code
+ * @param [callbacks] {Object} With the following keys (all optional)
+ * @param callbacks.shell.reply {function}
+ * @param callbacks.shell.payload.[payload_name] {function}
+ * @param callbacks.iopub.output {function}
+ * @param callbacks.iopub.clear_output {function}
+ * @param callbacks.input {function}
+ * @param {object} [options]
+ * @param [options.silent=false] {Boolean}
+ * @param [options.user_expressions=empty_dict] {Dict}
+ * @param [options.allow_stdin=false] {Boolean} true|false
+ *
+ * @example
+ *
+ * The options object should contain the options for the execute
+ * call. Its default values are:
+ *
+ * options = {
+ * silent : true,
+ * user_expressions : {},
+ * allow_stdin : false
+ * }
+ *
+ * When calling this method pass a callbacks structure of the
+ * form:
+ *
+ * callbacks = {
+ * shell : {
+ * reply : execute_reply_callback,
+ * payload : {
+ * set_next_input : set_next_input_callback,
+ * }
+ * },
+ * iopub : {
+ * output : output_callback,
+ * clear_output : clear_output_callback,
+ * },
+ * input : raw_input_callback
+ * }
+ *
+ * Each callback will be passed the entire message as a single
+ * arugment. Payload handlers will be passed the corresponding
+ * payload and the execute_reply message.
+ */
+ var content = {
+ code : code,
+ silent : true,
+ store_history : false,
+ user_expressions : {},
+ allow_stdin : false
+ };
+ callbacks = callbacks || {};
+ if (callbacks.input !== undefined) {
+ content.allow_stdin = true;
+ }
+ $.extend(true, content, options);
+ this.events.trigger('execution_request.Kernel', {kernel: this, content: content});
+ return this.send_shell_message("execute_request", content, callbacks);
+ };
+
+ /**
+ * When calling this method, pass a function to be called with the
+ * `complete_reply` message as its only argument when it arrives.
+ *
+ * `complete_reply` is documented
+ * [here](https://jupyter-client.readthedocs.io/en/latest/messaging.html#complete)
+ *
+ * @function complete
+ * @param code {string}
+ * @param cursor_pos {integer}
+ * @param callback {function}
+ */
+ Kernel.prototype.complete = function (code, cursor_pos, callback) {
+ var callbacks;
+ if (callback) {
+ callbacks = { shell : { reply : callback } };
+ }
+ var content = {
+ code : code,
+ cursor_pos : cursor_pos
+ };
+ return this.send_shell_message("complete_request", content, callbacks);
+ };
+
+ /**
+ * @function send_input_reply
+ */
+ Kernel.prototype.send_input_reply = function (input) {
+ var content = {
+ value : input
+ };
+ this.events.trigger('input_reply.Kernel', {kernel: this, content: content});
+ var msg = this._get_msg("input_reply", content);
+ msg.channel = 'stdin';
+ this._send(serialize.serialize(msg));
+ return msg.header.msg_id;
+ };
+
+ /**
+ * @function register_iopub_handler
+ */
+ Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
+ this._iopub_handlers[msg_type] = callback;
+ };
+
+ /**
+ * Get the iopub handler for a specific message type.
+ *
+ * @function get_iopub_handler
+ */
+ Kernel.prototype.get_iopub_handler = function (msg_type) {
+ return this._iopub_handlers[msg_type];
+ };
+
+ /**
+ * Get callbacks for a specific message.
+ *
+ * @function get_callbacks_for_msg
+ */
+ Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
+ if (msg_id == this.last_msg_id) {
+ return this.last_msg_callbacks;
+ } else {
+ return this._msg_callbacks[msg_id];
+ }
+ };
+
+ /**
+ * Clear callbacks for a specific message.
+ *
+ * @function clear_callbacks_for_msg
+ */
+ Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
+ if (this._msg_callbacks[msg_id] !== undefined ) {
+ delete this._msg_callbacks[msg_id];
+ }
+ };
+
+ /**
+ * @function _finish_shell
+ */
+ Kernel.prototype._finish_shell = function (msg_id) {
+ var callbacks = this._msg_callbacks[msg_id];
+ if (callbacks !== undefined) {
+ callbacks.shell_done = true;
+ if (callbacks.iopub_done) {
+ this.clear_callbacks_for_msg(msg_id);
+ }
+ }
+ };
+
+ /**
+ * @function _finish_iopub
+ */
+ Kernel.prototype._finish_iopub = function (msg_id) {
+ var callbacks = this._msg_callbacks[msg_id];
+ if (callbacks !== undefined) {
+ callbacks.iopub_done = true;
+ if (callbacks.shell_done) {
+ this.clear_callbacks_for_msg(msg_id);
+ }
+ }
+ };
+
+ /**
+ * Set callbacks for a particular message.
+ * Callbacks should be a struct of the following form:
+ * shell : {
+ *
+ * }
+ *
+ * @function set_callbacks_for_msg
+ */
+ Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
+ this.last_msg_id = msg_id;
+ if (callbacks) {
+ // shallow-copy mapping, because we will modify it at the top level
+ var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
+ cbcopy.shell = callbacks.shell;
+ cbcopy.iopub = callbacks.iopub;
+ cbcopy.input = callbacks.input;
+ cbcopy.shell_done = (!callbacks.shell);
+ cbcopy.iopub_done = (!callbacks.iopub);
+ } else {
+ this.last_msg_callbacks = {};
+ }
+ };
+
+ Kernel.prototype._handle_ws_message = function (e) {
+ var that = this;
+ this._msg_queue = this._msg_queue.then(function() {
+ return serialize.deserialize(e.data);
+ }).then(function(msg) {return that._finish_ws_message(msg);})
+ .catch(function(error) { console.error("Couldn't process kernel message", error); });
+ };
+
+ Kernel.prototype._finish_ws_message = function (msg) {
+ switch (msg.channel) {
+ case 'shell':
+ return this._handle_shell_reply(msg);
+ case 'iopub':
+ return this._handle_iopub_message(msg);
+ case 'stdin':
+ return this._handle_input_request(msg);
+ default:
+ console.error("unrecognized message channel", msg.channel, msg);
+ }
+ };
+
+ Kernel.prototype._handle_shell_reply = function (reply) {
+ this.events.trigger('shell_reply.Kernel', {kernel: this, reply:reply});
+ var that = this;
+ var content = reply.content;
+ var metadata = reply.metadata;
+ var parent_id = reply.parent_header.msg_id;
+ var callbacks = this.get_callbacks_for_msg(parent_id);
+ var promise = Promise.resolve();
+ if (!callbacks || !callbacks.shell) {
+ return;
+ }
+ var shell_callbacks = callbacks.shell;
+
+ // signal that shell callbacks are done
+ this._finish_shell(parent_id);
+
+ if (shell_callbacks.reply !== undefined) {
+ promise = promise.then(function() {return shell_callbacks.reply(reply);});
+ }
+ if (content.payload && shell_callbacks.payload) {
+ promise = promise.then(function() {
+ return that._handle_payloads(content.payload, shell_callbacks.payload, reply);
+ });
+ }
+ return promise;
+ };
+
+ /**
+ * @function _handle_payloads
+ */
+ Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
+ var promise = [];
+ var l = payloads.length;
+ // Payloads are handled by triggering events because we don't want the Kernel
+ // to depend on the Notebook or Pager classes.
+ for (var i=0; i<l; i++) {
+ var payload = payloads[i];
+ var callback = payload_callbacks[payload.source];
+ if (callback) {
+ promise.push(callback(payload, msg));
+ }
+ }
+ return Promise.all(promise);
+ };
+
+ /**
+ * @function _handle_status_message
+ */
+ Kernel.prototype._handle_status_message = function (msg) {
+ var execution_state = msg.content.execution_state;
+ var parent_id = msg.parent_header.msg_id;
+
+ // dispatch status msg callbacks, if any
+ var callbacks = this.get_callbacks_for_msg(parent_id);
+ if (callbacks && callbacks.iopub && callbacks.iopub.status) {
+ try {
+ callbacks.iopub.status(msg);
+ } catch (e) {
+ console.log("Exception in status msg handler", e, e.stack);
+ }
+ }
+
+ if (execution_state === 'busy') {
+ this.events.trigger('kernel_busy.Kernel', {kernel: this});
+
+ } else if (execution_state === 'idle') {
+ // signal that iopub callbacks are (probably) done
+ // async output may still arrive,
+ // but only for the most recent request
+ this._finish_iopub(parent_id);
+
+ // trigger status_idle event
+ this.events.trigger('kernel_idle.Kernel', {kernel: this});
+
+ } else if (execution_state === 'starting') {
+ this.events.trigger('kernel_starting.Kernel', {kernel: this});
+ var that = this;
+ this.kernel_info(function (reply) {
+ that.info_reply = reply.content;
+ that.events.trigger('kernel_ready.Kernel', {kernel: that});
+ });
+
+ } else if (execution_state === 'restarting') {
+ // autorestarting is distinct from restarting,
+ // in that it means the kernel died and the server is restarting it.
+ // kernel_restarting sets the notification widget,
+ // autorestart shows the more prominent dialog.
+ this._autorestart_attempt = this._autorestart_attempt + 1;
+ this.events.trigger('kernel_restarting.Kernel', {kernel: this});
+ this.events.trigger('kernel_autorestarting.Kernel', {kernel: this, attempt: this._autorestart_attempt});
+
+ } else if (execution_state === 'dead') {
+ this.events.trigger('kernel_dead.Kernel', {kernel: this});
+ this._kernel_dead();
+ }
+ };
+
+ /**
+ * Handle clear_output message
+ *
+ * @function _handle_clear_output
+ */
+ Kernel.prototype._handle_clear_output = function (msg) {
+ var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
+ if (!callbacks || !callbacks.iopub) {
+ return;
+ }
+ var callback = callbacks.iopub.clear_output;
+ if (callback) {
+ callback(msg);
+ }
+ };
+
+ /**
+ * handle an output message (execute_result, display_data, etc.)
+ *
+ * @function _handle_output_message
+ */
+ Kernel.prototype._handle_output_message = function (msg) {
+ var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
+ if (!callbacks || !callbacks.iopub) {
+ // The message came from another client. Let the UI decide what to
+ // do with it.
+ this.events.trigger('received_unsolicited_message.Kernel', msg);
+ return;
+ }
+ var callback = callbacks.iopub.output;
+ if (callback) {
+ callback(msg);
+ }
+ };
+
+ /**
+ * Handle an input message (execute_input).
+ *
+ * @function _handle_input message
+ */
+ Kernel.prototype._handle_input_message = function (msg) {
+ var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
+ if (!callbacks) {
+ // The message came from another client. Let the UI decide what to
+ // do with it.
+ this.events.trigger('received_unsolicited_message.Kernel', msg);
+ }
+ };
+
+ /**
+ * Dispatch IOPub messages to respective handlers. Each message
+ * type should have a handler.
+ *
+ * @function _handle_iopub_message
+ */
+ Kernel.prototype._handle_iopub_message = function (msg) {
+ var handler = this.get_iopub_handler(msg.header.msg_type);
+ if (handler !== undefined) {
+ return handler(msg);
+ }
+ };
+
+ /**
+ * @function _handle_input_request
+ */
+ Kernel.prototype._handle_input_request = function (request) {
+ var header = request.header;
+ var content = request.content;
+ var metadata = request.metadata;
+ var msg_type = header.msg_type;
+ if (msg_type !== 'input_request') {
+ console.log("Invalid input request!", request);
+ return;
+ }
+ var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
+ if (callbacks) {
+ if (callbacks.input) {
+ callbacks.input(request);
+ }
+ }
+ };
+
+ return {'Kernel': Kernel};
+});
diff --git a/notebook/static/services/kernels/serialize.js b/notebook/static/services/kernels/serialize.js
new file mode 100644
index 0000000..a0bfd7a
--- /dev/null
+++ b/notebook/static/services/kernels/serialize.js
@@ -0,0 +1,126 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'underscore',
+ ], function (_) {
+ "use strict";
+
+ var _deserialize_array_buffer = function (buf) {
+ var data = new DataView(buf);
+ // read the header: 1 + nbufs 32b integers
+ var nbufs = data.getUint32(0);
+ var offsets = [];
+ var i;
+ for (i = 1; i <= nbufs; i++) {
+ offsets.push(data.getUint32(i * 4));
+ }
+ var json_bytes = new Uint8Array(buf.slice(offsets[0], offsets[1]));
+ var msg = JSON.parse(
+ (new TextDecoder('utf8')).decode(json_bytes)
+ );
+ // the remaining chunks are stored as DataViews in msg.buffers
+ msg.buffers = [];
+ var start, stop;
+ for (i = 1; i < nbufs; i++) {
+ start = offsets[i];
+ stop = offsets[i+1] || buf.byteLength;
+ msg.buffers.push(new DataView(buf.slice(start, stop)));
+ }
+ return msg;
+ };
+
+ var _deserialize_binary = function(data) {
+ /**
+ * deserialize the binary message format
+ * callback will be called with a message whose buffers attribute
+ * will be an array of DataViews.
+ */
+ if (data instanceof Blob) {
+ // data is Blob, have to deserialize from ArrayBuffer in reader callback
+ var reader = new FileReader();
+ var promise = new Promise(function(resolve, reject) {
+ reader.onload = function () {
+ var msg = _deserialize_array_buffer(this.result);
+ resolve(msg);
+ };
+ });
+ reader.readAsArrayBuffer(data);
+ return promise;
+ } else {
+ // data is ArrayBuffer, can deserialize directly
+ var msg = _deserialize_array_buffer(data);
+ return msg;
+ }
+ };
+
+ var deserialize = function (data) {
+ /**
+ * deserialize a message and return a promise for the unpacked message
+ */
+ if (typeof data === "string") {
+ // text JSON message
+ return Promise.resolve(JSON.parse(data));
+ } else {
+ // binary message
+ return Promise.resolve(_deserialize_binary(data));
+ }
+ };
+
+ var _serialize_binary = function (msg) {
+ /**
+ * implement the binary serialization protocol
+ * serializes JSON message to ArrayBuffer
+ */
+ msg = _.clone(msg);
+ var offsets = [];
+ var buffers = [];
+ var i;
+ for (i = 0; i < msg.buffers.length; i++) {
+ // msg.buffers elements could be either views or ArrayBuffers
+ // buffers elements are ArrayBuffers
+ var b = msg.buffers[i];
+ buffers.push(b.buffer instanceof ArrayBuffer ? b.buffer : b);
+ }
+ delete msg.buffers;
+ var json_utf8 = (new TextEncoder('utf8')).encode(JSON.stringify(msg));
+ buffers.unshift(json_utf8);
+ var nbufs = buffers.length;
+ offsets.push(4 * (nbufs + 1));
+ for (i = 0; i + 1 < buffers.length; i++) {
+ offsets.push(offsets[offsets.length-1] + buffers[i].byteLength);
+ }
+ var msg_buf = new Uint8Array(
+ offsets[offsets.length-1] + buffers[buffers.length-1].byteLength
+ );
+ // use DataView.setUint32 for network byte-order
+ var view = new DataView(msg_buf.buffer);
+ // write nbufs to first 4 bytes
+ view.setUint32(0, nbufs);
+ // write offsets to next 4 * nbufs bytes
+ for (i = 0; i < offsets.length; i++) {
+ view.setUint32(4 * (i+1), offsets[i]);
+ }
+ // write all the buffers at their respective offsets
+ for (i = 0; i < buffers.length; i++) {
+ msg_buf.set(new Uint8Array(buffers[i]), offsets[i]);
+ }
+
+ // return raw ArrayBuffer
+ return msg_buf.buffer;
+ };
+
+ var serialize = function (msg) {
+ if (msg.buffers && msg.buffers.length) {
+ return _serialize_binary(msg);
+ } else {
+ return JSON.stringify(msg);
+ }
+ };
+
+ var exports = {
+ deserialize : deserialize,
+ serialize: serialize
+ };
+ return exports;
+});
diff --git a/notebook/static/services/sessions/session.js b/notebook/static/services/sessions/session.js
new file mode 100644
index 0000000..8f6641b
--- /dev/null
+++ b/notebook/static/services/sessions/session.js
@@ -0,0 +1,321 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+ 'services/kernels/kernel',
+], function($, utils, kernel) {
+ "use strict";
+
+ /**
+ * Session object for accessing the session REST api. The session
+ * should be used to start kernels and then shut them down -- for
+ * all other operations, the kernel object should be used.
+ *
+ * Preliminary documentation for the REST API is at
+ * https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#sessions-api
+ *
+ * Options should include:
+ * - notebook_path: the path (not including name) to the notebook
+ * - kernel_name: the type of kernel (e.g. python3)
+ * - base_url: the root url of the notebook server
+ * - ws_url: the url to access websockets
+ * - notebook: Notebook object
+ *
+ * @class Session
+ * @param {Object} options
+ */
+ var Session = function (options) {
+ this.id = null;
+ this.notebook_model = {
+ path: options.notebook_path
+ };
+ this.kernel_model = {
+ id: null,
+ name: options.kernel_name
+ };
+
+ this.base_url = options.base_url;
+ this.ws_url = options.ws_url;
+ this.session_service_url = utils.url_path_join(this.base_url, 'api/sessions');
+ this.session_url = null;
+
+ this.notebook = options.notebook;
+ this.kernel = null;
+ this.events = options.notebook.events;
+
+ this.bind_events();
+ };
+
+ Session.prototype.bind_events = function () {
+ var that = this;
+ var record_status = function (evt, info) {
+ console.log('Session: ' + evt.type + ' (' + info.session.id + ')');
+ };
+
+ this.events.on('kernel_created.Session', record_status);
+ this.events.on('kernel_dead.Session', record_status);
+ this.events.on('kernel_killed.Session', record_status);
+
+ // if the kernel dies, then also remove the session
+ this.events.on('kernel_dead.Kernel', function () {
+ that.delete();
+ });
+ };
+
+
+ // Public REST api functions
+
+ /**
+ * GET /api/sessions
+ *
+ * Get a list of the current sessions.
+ *
+ * @function list
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Session.prototype.list = function (success, error) {
+ $.ajax(this.session_service_url, {
+ processData: false,
+ cache: false,
+ type: "GET",
+ dataType: "json",
+ success: success,
+ error: this._on_error(error)
+ });
+ };
+
+ /**
+ * POST /api/sessions
+ *
+ * Start a new session. This function can only executed once.
+ *
+ * @function start
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Session.prototype.start = function (success, error) {
+ var that = this;
+ var on_success = function (data, status, xhr) {
+ if (that.kernel) {
+ that.kernel.name = that.kernel_model.name;
+ } else {
+ var kernel_service_url = utils.url_path_join(that.base_url, "api/kernels");
+ that.kernel = new kernel.Kernel(kernel_service_url, that.ws_url, that.kernel_model.name);
+ }
+ that.events.trigger('kernel_created.Session', {session: that, kernel: that.kernel});
+ that.kernel._kernel_created(data.kernel);
+ if (success) {
+ success(data, status, xhr);
+ }
+ };
+ var on_error = function (xhr, status, err) {
+ that.events.trigger('kernel_dead.Session', {session: that, xhr: xhr, status: status, error: err});
+ if (error) {
+ error(xhr, status, err);
+ }
+ };
+
+ $.ajax(this.session_service_url, {
+ processData: false,
+ cache: false,
+ type: "POST",
+ data: JSON.stringify(this._get_model()),
+ contentType: 'application/json',
+ dataType: "json",
+ success: this._on_success(on_success),
+ error: this._on_error(on_error)
+ });
+ };
+
+ /**
+ * GET /api/sessions/[:session_id]
+ *
+ * Get information about a session.
+ *
+ * @function get_info
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Session.prototype.get_info = function (success, error) {
+ $.ajax(this.session_url, {
+ processData: false,
+ cache: false,
+ type: "GET",
+ dataType: "json",
+ success: this._on_success(success),
+ error: this._on_error(error)
+ });
+ };
+
+ /**
+ * PATCH /api/sessions/[:session_id]
+ *
+ * Rename or move a notebook. If the given name or path are
+ * undefined, then they will not be changed.
+ *
+ * @function rename_notebook
+ * @param {string} [path] - new notebook path
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Session.prototype.rename_notebook = function (path, success, error) {
+ if (path !== undefined) {
+ this.notebook_model.path = path;
+ }
+
+ $.ajax(this.session_url, {
+ processData: false,
+ cache: false,
+ type: "PATCH",
+ data: JSON.stringify(this._get_model()),
+ contentType: 'application/json',
+ dataType: "json",
+ success: this._on_success(success),
+ error: this._on_error(error)
+ });
+ };
+
+ /**
+ * DELETE /api/sessions/[:session_id]
+ *
+ * Kill the kernel and shutdown the session.
+ *
+ * @function delete
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Session.prototype.delete = function (success, error) {
+ if (this.kernel) {
+ this.events.trigger('kernel_killed.Session', {session: this, kernel: this.kernel});
+ this.kernel._kernel_dead();
+ }
+
+ $.ajax(this.session_url, {
+ processData: false,
+ cache: false,
+ type: "DELETE",
+ dataType: "json",
+ success: this._on_success(success),
+ error: this._on_error(error)
+ });
+ };
+
+ /**
+ * Restart the session by deleting it and the starting it
+ * fresh. If options are given, they can include any of the
+ * following:
+ *
+ * - notebook_path - the path to the notebook
+ * - kernel_name - the name (type) of the kernel
+ *
+ * @function restart
+ * @param {Object} [options] - options for the new kernel
+ * @param {function} [success] - function executed on ajax success
+ * @param {function} [error] - functon executed on ajax error
+ */
+ Session.prototype.restart = function (options, success, error) {
+ var that = this;
+ var start = function () {
+ if (options && options.notebook_path) {
+ that.notebook_model.path = options.notebook_path;
+ }
+ if (options && options.kernel_name) {
+ that.kernel_model.name = options.kernel_name;
+ }
+ that.kernel_model.id = null;
+ that.start(success, error);
+ };
+ this.delete(start, start);
+ };
+
+ // Helper functions
+
+ /**
+ * Get the data model for the session, which includes the notebook path
+ * and kernel (name and id).
+ *
+ * @function _get_model
+ * @returns {Object} - the data model
+ */
+ Session.prototype._get_model = function () {
+ return {
+ notebook: this.notebook_model,
+ kernel: this.kernel_model
+ };
+ };
+
+ /**
+ * Update the data model from the given JSON object, which should
+ * have attributes of `id`, `notebook`, and/or `kernel`. If
+ * provided, the notebook data must include name and path, and the
+ * kernel data must include name and id.
+ *
+ * @function _update_model
+ * @param {Object} data - updated data model
+ */
+ Session.prototype._update_model = function (data) {
+ if (data && data.id) {
+ this.id = data.id;
+ this.session_url = utils.url_path_join(this.session_service_url, this.id);
+ }
+ if (data && data.notebook) {
+ this.notebook_model.path = data.notebook.path;
+ }
+ if (data && data.kernel) {
+ this.kernel_model.name = data.kernel.name;
+ this.kernel_model.id = data.kernel.id;
+ }
+ };
+
+ /**
+ * Handle a successful AJAX request by updating the session data
+ * model with the response, and then optionally calling a provided
+ * callback.
+ *
+ * @function _on_success
+ * @param {function} success - callback
+ */
+ Session.prototype._on_success = function (success) {
+ var that = this;
+ return function (data, status, xhr) {
+ that._update_model(data);
+ if (success) {
+ success(data, status, xhr);
+ }
+ };
+ };
+
+ /**
+ * Handle a failed AJAX request by logging the error message, and
+ * then optionally calling a provided callback.
+ *
+ * @function _on_error
+ * @param {function} error - callback
+ */
+ Session.prototype._on_error = function (error) {
+ return function (xhr, status, err) {
+ utils.log_ajax_error(xhr, status, err);
+ if (error) {
+ error(xhr, status, err);
+ }
+ };
+ };
+
+ /**
+ * Error type indicating that the session is already starting.
+ */
+ var SessionAlreadyStarting = function (message) {
+ this.name = "SessionAlreadyStarting";
+ this.message = (message || "");
+ };
+
+ SessionAlreadyStarting.prototype = Error.prototype;
+
+ return {
+ Session: Session,
+ SessionAlreadyStarting: SessionAlreadyStarting
+ };
+});
diff --git a/notebook/static/style/ipython.less b/notebook/static/style/ipython.less
new file mode 100644
index 0000000..d09fa90
--- /dev/null
+++ b/notebook/static/style/ipython.less
@@ -0,0 +1,12 @@
+// minimal imports from bootstrap - only variables and mixins
+@import "../components/bootstrap/less/variables.less";
+@import "../components/bootstrap/less/mixins.less";
+
+// minimal imports from font-awesome
+@import "../components/font-awesome/less/variables.less";
+
+// base
+@import "../base/less/style.less";
+
+// notebook
+@import "../notebook/less/style_noapp.less";
diff --git a/notebook/static/style/style.less b/notebook/static/style/style.less
new file mode 100644
index 0000000..f87f0e7
--- /dev/null
+++ b/notebook/static/style/style.less
@@ -0,0 +1,35 @@
+/*!
+*
+* Twitter Bootstrap
+*
+*/
+@import "../components/bootstrap/less/bootstrap.less";
+
+/*!
+*
+* Font Awesome
+*
+*/
+@import "../components/font-awesome/less/font-awesome.less";
+@fa-font-path: "../components/font-awesome/fonts";
+
+// base
+@import "../base/less/style.less";
+@import "../base/less/page.less";
+
+// auth
+@import "../auth/less/style.less";
+
+// tree
+@import "../tree/less/style.less";
+
+// edit
+@import "../edit/less/style.less";
+
+// notebook
+@import "../notebook/less/style.less";
+@import "../notebook/less/commandpalette.less";
+@import "../notebook/less/searchandreplace.less";
+
+// terminal
+@import "../terminal/less/terminal.less";
diff --git a/notebook/static/terminal/css/override.css b/notebook/static/terminal/css/override.css
new file mode 100644
index 0000000..6d74111
--- /dev/null
+++ b/notebook/static/terminal/css/override.css
@@ -0,0 +1,7 @@
+/*This file contains any manual css for this page that needs to override the global styles.
+This is only required when different pages style the same element differently. This is just
+a hack to deal with our current css styles and no new styling should be added in this file.*/
+
+body {
+ overflow: hidden;
+}
diff --git a/notebook/static/terminal/js/main.js b/notebook/static/terminal/js/main.js
new file mode 100644
index 0000000..b6ffd57
--- /dev/null
+++ b/notebook/static/terminal/js/main.js
@@ -0,0 +1,69 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+require([
+ 'jquery',
+ 'termjs',
+ 'base/js/utils',
+ 'base/js/page',
+ 'services/config',
+ 'terminal/js/terminado',
+ 'custom',
+], function(
+ $,
+ termjs,
+ utils,
+ page,
+ configmod,
+ terminado
+ ){
+ "use strict";
+ page = new page.Page();
+
+ var config = new configmod.ConfigSection('terminal',
+ {base_url: utils.get_body_data('baseUrl')});
+ config.load();
+ var common_config = new configmod.ConfigSection('common',
+ {base_url: utils.get_body_data('baseUrl')});
+ common_config.load();
+ // Test size: 25x80
+ var termRowHeight = function(){ return 1.00 * $("#dummy-screen")[0].offsetHeight / 25;};
+ // 1.02 here arrived at by trial and error to make the spacing look right
+ var termColWidth = function() { return 1.02 * $("#dummy-screen-rows")[0].offsetWidth / 80;};
+
+ var base_url = utils.get_body_data('baseUrl');
+ var ws_path = utils.get_body_data('wsPath');
+ var ws_url = location.protocol.replace('http', 'ws') + "//" + location.host
+ + base_url + ws_path;
+
+ var header = $("#header")[0];
+ function calculate_size() {
+ var height = $(window).height() - header.offsetHeight;
+ var width = $('#terminado-container').width();
+ var rows = Math.min(1000, Math.max(20, Math.floor(height/termRowHeight())-1));
+ var cols = Math.min(1000, Math.max(40, Math.floor(width/termColWidth())-1));
+ console.log("resize to :", rows , 'rows by ', cols, 'columns');
+ return {rows: rows, cols: cols};
+ }
+
+ page.show_header();
+
+ var size = calculate_size();
+ var terminal = terminado.make_terminal($("#terminado-container")[0], size, ws_url);
+
+ page.show_site();
+
+ utils.load_extensions_from_config(config);
+ utils.load_extensions_from_config(common_config);
+
+ window.onresize = function() {
+ var geom = calculate_size();
+ terminal.term.resize(geom.cols, geom.rows);
+ terminal.socket.send(JSON.stringify(["set_size", geom.rows, geom.cols,
+ $(window).height(), $(window).width()]));
+ };
+
+ // Expose terminal for fiddling with in the browser
+ window.terminal = terminal;
+
+});
diff --git a/notebook/static/terminal/js/terminado.js b/notebook/static/terminal/js/terminado.js
new file mode 100644
index 0000000..5192f71
--- /dev/null
+++ b/notebook/static/terminal/js/terminado.js
@@ -0,0 +1,41 @@
+define ([], function() {
+ "use strict";
+ function make_terminal(element, size, ws_url) {
+ var ws = new WebSocket(ws_url);
+ Terminal.brokenBold = true;
+ var term = new Terminal({
+ cols: size.cols,
+ rows: size.rows,
+ screenKeys: false,
+ useStyle: false
+ });
+ ws.onopen = function(event) {
+ ws.send(JSON.stringify(["set_size", size.rows, size.cols,
+ window.innerHeight, window.innerWidth]));
+ term.on('data', function(data) {
+ ws.send(JSON.stringify(['stdin', data]));
+ });
+
+ term.on('title', function(title) {
+ document.title = title;
+ });
+
+ term.open(element);
+
+ ws.onmessage = function(event) {
+ var json_msg = JSON.parse(event.data);
+ switch(json_msg[0]) {
+ case "stdout":
+ term.write(json_msg[1]);
+ break;
+ case "disconnect":
+ term.write("\r\n\r\n[CLOSED]\r\n");
+ break;
+ }
+ };
+ };
+ return {socket: ws, term: term};
+ }
+
+ return {make_terminal: make_terminal};
+});
diff --git a/notebook/static/terminal/less/terminal.less b/notebook/static/terminal/less/terminal.less
new file mode 100644
index 0000000..4605e26
--- /dev/null
+++ b/notebook/static/terminal/less/terminal.less
@@ -0,0 +1,32 @@
+.terminal-app {
+ background: @page-backdrop-color;
+
+ #header {
+ background: @body-bg;
+ .box-shadow(@global-shadow);
+ }
+
+ .terminal {
+ float: left;
+ font-family: monospace;
+ color: white;
+ background: black;
+ padding: @code_padding;
+ border-radius: @border-radius-base;
+ .box-shadow(@global-shadow-dark);
+
+ &, dummy-screen {
+ line-height: 1em;
+ font-size: @notebook_font_size;
+ }
+ }
+
+ .terminal-cursor {
+ color: black;
+ background: white;
+ }
+
+ #terminado-container {
+ margin-top: @page-header-padding;
+ }
+}
diff --git a/notebook/static/tree/js/kernellist.js b/notebook/static/tree/js/kernellist.js
new file mode 100644
index 0000000..725c456
--- /dev/null
+++ b/notebook/static/tree/js/kernellist.js
@@ -0,0 +1,96 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'base/js/namespace',
+ 'jquery',
+ 'tree/js/notebooklist',
+], function(IPython, $, notebooklist) {
+ "use strict";
+
+ var KernelList = function (selector, options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * selector: string
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * session_list: SessionList instance
+ * base_url: string
+ * notebook_path: string
+ */
+ notebooklist.NotebookList.call(this, selector, $.extend({
+ element_name: 'running'},
+ options));
+ this.kernelspecs = this.sessions = null;
+ this.events.on('kernelspecs_loaded.KernelSpec', $.proxy(this._kernelspecs_loaded, this));
+ };
+
+ KernelList.prototype = Object.create(notebooklist.NotebookList.prototype);
+
+ KernelList.prototype.add_duplicate_button = function () {
+ /**
+ * do nothing
+ */
+ };
+
+ KernelList.prototype._kernelspecs_loaded = function (event, kernelspecs) {
+ this.kernelspecs = kernelspecs;
+ if (this.sessions) {
+ // trigger delayed session load, since kernelspecs arrived later
+ this.sessions_loaded(this.sessions);
+ }
+ };
+
+ KernelList.prototype.sessions_loaded = function (d) {
+ this.sessions = d;
+ if (!this.kernelspecs) {
+ return; // wait for kernelspecs before first load
+ }
+ this.clear_list();
+ var item, path, session;
+ for (path in d) {
+ if (!d.hasOwnProperty(path)) {
+ // nothing is safe in javascript
+ continue;
+ }
+ session = d[path];
+ item = this.new_item(-1);
+ this.add_link({
+ name: path,
+ path: path,
+ type: 'notebook',
+ kernel_display_name: this.kernelspecs[session.kernel.name].spec.display_name
+ }, item);
+ }
+ $('#running_list_placeholder').toggle($.isEmptyObject(d));
+ };
+
+ KernelList.prototype.add_link = function (model, item) {
+ notebooklist.NotebookList.prototype.add_link.apply(this, [model, item]);
+
+ var running_indicator = item.find(".item_buttons")
+ .text('');
+
+ var that = this;
+ var kernel_name = $('<div/>')
+ .addClass('kernel-name')
+ .text(model.kernel_display_name)
+ .appendTo(running_indicator);
+
+ var shutdown_button = $('<button/>')
+ .addClass('btn btn-warning btn-xs')
+ .text('Shutdown')
+ .click(function() {
+ var path = $(this).parent().parent().parent().data('path');
+ that.shutdown_notebook(path);
+ })
+ .appendTo(running_indicator);
+ };
+
+ // Backwards compatability.
+ IPython.KernelList = KernelList;
+
+ return {'KernelList': KernelList};
+});
diff --git a/notebook/static/tree/js/main.js b/notebook/static/tree/js/main.js
new file mode 100644
index 0000000..43c2b92
--- /dev/null
+++ b/notebook/static/tree/js/main.js
@@ -0,0 +1,177 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+require([
+ 'jquery',
+ 'base/js/namespace',
+ 'base/js/dialog',
+ 'base/js/events',
+ 'base/js/page',
+ 'base/js/utils',
+ 'services/config',
+ 'contents',
+ 'tree/js/notebooklist',
+ 'tree/js/sessionlist',
+ 'tree/js/kernellist',
+ 'tree/js/terminallist',
+ 'tree/js/newnotebook',
+ 'auth/js/loginwidget',
+ // only loaded, not used:
+ 'jquery-ui',
+ 'bootstrap',
+ 'custom',
+], function(
+ $,
+ IPython,
+ dialog,
+ events,
+ page,
+ utils,
+ config,
+ contents_service,
+ notebooklist,
+ sesssionlist,
+ kernellist,
+ terminallist,
+ newnotebook,
+ loginwidget){
+ "use strict";
+
+ IPython.NotebookList = notebooklist.NotebookList;
+
+ page = new page.Page();
+
+ var common_options = {
+ base_url: utils.get_body_data("baseUrl"),
+ notebook_path: utils.get_body_data("notebookPath"),
+ };
+ var cfg = new config.ConfigSection('tree', common_options);
+ cfg.load();
+ common_options.config = cfg;
+ var common_config = new config.ConfigSection('common', common_options);
+ common_config.load();
+
+ var session_list = new sesssionlist.SesssionList($.extend({
+ events: events},
+ common_options));
+ var contents = new contents_service.Contents({
+ base_url: common_options.base_url,
+ common_config: common_config
+ });
+ var notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
+ contents: contents,
+ session_list: session_list},
+ common_options));
+ var kernel_list = new kernellist.KernelList('#running_list', $.extend({
+ session_list: session_list},
+ common_options));
+
+ var terminal_list;
+ if (utils.get_body_data("terminalsAvailable") === "True") {
+ terminal_list = new terminallist.TerminalList('#terminal_list', common_options);
+ }
+
+ var login_widget = new loginwidget.LoginWidget('#login_widget', common_options);
+
+ var new_buttons = new newnotebook.NewNotebookWidget("#new-buttons",
+ $.extend(
+ {contents: contents, events: events},
+ common_options
+ )
+ );
+
+ var interval_id=0;
+ // auto refresh every xx secondes, no need to be fast,
+ // update is done most of the time when page get focus
+ IPython.tree_time_refresh = 60; // in sec
+
+ // limit refresh on focus at 1/10sec, otherwise this
+ // can cause too frequent refresh on switching through windows or tabs.
+ IPython.min_delta_refresh = 10; // in sec
+
+ var _last_refresh = null;
+
+ var _refresh_list = function(){
+ _last_refresh = new Date();
+ session_list.load_sessions();
+ if (terminal_list) {
+ terminal_list.load_terminals();
+ }
+ };
+
+ var enable_autorefresh = function(){
+ /**
+ *refresh immediately , then start interval
+ */
+ var now = new Date();
+
+ if (now - _last_refresh < IPython.min_delta_refresh*1000){
+ console.log("Reenabling autorefresh too close to last tree refresh, not refreshing immediately again.");
+ } else {
+ _refresh_list();
+ }
+ if (!interval_id){
+ interval_id = setInterval(_refresh_list,
+ IPython.tree_time_refresh*1000
+ );
+ }
+ };
+
+ var disable_autorefresh = function(){
+ clearInterval(interval_id);
+ interval_id = 0;
+ };
+
+ // stop autorefresh when page lose focus
+ $(window).blur(function() {
+ disable_autorefresh();
+ });
+
+ //re-enable when page get focus back
+ $(window).focus(function() {
+ enable_autorefresh();
+ });
+
+ // finally start it, it will refresh immediately
+ enable_autorefresh();
+
+ page.show();
+
+ // For backwards compatability.
+ IPython.page = page;
+ IPython.notebook_list = notebook_list;
+ IPython.session_list = session_list;
+ IPython.kernel_list = kernel_list;
+ IPython.login_widget = login_widget;
+ IPython.new_notebook_widget = new_buttons;
+
+ events.trigger('app_initialized.DashboardApp');
+ utils.load_extensions_from_config(cfg);
+ utils.load_extensions_from_config(common_config);
+
+ // bound the upload method to the on change of the file select list
+ $("#alternate_upload").change(function (event){
+ notebook_list.handleFilesUpload(event,'form');
+ });
+
+ // set hash on tab click
+ $("#tabs").find("a").click(function(e) {
+ // Prevent the document from jumping when the active tab is changed to a
+ // tab that has a lot of content.
+ e.preventDefault();
+
+ // Set the hash without causing the page to jump.
+ // http://stackoverflow.com/a/14690177/2824256
+ var hash = $(this).attr("href");
+ if(window.history.pushState) {
+ window.history.pushState(null, null, hash);
+ } else {
+ window.location.hash = hash;
+ }
+ });
+
+ // load tab if url hash
+ if (window.location.hash) {
+ $("#tabs").find("a[href=" + window.location.hash + "]").click();
+ }
+});
diff --git a/notebook/static/tree/js/newnotebook.js b/notebook/static/tree/js/newnotebook.js
new file mode 100644
index 0000000..622f0fb
--- /dev/null
+++ b/notebook/static/tree/js/newnotebook.js
@@ -0,0 +1,108 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/namespace',
+ 'base/js/utils',
+ 'base/js/dialog',
+], function ($, IPython, utils, dialog) {
+ "use strict";
+
+ var NewNotebookWidget = function (selector, options) {
+ this.selector = selector;
+ this.base_url = options.base_url;
+ this.notebook_path = options.notebook_path;
+ this.contents = options.contents;
+ this.events = options.events;
+ this.default_kernel = null;
+ this.kernelspecs = {};
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.request_kernelspecs();
+ }
+ this.bind_events();
+ };
+
+ NewNotebookWidget.prototype.bind_events = function () {
+ var that = this;
+ this.element.find('#new_notebook').click(function () {
+ that.new_notebook();
+ });
+ };
+
+ NewNotebookWidget.prototype.request_kernelspecs = function () {
+ /** request and then load kernel specs */
+ var url = utils.url_path_join(this.base_url, 'api/kernelspecs');
+ utils.promising_ajax(url).then($.proxy(this._load_kernelspecs, this));
+ };
+
+ NewNotebookWidget.prototype._load_kernelspecs = function (data) {
+ /** load kernelspec list */
+ var that = this;
+ this.kernelspecs = data.kernelspecs;
+ var menu = this.element.find("#notebook-kernels");
+ var keys = Object.keys(data.kernelspecs).sort(function (a, b) {
+ var da = data.kernelspecs[a].spec.display_name;
+ var db = data.kernelspecs[b].spec.display_name;
+ if (da === db) {
+ return 0;
+ } else if (da > db) {
+ return 1;
+ } else {
+ return -1;
+ }
+ });
+
+ // Create the kernel list in reverse order because
+ // the .after insertion causes each item to be added
+ // to the top of the list.
+ for (var i = keys.length - 1; i >= 0; i--) {
+ var ks = this.kernelspecs[keys[i]];
+ var li = $("<li>")
+ .attr("id", "kernel-" +ks.name)
+ .data('kernelspec', ks).append(
+ $('<a>')
+ .attr('href', '#')
+ .click($.proxy(this.new_notebook, this, ks.name))
+ .text(ks.spec.display_name)
+ .attr('title', 'Create a new notebook with ' + ks.spec.display_name)
+ );
+ menu.after(li);
+ }
+ this.events.trigger('kernelspecs_loaded.KernelSpec', data.kernelspecs);
+ };
+
+ NewNotebookWidget.prototype.new_notebook = function (kernel_name) {
+ /** create and open a new notebook */
+ var that = this;
+ kernel_name = kernel_name || this.default_kernel;
+ var w = window.open(undefined, IPython._target);
+ this.contents.new_untitled(that.notebook_path, {type: "notebook"}).then(
+ function (data) {
+ var url = utils.url_path_join(
+ that.base_url, 'notebooks',
+ utils.encode_uri_components(data.path)
+ );
+ if (kernel_name) {
+ url += "?kernel_name=" + kernel_name;
+ }
+ w.location = url;
+ }).catch(function (e) {
+ w.close();
+ dialog.modal({
+ title : 'Creating Notebook Failed',
+ body : $('<div/>')
+ .text("An error occurred while creating a new notebook.")
+ .append($('<div/>')
+ .addClass('alert alert-danger')
+ .text(e.message || e)),
+ buttons: {
+ OK: {'class' : 'btn-primary'}
+ }
+ });
+ });
+ };
+
+ return {'NewNotebookWidget': NewNotebookWidget};
+});
diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js
new file mode 100644
index 0000000..ba5ca31
--- /dev/null
+++ b/notebook/static/tree/js/notebooklist.js
@@ -0,0 +1,908 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'base/js/namespace',
+ 'jquery',
+ 'base/js/utils',
+ 'base/js/dialog',
+ 'base/js/events',
+ 'base/js/keyboard',
+], function(IPython, $, utils, dialog, events, keyboard) {
+ "use strict";
+
+ var NotebookList = function (selector, options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * selector: string
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * session_list: SessionList instance
+ * element_name: string
+ * base_url: string
+ * notebook_path: string
+ * contents: Contents instance
+ */
+ var that = this;
+ this.session_list = options.session_list;
+ this.events = this.session_list.events;
+ // allow code re-use by just changing element_name in kernellist.js
+ this.element_name = options.element_name || 'notebook';
+ this.selector = selector;
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.style();
+ this.bind_events();
+ }
+ this.notebooks_list = [];
+ this.sessions = {};
+ this.base_url = options.base_url || utils.get_body_data("baseUrl");
+ this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
+ this.contents = options.contents;
+ if (this.session_list && this.session_list.events) {
+ this.session_list.events.on('sessions_loaded.Dashboard',
+ function(e, d) { that.sessions_loaded(d); });
+ }
+ this.selected = [];
+ this._max_upload_size_mb = 25;
+ };
+
+ NotebookList.prototype.style = function () {
+ var prefix = '#' + this.element_name;
+ $(prefix + '_toolbar').addClass('list_toolbar');
+ $(prefix + '_list_info').addClass('toolbar_info');
+ $(prefix + '_buttons').addClass('toolbar_buttons');
+ $(prefix + '_list_header').addClass('list_header');
+ this.element.addClass("list_container");
+ };
+
+ NotebookList.prototype.bind_events = function () {
+ var that = this;
+ $('#refresh_' + this.element_name + '_list').click(function () {
+ that.load_sessions();
+ });
+ this.element.bind('dragover', function () {
+ return false;
+ });
+ this.element.bind('drop', function(event){
+ that.handleFilesUpload(event,'drop');
+ return false;
+ });
+
+ // Bind events for singleton controls.
+ if (!NotebookList._bound_singletons) {
+ NotebookList._bound_singletons = true;
+ $('#new-file').click(function(e) {
+ var w = window.open('', IPython._target);
+ that.contents.new_untitled(that.notebook_path || '', {type: 'file', ext: '.txt'}).then(function(data) {
+ var url = utils.url_path_join(
+ that.base_url, 'edit',
+ utils.encode_uri_components(data.path)
+ );
+ w.location = url;
+ }).catch(function (e) {
+ w.close();
+ dialog.modal({
+ title: 'Creating File Failed',
+ body: $('<div/>')
+ .text("An error occurred while creating a new file.")
+ .append($('<div/>')
+ .addClass('alert alert-danger')
+ .text(e.message || e)),
+ buttons: {
+ OK: {'class': 'btn-primary'}
+ }
+ });
+ console.warn('Error durring New file creation', e);
+ });
+ that.load_sessions();
+ });
+ $('#new-folder').click(function(e) {
+ that.contents.new_untitled(that.notebook_path || '', {type: 'directory'})
+ .then(function(){
+ that.load_list();
+ }).catch(function (e) {
+ dialog.modal({
+ title: 'Creating Folder Failed',
+ body: $('<div/>')
+ .text("An error occurred while creating a new folder.")
+ .append($('<div/>')
+ .addClass('alert alert-danger')
+ .text(e.message || e)),
+ buttons: {
+ OK: {'class': 'btn-primary'}
+ }
+ });
+ console.warn('Error durring New directory creation', e);
+ });
+ that.load_sessions();
+ });
+
+ // Bind events for action buttons.
+ $('.rename-button').click($.proxy(this.rename_selected, this));
+ $('.shutdown-button').click($.proxy(this.shutdown_selected, this));
+ $('.duplicate-button').click($.proxy(this.duplicate_selected, this));
+ $('.delete-button').click($.proxy(this.delete_selected, this));
+
+ // Bind events for selection menu buttons.
+ $('#selector-menu').click(function (event) {
+ that.select($(event.target).attr('id'));
+ });
+ var select_all = $('#select-all');
+ select_all.change(function () {
+ if (!select_all.prop('checked') || select_all.data('indeterminate')) {
+ that.select('select-none');
+ } else {
+ that.select('select-all');
+ }
+ });
+ $('#button-select-all').click(function (e) {
+ // toggle checkbox if the click doesn't come from the checkbox already
+ if (!$(e.target).is('input[type=checkbox]')) {
+ if (select_all.prop('checked') || select_all.data('indeterminate')) {
+ that.select('select-none');
+ } else {
+ that.select('select-all');
+ }
+ }
+ });
+ }
+ };
+
+ NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
+ var that = this;
+ var files;
+ if(dropOrForm === 'drop'){
+ files = event.originalEvent.dataTransfer.files;
+ } else
+ {
+ files = event.originalEvent.target.files;
+ }
+
+ var reader_onload = function (event) {
+ var item = $(event.target).data('item');
+ that.add_file_data(event.target.result, item);
+ that.add_upload_button(item);
+ };
+ var reader_onerror = function (event) {
+ var item = $(event.target).data('item');
+ var name = item.data('name');
+ item.remove();
+ dialog.modal({
+ title : 'Failed to read file',
+ body : "Failed to read file '" + name + "'",
+ buttons : {'OK' : { 'class' : 'btn-primary' }}
+ });
+ };
+
+ for (var i = 0; i < files.length; i++) {
+ var f = files[i];
+ var name_and_ext = utils.splitext(f.name);
+ var file_ext = name_and_ext[1];
+
+ // skip large files with a warning
+ if (f.size > this._max_upload_size_mb * 1024 * 1024) {
+ dialog.modal({
+ title : 'Cannot upload file',
+ body : "Cannot upload file (>" + this._max_upload_size_mb + " MB) '" + f.name + "'",
+ buttons : {'OK' : { 'class' : 'btn-primary' }}
+ });
+ continue;
+ }
+
+ var reader = new FileReader();
+ if (file_ext === '.ipynb') {
+ reader.readAsText(f);
+ } else {
+ // read non-notebook files as binary
+ reader.readAsArrayBuffer(f);
+ }
+ var item = that.new_item(0, true);
+ item.addClass('new-file');
+ that.add_name_input(f.name, item, file_ext === '.ipynb' ? 'notebook' : 'file');
+ // Store the list item in the reader so we can use it later
+ // to know which item it belongs to.
+ $(reader).data('item', item);
+ reader.onload = reader_onload;
+ reader.onerror = reader_onerror;
+ }
+ // Replace the file input form wth a clone of itself. This is required to
+ // reset the form. Otherwise, if you upload a file, delete it and try to
+ // upload it again, the changed event won't fire.
+ var form = $('input.fileinput');
+ form.replaceWith(form.clone(true));
+ return false;
+ };
+
+ NotebookList.prototype.clear_list = function (remove_uploads) {
+ /**
+ * Clears the navigation tree.
+ *
+ * Parameters
+ * remove_uploads: bool=False
+ * Should upload prompts also be removed from the tree.
+ */
+ if (remove_uploads) {
+ this.element.children('.list_item').remove();
+ } else {
+ this.element.children('.list_item:not(.new-file)').remove();
+ }
+ };
+
+ NotebookList.prototype.load_sessions = function(){
+ this.session_list.load_sessions();
+ };
+
+
+ NotebookList.prototype.sessions_loaded = function(data){
+ this.sessions = data;
+ this.load_list();
+ };
+
+ NotebookList.prototype.load_list = function () {
+ var that = this;
+ this.contents.list_contents(that.notebook_path).then(
+ $.proxy(this.draw_notebook_list, this),
+ function(error) {
+ that.draw_notebook_list({content: []}, "Server error: " + error.message);
+ }
+ );
+ };
+
+ /**
+ * Draw the list of notebooks
+ * @method draw_notebook_list
+ * @param {Array} list An array of dictionaries representing files or
+ * directories.
+ * @param {String} error_msg An error message
+ */
+
+
+ var type_order = {'directory':0,'notebook':1,'file':2};
+
+ NotebookList.prototype.draw_notebook_list = function (list, error_msg) {
+ // Remember what was selected before the refresh.
+ var selected_before = this.selected;
+
+ list.content.sort(function(a, b) {
+ if (type_order[a['type']] < type_order[b['type']]) {
+ return -1;
+ }
+ if (type_order[a['type']] > type_order[b['type']]) {
+ return 1;
+ }
+ if (a['name'].toLowerCase() < b['name'].toLowerCase()) {
+ return -1;
+ }
+ if (a['name'].toLowerCase() > b['name'].toLowerCase()) {
+ return 1;
+ }
+ return 0;
+ });
+ var message = error_msg || 'Notebook list empty.';
+ var item = null;
+ var model = null;
+ var len = list.content.length;
+ this.clear_list();
+ var n_uploads = this.element.children('.list_item').length;
+ if (len === 0) {
+ item = this.new_item(0);
+ var span12 = item.children().first();
+ span12.empty();
+ span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
+ }
+ var path = this.notebook_path;
+ var offset = n_uploads;
+ if (path !== '') {
+ item = this.new_item(offset, false);
+ model = {
+ type: 'directory',
+ name: '..',
+ path: utils.url_path_split(path)[0],
+ };
+ this.add_link(model, item);
+ offset += 1;
+ }
+ for (var i=0; i<len; i++) {
+ model = list.content[i];
+ item = this.new_item(i+offset, true);
+ try {
+ this.add_link(model, item);
+ } catch(err) {
+ console.log('Error adding link: ' + err);
+ }
+ }
+ // Trigger an event when we've finished drawing the notebook list.
+ events.trigger('draw_notebook_list.NotebookList');
+
+ // Reselect the items that were selected before. Notify listeners
+ // that the selected items may have changed. O(n^2) operation.
+ selected_before.forEach(function(item) {
+ var list_items = $('.list_item');
+ for (var i=0; i<list_items.length; i++) {
+ var $list_item = $(list_items[i]);
+ if ($list_item.data('path') === item.path) {
+ $list_item.find('input[type=checkbox]').prop('checked', true);
+ break;
+ }
+ }
+ });
+ this._selection_changed();
+ };
+
+
+ /**
+ * Creates a new item.
+ * @param {integer} index
+ * @param {boolean} [selectable] - tristate, undefined: don't draw checkbox,
+ * false: don't draw checkbox but pad
+ * where it should be, true: draw checkbox.
+ * @return {JQuery} row
+ */
+ NotebookList.prototype.new_item = function (index, selectable) {
+ var row = $('<div/>')
+ .addClass("list_item")
+ .addClass("row");
+
+ var item = $("<div/>")
+ .addClass("col-md-12")
+ .appendTo(row);
+
+ var checkbox;
+ if (selectable !== undefined) {
+ checkbox = $('<input/>')
+ .attr('type', 'checkbox')
+ .attr('title', 'Click here to rename, delete, etc.')
+ .appendTo(item);
+ }
+
+ $('<i/>')
+ .addClass('item_icon')
+ .appendTo(item);
+
+ var link = $("<a/>")
+ .addClass("item_link")
+ .appendTo(item);
+
+ $("<span/>")
+ .addClass("item_name")
+ .appendTo(link);
+
+ if (selectable === false) {
+ checkbox.css('visibility', 'hidden');
+ } else if (selectable === true) {
+ var that = this;
+ row.click(function(e) {
+ // toggle checkbox only if the click doesn't come from the checkbox or the link
+ if (!$(e.target).is('span[class=item_name]') && !$(e.target).is('input[type=checkbox]')) {
+ checkbox.prop('checked', !checkbox.prop('checked'));
+ }
+ that._selection_changed();
+ });
+ }
+
+ var buttons = $('<div/>')
+ .addClass("item_buttons pull-right")
+ .appendTo(item);
+
+ $('<div/>')
+ .addClass('running-indicator')
+ .text('Running')
+ .css('visibility', 'hidden')
+ .appendTo(buttons);
+
+ if (index === -1) {
+ this.element.append(row);
+ } else {
+ this.element.children().eq(index).after(row);
+ }
+ return row;
+ };
+
+
+ NotebookList.icons = {
+ directory: 'folder_icon',
+ notebook: 'notebook_icon',
+ file: 'file_icon',
+ };
+
+ NotebookList.uri_prefixes = {
+ directory: 'tree',
+ notebook: 'notebooks',
+ file: 'edit',
+ };
+
+ /**
+ * Select all items in the tree of specified type.
+ * selection_type : string among "select-all", "select-folders", "select-notebooks", "select-running-notebooks", "select-files"
+ * any other string (like "select-none") deselects all items
+ */
+ NotebookList.prototype.select = function(selection_type) {
+ var that = this;
+ $('.list_item').each(function(index, item) {
+ var item_type = $(item).data('type');
+ var state = false;
+ state = state || (selection_type === "select-all");
+ state = state || (selection_type === "select-folders" && item_type === 'directory');
+ state = state || (selection_type === "select-notebooks" && item_type === 'notebook');
+ state = state || (selection_type === "select-running-notebooks" && item_type === 'notebook' && that.sessions[$(item).data('path')] !== undefined);
+ state = state || (selection_type === "select-files" && item_type === 'file');
+ $(item).find('input[type=checkbox]').prop('checked', state);
+ });
+ this._selection_changed();
+ };
+
+
+ /**
+ * Handles when any row selector checkbox is toggled.
+ */
+ NotebookList.prototype._selection_changed = function() {
+ // Use a JQuery selector to find each row with a checked checkbox. If
+ // we decide to add more checkboxes in the future, this code will need
+ // to be changed to distinguish which checkbox is the row selector.
+ var selected = [];
+ var has_running_notebook = false;
+ var has_directory = false;
+ var has_file = false;
+ var that = this;
+ var checked = 0;
+ $('.list_item :checked').each(function(index, item) {
+ var parent = $(item).parent().parent();
+
+ // If the item doesn't have an upload button, isn't the
+ // breadcrumbs and isn't the parent folder '..', then it can be selected.
+ // Breadcrumbs path == ''.
+ if (parent.find('.upload_button').length === 0 && parent.data('path') !== '' && parent.data('path') !== utils.url_path_split(that.notebook_path)[0]) {
+ checked++;
+ selected.push({
+ name: parent.data('name'),
+ path: parent.data('path'),
+ type: parent.data('type')
+ });
+
+ // Set flags according to what is selected. Flags are later
+ // used to decide which action buttons are visible.
+ has_running_notebook = has_running_notebook ||
+ (parent.data('type') === 'notebook' && that.sessions[parent.data('path')] !== undefined);
+ has_file = has_file || (parent.data('type') === 'file');
+ has_directory = has_directory || (parent.data('type') === 'directory');
+ }
+ });
+ this.selected = selected;
+
+ // Rename is only visible when one item is selected, and it is not a running notebook
+ if (selected.length === 1 && !has_running_notebook) {
+ $('.rename-button').css('display', 'inline-block');
+ } else {
+ $('.rename-button').css('display', 'none');
+ }
+
+ // Shutdown is only visible when one or more notebooks running notebooks
+ // are selected and no non-notebook items are selected.
+ if (has_running_notebook && !(has_file || has_directory)) {
+ $('.shutdown-button').css('display', 'inline-block');
+ } else {
+ $('.shutdown-button').css('display', 'none');
+ }
+
+ // Duplicate isn't visible when a directory is selected.
+ if (selected.length > 0 && !has_directory) {
+ $('.duplicate-button').css('display', 'inline-block');
+ } else {
+ $('.duplicate-button').css('display', 'none');
+ }
+
+ // Delete is visible if one or more items are selected.
+ if (selected.length > 0) {
+ $('.delete-button').css('display', 'inline-block');
+ } else {
+ $('.delete-button').css('display', 'none');
+ }
+
+ // If all of the items are selected, show the selector as checked. If
+ // some of the items are selected, show it as checked. Otherwise,
+ // uncheck it.
+ var total = 0;
+ $('.list_item input[type=checkbox]').each(function(index, item) {
+ var parent = $(item).parent().parent();
+ // If the item doesn't have an upload button and it's not the
+ // breadcrumbs, it can be selected. Breadcrumbs path == ''.
+ if (parent.find('.upload_button').length === 0 && parent.data('path') !== '' && parent.data('path') !== utils.url_path_split(that.notebook_path)[0]) {
+ total++;
+ }
+ });
+
+ var select_all = $("#select-all");
+ if (checked === 0) {
+ select_all.prop('checked', false);
+ select_all.prop('indeterminate', false);
+ select_all.data('indeterminate', false);
+ } else if (checked === total) {
+ select_all.prop('checked', true);
+ select_all.prop('indeterminate', false);
+ select_all.data('indeterminate', false);
+ } else {
+ select_all.prop('checked', false);
+ select_all.prop('indeterminate', true);
+ select_all.data('indeterminate', true);
+ }
+ // Update total counter
+ $('#counter-select-all').html(checked===0 ? '&nbsp;' : checked);
+
+ // If at aleast on item is selected, hide the selection instructions.
+ if (checked > 0) {
+ $('.dynamic-instructions').hide();
+ } else {
+ $('.dynamic-instructions').show();
+ }
+ };
+
+ NotebookList.prototype.add_link = function (model, item) {
+ var path = model.path,
+ name = model.name;
+ var running = (model.type === 'notebook' && this.sessions[path] !== undefined);
+
+ item.data('name', name);
+ item.data('path', path);
+ item.data('type', model.type);
+ item.find(".item_name").text(name);
+ var icon = NotebookList.icons[model.type];
+ if (running) {
+ icon = 'running_' + icon;
+ }
+ var uri_prefix = NotebookList.uri_prefixes[model.type];
+ if (model.type === 'file' &&
+ model.mimetype && model.mimetype.substr(0,5) !== 'text/'
+ && !model.mimetype.endsWith('javascript')
+ ) {
+ // send text/unidentified files to editor, others go to raw viewer
+ uri_prefix = 'files';
+ }
+
+ item.find(".item_icon").addClass(icon).addClass('icon-fixed-width');
+ var link = item.find("a.item_link")
+ .attr('href',
+ utils.url_path_join(
+ this.base_url,
+ uri_prefix,
+ utils.encode_uri_components(path)
+ )
+ );
+
+ item.find(".item_buttons .running-indicator").css('visibility', running ? '' : 'hidden');
+
+ // directory nav doesn't open new tabs
+ // files, notebooks do
+ if (model.type !== "directory") {
+ link.attr('target',IPython._target);
+ }
+ };
+
+
+ NotebookList.prototype.add_name_input = function (name, item, icon_type) {
+ item.data('name', name);
+ item.find(".item_icon").addClass(NotebookList.icons[icon_type]).addClass('icon-fixed-width');
+ item.find(".item_name").empty().append(
+ $('<input/>')
+ .addClass("filename_input")
+ .attr('value', name)
+ .attr('size', '30')
+ .attr('type', 'text')
+ .keyup(function(event){
+ if(event.keyCode === 13){item.find('.upload_button').click();}
+ else if(event.keyCode === 27){item.remove();}
+ })
+ );
+ };
+
+
+ NotebookList.prototype.add_file_data = function (data, item) {
+ item.data('filedata', data);
+ };
+
+
+ NotebookList.prototype.shutdown_selected = function() {
+ var that = this;
+ this.selected.forEach(function(item) {
+ if (item.type === 'notebook') {
+ that.shutdown_notebook(item.path);
+ }
+ });
+ };
+
+ NotebookList.prototype.shutdown_notebook = function(path) {
+ var that = this;
+ var settings = {
+ processData : false,
+ cache : false,
+ type : "DELETE",
+ dataType : "json",
+ success : function () {
+ that.load_sessions();
+ },
+ error : utils.log_ajax_error,
+ };
+
+ var session = this.sessions[path];
+ if (session) {
+ var url = utils.url_path_join(
+ this.base_url,
+ 'api/sessions',
+ encodeURIComponent(session.id)
+ );
+ $.ajax(url, settings);
+ }
+ };
+
+ NotebookList.prototype.rename_selected = function() {
+ if (this.selected.length !== 1){
+ return;
+ }
+
+ var that = this;
+ var item_path = this.selected[0].path;
+ var item_name = this.selected[0].name;
+ var item_type = this.selected[0].type;
+ var input = $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
+ .val(item_name);
+ var dialog_body = $('<div/>').append(
+ $("<p/>").addClass("rename-message")
+ .text('Enter a new '+ item_type + ' name:')
+ ).append(
+ $("<br/>")
+ ).append(input);
+ var d = dialog.modal({
+ title : "Rename "+ item_type,
+ body : dialog_body,
+ buttons : {
+ OK : {
+ class: "btn-primary",
+ click: function() {
+ that.contents.rename(item_path, utils.url_path_join(that.notebook_path, input.val())).then(function() {
+ that.load_list();
+ }).catch(function(e) {
+ dialog.modal({
+ title: "Rename Failed",
+ body: $('<div/>')
+ .text("An error occurred while renaming \"" + item_name + "\" to \"" + input.val() + "\".")
+ .append($('<div/>')
+ .addClass('alert alert-danger')
+ .text(e.message || e)),
+ buttons: {
+ OK: {'class': 'btn-primary'}
+ }
+ });
+ console.warn('Error durring renaming :', e);
+ });
+ }
+ },
+ Cancel : {}
+ },
+ open : function () {
+ // Upon ENTER, click the OK button.
+ input.keydown(function (event) {
+ if (event.which === keyboard.keycodes.enter) {
+ d.find('.btn-primary').first().click();
+ return false;
+ }
+ });
+ input.focus();
+ if (input.val().indexOf(".") > 0) {
+ input[0].setSelectionRange(0,input.val().indexOf("."));
+ } else {
+ input.select();
+ }
+ }
+ });
+ };
+
+ NotebookList.prototype.delete_selected = function() {
+ var message;
+ if (this.selected.length === 1) {
+ message = 'Are you sure you want to permanently delete: ' + this.selected[0].name + '?';
+ } else {
+ message = 'Are you sure you want to permanently delete the ' + this.selected.length + ' files/folders selected?';
+ }
+ var that = this;
+ dialog.modal({
+ title : "Delete",
+ body : message,
+ buttons : {
+ Delete : {
+ class: "btn-danger",
+ click: function() {
+ // Shutdown any/all selected notebooks before deleting
+ // the files.
+ that.shutdown_selected();
+
+ // Delete selected.
+ that.selected.forEach(function(item) {
+ that.contents.delete(item.path).then(function() {
+ that.notebook_deleted(item.path);
+ }).catch(function(e) {
+ dialog.modal({
+ title: "Delete Failed",
+ body: $('<div/>')
+ .text("An error occurred while deleting \"" + item.path + "\".")
+ .append($('<div/>')
+ .addClass('alert alert-danger')
+ .text(e.message || e)),
+ buttons: {
+ OK: {'class': 'btn-primary'}
+ }
+ });
+ console.warn('Error durring content deletion:', e);
+ });
+ });
+ }
+ },
+ Cancel : {}
+ }
+ });
+ };
+
+ NotebookList.prototype.duplicate_selected = function() {
+ var message;
+ if (this.selected.length === 1) {
+ message = 'Are you sure you want to duplicate: ' + this.selected[0].name + '?';
+ } else {
+ message = 'Are you sure you want to duplicate the ' + this.selected.length + ' files selected?';
+ }
+ var that = this;
+ dialog.modal({
+ title : "Duplicate",
+ body : message,
+ buttons : {
+ Duplicate : {
+ class: "btn-primary",
+ click: function() {
+ that.selected.forEach(function(item) {
+ that.contents.copy(item.path, that.notebook_path).then(function () {
+ that.load_list();
+ }).catch(function(e) {
+ dialog.modal({
+ title: "Duplicate Failed",
+ body: $('<div/>')
+ .text("An error occurred while duplicating \"" + item.path + "\".")
+ .append($('<div/>')
+ .addClass('alert alert-danger')
+ .text(e.message || e)),
+ buttons: {
+ OK: {'class': 'btn-primary'}
+ }
+ });
+ console.warn('Error durring content duplication', e);
+ });
+ });
+ }
+ },
+ Cancel : {}
+ }
+ });
+ };
+
+ NotebookList.prototype.notebook_deleted = function(path) {
+ /**
+ * Remove the deleted notebook.
+ */
+ var that = this;
+ $( ":data(path)" ).each(function() {
+ var element = $(this);
+ if (element.data("path") === path) {
+ element.remove();
+ events.trigger('notebook_deleted.NotebookList');
+ that._selection_changed();
+ }
+ });
+ };
+
+
+ NotebookList.prototype.add_upload_button = function (item) {
+ var that = this;
+ var upload_button = $('<button/>').text("Upload")
+ .addClass('btn btn-primary btn-xs upload_button')
+ .click(function (e) {
+ var filename = item.find('.item_name > input').val();
+ var path = utils.url_path_join(that.notebook_path, filename);
+ var filedata = item.data('filedata');
+ var format = 'text';
+ if (filename.length === 0 || filename[0] === '.') {
+ dialog.modal({
+ title : 'Invalid file name',
+ body : "File names must be at least one character and not start with a dot",
+ buttons : {'OK' : { 'class' : 'btn-primary' }}
+ });
+ return false;
+ }
+ if (filedata instanceof ArrayBuffer) {
+ // base64-encode binary file data
+ var bytes = '';
+ var buf = new Uint8Array(filedata);
+ var nbytes = buf.byteLength;
+ for (var i=0; i<nbytes; i++) {
+ bytes += String.fromCharCode(buf[i]);
+ }
+ filedata = btoa(bytes);
+ format = 'base64';
+ }
+ var model = { name: filename, path: path };
+
+ var name_and_ext = utils.splitext(filename);
+ var file_ext = name_and_ext[1];
+ var content_type;
+ if (file_ext === '.ipynb') {
+ model.type = 'notebook';
+ model.format = 'json';
+ try {
+ model.content = JSON.parse(filedata);
+ } catch (e) {
+ dialog.modal({
+ title : 'Cannot upload invalid Notebook',
+ body : "The error was: " + e,
+ buttons : {'OK' : {
+ 'class' : 'btn-primary',
+ click: function () {
+ item.remove();
+ }
+ }}
+ });
+ console.warn('Error durring notebook uploading', e);
+ return false;
+ }
+ content_type = 'application/json';
+ } else {
+ model.type = 'file';
+ model.format = format;
+ model.content = filedata;
+ content_type = 'application/octet-stream';
+ }
+ filedata = item.data('filedata');
+
+ var on_success = function () {
+ item.removeClass('new-file');
+ that.add_link(model, item);
+ that.session_list.load_sessions();
+ };
+
+ var exists = false;
+ $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
+ if ($(v).data('name') === filename) { exists = true; return false; }
+ });
+
+ if (exists) {
+ dialog.modal({
+ title : "Replace file",
+ body : 'There is already a file named ' + filename + ', do you want to replace it?',
+ buttons : {
+ Overwrite : {
+ class: "btn-danger",
+ click: function () {
+ that.contents.save(path, model).then(on_success);
+ }
+ },
+ Cancel : {
+ click: function() { item.remove(); }
+ }
+ }
+ });
+ } else {
+ that.contents.save(path, model).then(on_success);
+ }
+
+ return false;
+ });
+ var cancel_button = $('<button/>').text("Cancel")
+ .addClass("btn btn-default btn-xs")
+ .click(function (e) {
+ item.remove();
+ return false;
+ });
+ item.find(".item_buttons").empty()
+ .append(upload_button)
+ .append(cancel_button);
+ };
+
+ return {'NotebookList': NotebookList};
+});
diff --git a/notebook/static/tree/js/sessionlist.js b/notebook/static/tree/js/sessionlist.js
new file mode 100644
index 0000000..75461f7
--- /dev/null
+++ b/notebook/static/tree/js/sessionlist.js
@@ -0,0 +1,86 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'jquery',
+ 'base/js/utils',
+], function($, utils) {
+ "use strict";
+
+ var SesssionList = function (options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * events: $(Events) instance
+ * base_url : string
+ */
+ this.events = options.events;
+ this.sessions = {};
+ this.base_url = options.base_url || utils.get_body_data("baseUrl");
+
+ // Add collapse arrows.
+ $('#running .panel-group .panel .panel-heading a').each(function(index, el) {
+ var $link = $(el);
+ var $icon = $('<i />')
+ .addClass('fa fa-caret-down');
+ $link.append($icon);
+ $link.down = true;
+ $link.click(function () {
+ if ($link.down) {
+ $link.down = false;
+ // jQeury doesn't know how to animate rotations. Abuse
+ // jQueries animate function by using an unused css attribute
+ // to do the animation (borderSpacing).
+ $icon.animate({ borderSpacing: 90 }, {
+ step: function(now,fx) {
+ $icon.css('transform','rotate(-' + now + 'deg)');
+ }
+ }, 250);
+ } else {
+ $link.down = true;
+ // See comment above.
+ $icon.animate({ borderSpacing: 0 }, {
+ step: function(now,fx) {
+ $icon.css('transform','rotate(-' + now + 'deg)');
+ }
+ }, 250);
+ }
+ });
+ });
+ };
+
+ SesssionList.prototype.load_sessions = function(){
+ var that = this;
+ var settings = {
+ processData : false,
+ cache : false,
+ type : "GET",
+ dataType : "json",
+ success : $.proxy(that.sessions_loaded, this),
+ error : utils.log_ajax_error,
+ };
+ var url = utils.url_path_join(this.base_url, 'api/sessions');
+ $.ajax(url, settings);
+ };
+
+ SesssionList.prototype.sessions_loaded = function(data){
+ this.sessions = {};
+ var len = data.length;
+ var nb_path;
+ for (var i=0; i<len; i++) {
+ nb_path = data[i].notebook.path;
+ this.sessions[nb_path] = {
+ id: data[i].id,
+ kernel: {
+ name: data[i].kernel.name
+ }
+ };
+ }
+ this.events.trigger('sessions_loaded.Dashboard', this.sessions);
+ };
+
+ return {'SesssionList': SesssionList};
+});
diff --git a/notebook/static/tree/js/terminallist.js b/notebook/static/tree/js/terminallist.js
new file mode 100644
index 0000000..66da095
--- /dev/null
+++ b/notebook/static/tree/js/terminallist.js
@@ -0,0 +1,124 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+define([
+ 'base/js/namespace',
+ 'base/js/utils',
+ 'jquery',
+ 'tree/js/notebooklist',
+], function(IPython, utils, $, notebooklist) {
+ "use strict";
+
+ var TerminalList = function (selector, options) {
+ /**
+ * Constructor
+ *
+ * Parameters:
+ * selector: string
+ * options: dictionary
+ * Dictionary of keyword arguments.
+ * base_url: string
+ */
+ this.base_url = options.base_url || utils.get_body_data("baseUrl");
+ this.element_name = options.element_name || 'running';
+ this.selector = selector;
+ this.terminals = [];
+ if (this.selector !== undefined) {
+ this.element = $(selector);
+ this.style();
+ this.bind_events();
+ this.load_terminals();
+ }
+ };
+
+ TerminalList.prototype = Object.create(notebooklist.NotebookList.prototype);
+
+ TerminalList.prototype.bind_events = function () {
+ var that = this;
+ $('#refresh_' + this.element_name + '_list').click(function () {
+ that.load_terminals();
+ });
+ $('#new-terminal').click($.proxy(this.new_terminal, this));
+ };
+
+ TerminalList.prototype.new_terminal = function () {
+ var w = window.open(undefined, IPython._target);
+ var base_url = this.base_url;
+ var settings = {
+ type : "POST",
+ dataType: "json",
+ success : function (data, status, xhr) {
+ var name = data.name;
+ w.location = utils.url_path_join(base_url, 'terminals',
+ utils.encode_uri_components(name));
+ },
+ error : function(jqXHR, status, error){
+ w.close();
+ utils.log_ajax_error(jqXHR, status, error);
+ },
+ };
+ var url = utils.url_path_join(
+ this.base_url,
+ 'api/terminals'
+ );
+ $.ajax(url, settings);
+ };
+
+ TerminalList.prototype.load_terminals = function() {
+ var url = utils.url_path_join(this.base_url, 'api/terminals');
+ $.ajax(url, {
+ type: "GET",
+ cache: false,
+ dataType: "json",
+ success: $.proxy(this.terminals_loaded, this),
+ error : utils.log_ajax_error
+ });
+ };
+
+ TerminalList.prototype.terminals_loaded = function (data) {
+ this.terminals = data;
+ this.clear_list();
+ var item, term;
+ for (var i=0; i < this.terminals.length; i++) {
+ term = this.terminals[i];
+ item = this.new_item(-1);
+ this.add_link(term.name, item);
+ this.add_shutdown_button(term.name, item);
+ }
+ $('#terminal_list_header').toggle(data.length === 0);
+ };
+
+ TerminalList.prototype.add_link = function(name, item) {
+ item.data('term-name', name);
+ item.find(".item_name").text("terminals/" + name);
+ item.find(".item_icon").addClass("fa fa-terminal");
+ var link = item.find("a.item_link")
+ .attr('href', utils.url_path_join(this.base_url, "terminals",
+ utils.encode_uri_components(name)));
+ link.attr('target', IPython._target||'_blank');
+ this.add_shutdown_button(name, item);
+ };
+
+ TerminalList.prototype.add_shutdown_button = function(name, item) {
+ var that = this;
+ var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-warning").
+ click(function (e) {
+ var settings = {
+ processData : false,
+ type : "DELETE",
+ dataType : "json",
+ success : function () {
+ that.load_terminals();
+ },
+ error : utils.log_ajax_error,
+ };
+ var url = utils.url_path_join(that.base_url, 'api/terminals',
+ utils.encode_uri_components(name));
+ $.ajax(url, settings);
+ return false;
+ });
+ item.find(".item_buttons").text("").append(shutdown_button);
+ };
+
+ return {TerminalList: TerminalList};
+});
diff --git a/notebook/static/tree/less/altuploadform.less b/notebook/static/tree/less/altuploadform.less
new file mode 100644
index 0000000..c5798f0
--- /dev/null
+++ b/notebook/static/tree/less/altuploadform.less
@@ -0,0 +1,28 @@
+/* We need an invisible input field on top of the sentense*/
+/* "Drag file onto the list ..." */
+
+.alternate_upload {
+ background-color:none;
+ display: inline;
+
+ &.form
+ {
+ padding: 0;
+ margin:0;
+ }
+
+ input.fileinput
+ {
+ text-align: center;
+ vertical-align: middle;
+ display: inline;
+ opacity: 0;
+ z-index: 2;
+ width: 12ex;
+ margin-right: -12ex;
+ }
+
+ .btn-upload {
+ height: @btn_mini_height;
+ }
+}
diff --git a/notebook/static/tree/less/style.less b/notebook/static/tree/less/style.less
new file mode 100644
index 0000000..04fac7c
--- /dev/null
+++ b/notebook/static/tree/less/style.less
@@ -0,0 +1,7 @@
+/*!
+*
+* IPython tree view
+*
+*/
+@import "altuploadform.less";
+@import "tree.less"; \ No newline at end of file
diff --git a/notebook/static/tree/less/tree.less b/notebook/static/tree/less/tree.less
new file mode 100644
index 0000000..30ffe5a
--- /dev/null
+++ b/notebook/static/tree/less/tree.less
@@ -0,0 +1,327 @@
+
+/**
+ * Primary styles
+ *
+ * Author: Jupyter Development Team
+ */
+
+@dashboard_tb_pad: 4px;
+@dashboard_lr_pad: 7px;
+// These are the total heights of the Bootstrap small and mini buttons. These values
+// are not less variables so we have to track them statically.
+@btn_small_height: 24px;
+@btn_mini_height: 22px;
+@dark_dashboard_color: @breadcrumb-color;
+
+// The left padding of the selector button's contents.
+@dashboard-selectorbtn-lpad: 7px;
+
+ul#tabs {
+ margin-bottom: @dashboard_tb_pad;
+}
+
+ul#tabs a {
+ padding-top: @dashboard_tb_pad + 2px;
+ padding-bottom: @dashboard_tb_pad;
+}
+
+ul.breadcrumb {
+ a:focus, a:hover {
+ text-decoration: none;
+ }
+ i.icon-home {
+ font-size: 16px;
+ margin-right: 4px;
+ }
+
+ span {
+ color: @dark_dashboard_color;
+ }
+}
+
+.list_toolbar {
+ padding: @dashboard_tb_pad 0 @dashboard_tb_pad 0;
+ vertical-align: middle;
+
+ .tree-buttons {
+ padding-top: 1px;
+ }
+}
+
+.dynamic-buttons {
+ padding-top: @dashboard_tb_pad - 1px;
+ display: inline-block;
+}
+
+.list_toolbar [class*="span"] {
+ min-height: @btn_small_height;
+}
+
+.list_header {
+ font-weight: bold;
+ background-color: @page-backdrop-color
+}
+
+.list_placeholder {
+ font-weight: bold;
+ padding-top: @dashboard_tb_pad;
+ padding-bottom: @dashboard_tb_pad;
+ padding-left: @dashboard_lr_pad;
+ padding-right: @dashboard_lr_pad;
+}
+
+.list_container {
+ margin-top: @dashboard_tb_pad;
+ margin-bottom: 5*@dashboard_tb_pad;
+ border: 1px solid @table-border-color;
+ border-radius: @border-radius-base;
+}
+
+.list_container > div {
+ border-bottom: 1px solid @table-border-color;
+ &:hover .list-item{
+ background-color: red;
+ };
+}
+
+.list_container > div:last-child {
+ border: none;
+}
+
+.list_item {
+ &:hover .list_item {
+ background-color: @table-border-color;
+ };
+ a {text-decoration: none;}
+ &:hover {
+ background-color: darken(white,2%);
+ }
+}
+
+.list_header>div, .list_item>div {
+ padding-top: @dashboard_tb_pad;
+ padding-bottom: @dashboard_tb_pad;
+ padding-left: @dashboard_lr_pad;
+ padding-right: @dashboard_lr_pad;
+ line-height: @btn_mini_height;
+
+ input {
+ margin-right: @dashboard_lr_pad;
+ margin-left: @dashboard_lr_pad + @dashboard-selectorbtn-lpad;
+ vertical-align: baseline;
+ line-height: @btn_mini_height;
+ position: relative;
+ top: -1px;
+ }
+
+ .item_link {
+ margin-left: -1px;
+ vertical-align: baseline;
+ line-height: @btn_mini_height;
+ }
+}
+
+.new-file input[type=checkbox] {
+ visibility: hidden;
+}
+
+.item_name {
+ line-height: @btn_mini_height;
+ height: @btn_small_height;
+}
+
+.item_icon {
+ font-size: 14px;
+ color: @dark_dashboard_color;
+ margin-right: @dashboard_lr_pad;
+ margin-left: @dashboard_lr_pad;
+ line-height: @btn_mini_height;
+ vertical-align: baseline;
+}
+
+.item_buttons {
+ line-height: 1em;
+ .btn-toolbar();
+ .btn {
+ min-width: 13ex;
+ }
+ .running-indicator {
+ padding-top: @dashboard_tb_pad;
+ color: @brand-success;
+ }
+ .kernel-name {
+ padding-top: @dashboard_tb_pad;
+ color: @brand-info;
+ margin-right: @dashboard_lr_pad;
+ float: left;
+ }
+}
+
+.toolbar_info {
+ height: @btn_small_height;
+ line-height: @btn_small_height;
+}
+
+.list_item input:not([type=checkbox]) {
+ // These settings give these inputs a height that matches @btn_mini_height = 22
+ padding-top: 3px;
+ padding-bottom: 3px;
+ height: @btn_mini_height;
+ line-height: 14px;
+ margin: 0px;
+}
+
+.highlight_text {
+ color: blue;
+}
+
+#project_name {
+ display: inline-block;
+ padding-left: @dashboard_lr_pad;
+ margin-left: -2px;
+
+ > .breadcrumb {
+ padding: 0px;
+ margin-bottom: 0px;
+ background-color: transparent;
+ font-weight: bold;
+ }
+}
+
+#tree-selector {
+ padding-right: 0px;
+}
+
+#button-select-all {
+ min-width: 50px;
+}
+
+#select-all {
+ margin-left: @dashboard_lr_pad;
+ margin-right: 2px;
+}
+
+.menu_icon {
+ margin-right: 2px;
+}
+
+.tab-content .row {
+ margin-left: 0px;
+ margin-right: 0px;
+}
+
+.folder_icon:before {
+ .icon(@fa-var-folder-o);
+}
+
+.notebook_icon:before {
+ .icon(@fa-var-book);
+ position: relative;
+ top: -1px;
+}
+
+.running_notebook_icon:before {
+ .icon(@fa-var-book);
+ position: relative;
+ top: -1px;
+
+ color: @brand-success;
+}
+
+
+.file_icon:before {
+ .icon(@fa-var-file-o);
+ position: relative;
+ top: -2px;
+}
+
+#notebook_toolbar .pull-right {
+ padding-top: 0px;
+ margin-right: -1px;
+}
+
+ul#new-menu {
+ // align right instead of left
+ left: auto;
+ right: 0;
+}
+
+.kernel-menu-icon {
+ padding-right: 12px;
+ width: 24px;
+ content: @fa-var-square-o;
+}
+
+.kernel-menu-icon:before {
+ content: @fa-var-square-o;
+}
+
+.kernel-menu-icon-current:before {
+ content: @fa-var-check;
+}
+
+#tab_content {
+ padding-top: @page-header-padding;
+}
+
+#running {
+ .panel-group{
+ .panel {
+ margin-top: 3px;
+ margin-bottom: 1em;
+
+ .panel-heading {
+ background-color: @page-backdrop-color;
+ padding-top: @dashboard_tb_pad;
+ padding-bottom: @dashboard_tb_pad;
+ padding-left: @dashboard_lr_pad;
+ padding-right: @dashboard_lr_pad;
+ line-height: @btn_mini_height;
+
+ a:focus, a:hover {
+ text-decoration: none;
+ }
+ }
+
+ .panel-body {
+ padding: 0px;
+
+ .list_container {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ border: 0px;
+ border-radius: 0px;
+
+ .list_item {
+ border-bottom: 1px solid @table-border-color;
+
+ &:last-child {
+ border-bottom: 0px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+.delete-button {
+ display: none;
+}
+
+.duplicate-button {
+ display: none;
+}
+
+.rename-button {
+ display: none;
+}
+
+.shutdown-button {
+ display: none;
+}
+
+.dynamic-instructions {
+ display: inline-block;
+ padding-top: @dashboard_tb_pad;
+}
diff --git a/notebook/templates/404.html b/notebook/templates/404.html
new file mode 100644
index 0000000..7335051
--- /dev/null
+++ b/notebook/templates/404.html
@@ -0,0 +1,5 @@
+{% extends "error.html" %}
+{% block error_detail %}
+<p>You are requesting a page that does not exist!</p>
+{% endblock %}
+
diff --git a/notebook/templates/edit.html b/notebook/templates/edit.html
new file mode 100644
index 0000000..b83988d
--- /dev/null
+++ b/notebook/templates/edit.html
@@ -0,0 +1,105 @@
+{% extends "page.html" %}
+
+{% block title %}{{page_title}}{% endblock %}
+
+{% block stylesheet %}
+<link rel="stylesheet" href="{{ static_url('components/codemirror/lib/codemirror.css') }}">
+<link rel="stylesheet" href="{{ static_url('components/codemirror/addon/dialog/dialog.css') }}">
+{{super()}}
+{% endblock %}
+
+{% block bodyclasses %}edit_app {{super()}}{% endblock %}
+
+{% block params %}
+data-base-url="{{base_url | urlencode}}"
+data-file-path="{{file_path}}"
+{{super()}}
+{% endblock %}
+
+{% block headercontainer %}
+
+<span id="save_widget" class="pull-left save_widget">
+ <span class="filename"></span>
+ <span class="last_modified"></span>
+</span>
+
+{% endblock %}
+
+{% block header %}
+
+<div id="menubar-container" class="container">
+ <div id="menubar">
+ <div id="menus" class="navbar navbar-default" role="navigation">
+ <div class="container-fluid">
+ <p class="navbar-text indicator_area">
+ <span id="current-mode" >current mode</span>
+ </p>
+ <button type="button" class="btn btn-default navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+ <i class="fa fa-bars"></i>
+ <span class="navbar-text">Menu</span>
+ </button>
+ <ul class="nav navbar-nav navbar-right">
+ <li id="notification_area"></li>
+ </ul>
+ <div class="navbar-collapse collapse">
+ <ul class="nav navbar-nav">
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
+ <ul id="file-menu" class="dropdown-menu">
+ <li id="new-file"><a href="#">New</a></li>
+ <li id="save-file"><a href="#">Save</a></li>
+ <li id="rename-file"><a href="#">Rename</a></li>
+ <li id="download-file"><a href="#">Download</a></li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
+ <ul id="edit-menu" class="dropdown-menu">
+ <li id="menu-find"><a href="#">Find</a></li>
+ <li id="menu-replace"><a href="#">Find &amp; Replace</a></li>
+ <li class="divider"></li>
+ <li class="dropdown-header">Key Map</li>
+ <li id="menu-keymap-default"><a href="#">Default<i class="fa"></i></a></li>
+ <li id="menu-keymap-sublime"><a href="#">Sublime Text<i class="fa"></i></a></li>
+ <li id="menu-keymap-vim"><a href="#">Vim<i class="fa"></i></a></li>
+ <li id="menu-keymap-emacs"><a href="#">emacs<i class="fa"></i></a></li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
+ <ul id="view-menu" class="dropdown-menu">
+ <li id="toggle_header" title="Show/Hide the logo and notebook title (above menu bar)">
+ <a href="#">Toggle Header</a></li>
+ <li id="menu-line-numbers"><a href="#">Toggle Line Numbers</a></li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Language</a>
+ <ul id="mode-menu" class="dropdown-menu">
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="lower-header-bar"></div>
+
+{% endblock %}
+
+{% block site %}
+
+<div id="texteditor-backdrop">
+<div id="texteditor-container" class="container"></div>
+</div>
+
+{% endblock %}
+
+{% block script %}
+
+ {{super()}}
+
+{% if ignore_minified_js %}
+ <script src="{{ static_url("edit/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
+{% else %}
+ <script src="{{ static_url("edit/js/main.min.js") }}" type="text/javascript" charset="utf-8"></script>
+{% endif %}
+{% endblock %}
diff --git a/notebook/templates/error.html b/notebook/templates/error.html
new file mode 100644
index 0000000..bedf06c
--- /dev/null
+++ b/notebook/templates/error.html
@@ -0,0 +1,31 @@
+{% extends "page.html" %}
+
+{% block login_widget %}
+{% endblock %}
+
+{% block stylesheet %}
+{{super()}}
+<style type="text/css">
+/* disable initial hide */
+div#header, div#site {
+ display: block;
+}
+</style>
+{% endblock %}
+{% block site %}
+
+<div class="error">
+ {% block h1_error %}
+ <h1>{{status_code}} : {{status_message}}</h1>
+ {% endblock h1_error %}
+ {% block error_detail %}
+ {% if message %}
+ <p>The error was:</p>
+ <div class="traceback-wrapper">
+ <pre class="traceback">{{message}}</pre>
+ </div>
+ {% endif %}
+ {% endblock %}
+</header>
+
+{% endblock %}
diff --git a/notebook/templates/login.html b/notebook/templates/login.html
new file mode 100644
index 0000000..46cd004
--- /dev/null
+++ b/notebook/templates/login.html
@@ -0,0 +1,57 @@
+{% extends "page.html" %}
+
+
+{% block stylesheet %}
+{{super()}}
+<link rel="stylesheet" href="{{ static_url("auth/css/override.css") }}" type="text/css" />
+{% endblock %}
+
+{% block login_widget %}
+{% endblock %}
+
+{% block site %}
+
+<div id="ipython-main-app" class="container">
+
+ {% if login_available %}
+ <div class="row">
+ <div class="navbar col-sm-8 col-sm-offset2">
+ <div class="navbar-inner">
+ <div class="container">
+ <div class="center-nav">
+ <p class="navbar-text nav">Password:</p>
+ <form action="{{base_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
+ <input type="password" name="password" id="password_input" class="form-control">
+ <button type="submit" id="login_submit">Log in</button>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ {% if message %}
+ <div class="row">
+ {% for key in message %}
+ <div class="message {{key}}">
+ {{message[key]}}
+ </div>
+ {% endfor %}
+ </div>
+ {% endif %}
+
+<div/>
+
+{% endblock %}
+
+
+{% block script %}
+{{super()}}
+
+<script type="text/javascript">
+ require(["auth/js/main"], function (auth) {
+ auth.login_main();
+ });
+</script>
+
+{% endblock %}
diff --git a/notebook/templates/logout.html b/notebook/templates/logout.html
new file mode 100644
index 0000000..81c3c91
--- /dev/null
+++ b/notebook/templates/logout.html
@@ -0,0 +1,43 @@
+{% extends "page.html" %}
+
+{% block stylesheet %}
+{{super()}}
+<link rel="stylesheet" href="{{ static_url("auth/css/override.css") }}" type="text/css" />
+{% endblock %}
+
+{% block login_widget %}
+{% endblock %}
+
+{% block site %}
+
+<div id="ipython-main-app" class="container">
+
+ {% if message %}
+ {% for key in message %}
+ <div class="message {{key}}">
+ {{message[key]}}
+ </div>
+ {% endfor %}
+ {% endif %}
+
+ {% if not login_available %}
+ Proceed to the <a href="{{base_url}}">dashboard</a>.
+ {% else %}
+ Proceed to the <a href="{{base_url}}login">login page</a>.
+ {% endif %}
+
+
+<div/>
+
+{% endblock %}
+
+{% block script %}
+{{super()}}
+
+<script type="text/javascript">
+ require(["auth/js/main"], function (auth) {
+ auth.logout_main();
+ });
+</script>
+
+{% endblock %}
diff --git a/notebook/templates/notebook.html b/notebook/templates/notebook.html
new file mode 100644
index 0000000..8790212
--- /dev/null
+++ b/notebook/templates/notebook.html
@@ -0,0 +1,353 @@
+{% extends "page.html" %}
+
+{% block stylesheet %}
+
+{% if mathjax_url %}
+<script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full,Safe&delayStartupUntil=configured" charset="utf-8"></script>
+{% endif %}
+<script type="text/javascript">
+// MathJax disabled, set as null to distingish from *missing* MathJax,
+// where it will be undefined, and should prompt a dialog later.
+window.mathjax_url = "{{mathjax_url}}";
+</script>
+
+<link rel="stylesheet" href="{{ static_url("components/bootstrap-tour/build/css/bootstrap-tour.min.css") }}" type="text/css" />
+<link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
+
+{{super()}}
+
+<link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
+<link rel="stylesheet" href="" id='kernel-css' type="text/css" />
+
+{% endblock %}
+
+{% block bodyclasses %}notebook_app {{super()}}{% endblock %}
+
+{% block params %}
+
+{{super()}}
+data-base-url="{{base_url | urlencode}}"
+data-ws-url="{{ws_url | urlencode}}"
+data-notebook-name="{{notebook_name | urlencode}}"
+data-notebook-path="{{notebook_path | urlencode}}"
+
+{% endblock %}
+
+
+{% block headercontainer %}
+
+
+<span id="save_widget" class="pull-left save_widget">
+ <span id="notebook_name" class="filename"></span>
+ <span class="checkpoint_status"></span>
+ <span class="autosave_status"></span>
+</span>
+
+<span id="kernel_logo_widget">
+ {% block kernel_logo_widget %}
+ <img class="current_kernel_logo" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
+ {% endblock %}
+</span>
+
+{% endblock headercontainer %}
+
+{% block header %}
+<div id="menubar-container" class="container">
+<div id="menubar">
+ <div id="menus" class="navbar navbar-default" role="navigation">
+ <div class="container-fluid">
+ <button type="button" class="btn btn-default navbar-btn navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+ <i class="fa fa-bars"></i>
+ <span class="navbar-text">Menu</span>
+ </button>
+ <p id="kernel_indicator" class="navbar-text indicator_area">
+ <span class="kernel_indicator_name">Kernel</span>
+ <i id="kernel_indicator_icon"></i>
+ </p>
+ <i id="readonly-indicator" class="navbar-text" title='This notebook is read-only'>
+ <span class="fa-stack">
+ <i class="fa fa-save fa-stack-1x"></i>
+ <i class="fa fa-ban fa-stack-2x text-danger"></i>
+ </span>
+ </i>
+ <i id="modal_indicator" class="navbar-text"></i>
+ <span id="notification_area"></span>
+ <div class="navbar-collapse collapse">
+ <ul class="nav navbar-nav">
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
+ <ul id="file_menu" class="dropdown-menu">
+ <li id="new_notebook" class="dropdown-submenu">
+ <a href="#">New Notebook</a>
+ <ul class="dropdown-menu" id="menu-new-notebook-submenu"></ul>
+ </li>
+ <li id="open_notebook"
+ title="Opens a new window with the Dashboard view">
+ <a href="#">Open...</a></li>
+ <!-- <hr/> -->
+ <li class="divider"></li>
+ <li id="copy_notebook"
+ title="Open a copy of this notebook's contents and start a new kernel">
+ <a href="#">Make a Copy...</a></li>
+ <li id="rename_notebook"><a href="#">Rename...</a></li>
+ <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
+ <!-- <hr/> -->
+ <li class="divider"></li>
+ <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
+ <ul class="dropdown-menu">
+ <li><a href="#"></a></li>
+ <li><a href="#"></a></li>
+ <li><a href="#"></a></li>
+ <li><a href="#"></a></li>
+ <li><a href="#"></a></li>
+ </ul>
+ </li>
+ <li class="divider"></li>
+ <li id="print_preview"><a href="#">Print Preview</a></li>
+ <li class="dropdown-submenu"><a href="#">Download as</a>
+ <ul class="dropdown-menu">
+ <li id="download_ipynb"><a href="#">Notebook (.ipynb)</a></li>
+ <li id="download_script"><a href="#">Script</a></li>
+ <li id="download_html"><a href="#">HTML (.html)</a></li>
+ <li id="download_markdown"><a href="#">Markdown (.md)</a></li>
+ <li id="download_rst"><a href="#">reST (.rst)</a></li>
+ <li id="download_pdf"><a href="#">PDF via LaTeX (.pdf)</a></li>
+ </ul>
+ </li>
+ <li class="divider"></li>
+ <li id="trust_notebook"
+ title="Trust the output of this notebook">
+ <a href="#" >Trust Notebook</a></li>
+ <li class="divider"></li>
+ <li id="kill_and_exit"
+ title="Shutdown this notebook's kernel, and close this window">
+ <a href="#" >Close and Halt</a></li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
+ <ul id="edit_menu" class="dropdown-menu">
+ <li id="cut_cell"><a href="#">Cut Cells</a></li>
+ <li id="copy_cell"><a href="#">Copy Cells</a></li>
+ <li id="paste_cell_above" class="disabled"><a href="#">Paste Cells Above</a></li>
+ <li id="paste_cell_below" class="disabled"><a href="#">Paste Cells Below</a></li>
+ <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cells &amp; Replace</a></li>
+ <li id="delete_cell"><a href="#">Delete Cells</a></li>
+ <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cells</a></li>
+ <li class="divider"></li>
+ <li id="split_cell"><a href="#">Split Cell</a></li>
+ <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
+ <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
+ <li class="divider"></li>
+ <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
+ <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
+ <li class="divider"></li>
+ <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
+ <li class="divider"></li>
+ <li id="find_and_replace"><a href="#"> Find and Replace </a></li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
+ <ul id="view_menu" class="dropdown-menu">
+ <li id="toggle_header"
+ title="Show/Hide the logo and notebook title (above menu bar)">
+ <a href="#">Toggle Header</a></li>
+ <li id="toggle_toolbar"
+ title="Show/Hide the action icons (below menu bar)">
+ <a href="#">Toggle Toolbar</a></li>
+ <li id="menu-cell-toolbar" class="dropdown-submenu">
+ <a href="#">Cell Toolbar</a>
+ <ul class="dropdown-menu" id="menu-cell-toolbar-submenu"></ul>
+ </li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
+ <ul id="insert_menu" class="dropdown-menu">
+ <li id="insert_cell_above"
+ title="Insert an empty Code cell above the currently active cell">
+ <a href="#">Insert Cell Above</a></li>
+ <li id="insert_cell_below"
+ title="Insert an empty Code cell below the currently active cell">
+ <a href="#">Insert Cell Below</a></li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
+ <ul id="cell_menu" class="dropdown-menu">
+ <li id="run_cell" title="Run this cell, and move cursor to the next one">
+ <a href="#">Run Cells</a></li>
+ <li id="run_cell_select_below" title="Run this cell, select below">
+ <a href="#">Run Cells and Select Below</a></li>
+ <li id="run_cell_insert_below" title="Run this cell, insert below">
+ <a href="#">Run Cells and Insert Below</a></li>
+ <li id="run_all_cells" title="Run all cells in the notebook">
+ <a href="#">Run All</a></li>
+ <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
+ <a href="#">Run All Above</a></li>
+ <li id="run_all_cells_below" title="Run this cell and all cells below it">
+ <a href="#">Run All Below</a></li>
+ <li class="divider"></li>
+ <li id="change_cell_type" class="dropdown-submenu"
+ title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
+ <a href="#">Cell Type</a>
+ <ul class="dropdown-menu">
+ <li id="to_code"
+ title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
+ <a href="#">Code</a></li>
+ <li id="to_markdown"
+ title="Contents will be rendered as HTML and serve as explanatory text">
+ <a href="#">Markdown</a></li>
+ <li id="to_raw"
+ title="Contents will pass through nbconvert unmodified">
+ <a href="#">Raw NBConvert</a></li>
+ </ul>
+ </li>
+ <li class="divider"></li>
+ <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Outputs</a>
+ <ul class="dropdown-menu">
+ <li id="toggle_current_output"
+ title="Hide/Show the output of the current cell">
+ <a href="#">Toggle</a>
+ </li>
+ <li id="toggle_current_output_scroll"
+ title="Scroll the output of the current cell">
+ <a href="#">Toggle Scrolling</a>
+ </li>
+ <li id="clear_current_output"
+ title="Clear the output of the current cell">
+ <a href="#">Clear</a>
+ </li>
+ </ul>
+ </li>
+ <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
+ <ul class="dropdown-menu">
+ <li id="toggle_all_output"
+ title="Hide/Show the output of all cells">
+ <a href="#">Toggle</a>
+ </li>
+ <li id="toggle_all_output_scroll"
+ title="Scroll the output of all cells">
+ <a href="#">Toggle Scrolling</a>
+ </li>
+ <li id="clear_all_output"
+ title="Clear the output of all cells">
+ <a href="#">Clear</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
+ <ul id="kernel_menu" class="dropdown-menu">
+ <li id="int_kernel"
+ title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
+ <a href="#">Interrupt</a>
+ </li>
+ <li id="restart_kernel"
+ title="Restart the Kernel">
+ <a href="#">Restart</a>
+ </li>
+ <li id="restart_clear_output"
+ title="Restart the Kernel and clear all output">
+ <a href="#">Restart &amp; Clear Output</a>
+ </li>
+ <li id="restart_run_all"
+ title="Restart the Kernel and re-run the notebook">
+ <a href="#">Restart &amp; Run All</a>
+ </li>
+ <li id="reconnect_kernel"
+ title="Reconnect to the Kernel">
+ <a href="#">Reconnect</a>
+ </li>
+ <li class="divider"></li>
+ <li id="menu-change-kernel" class="dropdown-submenu">
+ <a href="#">Change kernel</a>
+ <ul class="dropdown-menu" id="menu-change-kernel-submenu"></ul>
+ </li>
+ </ul>
+ </li>
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
+ <ul id="help_menu" class="dropdown-menu">
+ {% block help %}
+ <li id="notebook_tour" title="A quick tour of the notebook user interface"><a href="#">User Interface Tour</a></li>
+ <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
+ <li class="divider"></li>
+ {% set
+ sections = (
+ (
+ ("http://nbviewer.ipython.org/github/ipython/ipython/blob/3.x/examples/Notebook/Index.ipynb", "Notebook Help", True),
+ ("https://help.github.com/articles/markdown-basics/","Markdown",True),
+ ),
+ )
+ %}
+
+ {% for helplinks in sections %}
+ {% for link in helplinks %}
+ <li><a rel="noreferrer" href="{{link[0]}}" target="{{'_blank' if link[2]}}" title="{{'Opens in a new window' if link[2]}}">
+ {% if link[2] %}
+ <i class="fa fa-external-link menu-icon pull-right"></i>
+ {% endif %}
+
+ {{link[1]}}
+ </a></li>
+ {% endfor %}
+ {% if not loop.last %}
+ <li class="divider"></li>
+ {% endif %}
+ {% endfor %}
+ <li class="divider"></li>
+ <li title="About Jupyter Notebook"><a id="notebook_about" href="#">About</a></li>
+ {% endblock %}
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div id="maintoolbar" class="navbar">
+ <div class="toolbar-inner navbar-inner navbar-nobg">
+ <div id="maintoolbar-container" class="container"></div>
+ </div>
+</div>
+</div>
+
+<div class="lower-header-bar"></div>
+{% endblock header %}
+
+{% block site %}
+
+<div id="ipython-main-app">
+ <div id="notebook_panel">
+ <div id="notebook"></div>
+ <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
+ </div>
+</div>
+
+
+{% endblock %}
+
+{% block after_site %}
+
+<div id="pager">
+ <div id="pager-contents">
+ <div id="pager-container" class="container"></div>
+ </div>
+ <div id='pager-button-area'></div>
+</div>
+
+{% endblock %}
+
+{% block script %}
+{{super()}}
+<script type="text/javascript">
+ sys_info = {{sys_info|safe}};
+</script>
+
+<script src="{{ static_url("components/text-encoding/lib/encoding.js") }}" charset="utf-8"></script>
+
+{% if ignore_minified_js %}
+ <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
+{% else %}
+ <script src="{{ static_url("notebook/js/main.min.js") }}" type="text/javascript" charset="utf-8"></script>
+{% endif %}
+
+{% endblock %}
diff --git a/notebook/templates/page.html b/notebook/templates/page.html
new file mode 100644
index 0000000..3e03e85
--- /dev/null
+++ b/notebook/templates/page.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+
+ <title>{% block title %}Jupyter Notebook{% endblock %}</title>
+ {% block favicon %}<link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">{% endblock %}
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
+ <link rel="stylesheet" href="{{static_url("components/jquery-typeahead/dist/jquery.typeahead.min.css") }}" type="text/css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+ {% block stylesheet %}
+ <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
+ {% endblock %}
+ <link rel="stylesheet" href="{{ base_url }}custom/custom.css" type="text/css" />
+ <script src="{{static_url("components/es6-promise/promise.min.js")}}" type="text/javascript" charset="utf-8"></script>
+ <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
+ <script>
+ require.config({
+ {% if version_hash %}
+ urlArgs: "v={{version_hash}}",
+ {% endif %}
+ baseUrl: '{{static_url("", include_version=False)}}',
+ paths: {
+ 'auth/js/main': 'auth/js/main.min',
+ custom : '{{ base_url }}custom',
+ nbextensions : '{{ base_url }}nbextensions',
+ kernelspecs : '{{ base_url }}kernelspecs',
+ underscore : 'components/underscore/underscore-min',
+ backbone : 'components/backbone/backbone-min',
+ jquery: 'components/jquery/jquery.min',
+ bootstrap: 'components/bootstrap/js/bootstrap.min',
+ bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
+ 'jquery-ui': 'components/jquery-ui/ui/minified/jquery-ui.min',
+ moment: 'components/moment/moment',
+ codemirror: 'components/codemirror',
+ termjs: 'components/term.js/src/term',
+ typeahead: 'components/jquery-typeahead/dist/jquery.typeahead'
+ },
+ map: { // for backward compatibility
+ "*": {
+ "jqueryui": "jquery-ui",
+ }
+ },
+ shim: {
+ typeahead: {
+ deps: ["jquery"],
+ exports: "typeahead"
+ },
+ underscore: {
+ exports: '_'
+ },
+ backbone: {
+ deps: ["underscore", "jquery"],
+ exports: "Backbone"
+ },
+ bootstrap: {
+ deps: ["jquery"],
+ exports: "bootstrap"
+ },
+ bootstraptour: {
+ deps: ["bootstrap"],
+ exports: "Tour"
+ },
+ "jquery-ui": {
+ deps: ["jquery"],
+ exports: "$"
+ }
+ },
+ waitSeconds: 30,
+ });
+
+ require.config({
+ map: {
+ '*':{
+ 'contents': '{{ contents_js_source }}',
+ }
+ }
+ });
+
+ define("bootstrap", function () {
+ return window.$;
+ });
+
+ define("jquery", function () {
+ return window.$;
+ });
+
+ define("jqueryui", function () {
+ return window.$;
+ });
+
+ define("jquery-ui", function () {
+ return window.$;
+ });
+ // error-catching custom.js shim.
+ define("custom", function (require, exports, module) {
+ try {
+ var custom = require('custom/custom');
+ console.debug('loaded custom.js');
+ return custom;
+ } catch (e) {
+ console.error("error loading custom.js", e);
+ return {};
+ }
+ })
+ </script>
+
+ {% block meta %}
+ {% endblock %}
+
+</head>
+
+<body class="{% block bodyclasses %}{% endblock %}" {% block params %}{% endblock %}>
+
+<noscript>
+ <div id='noscript'>
+ Jupyter Notebook requires JavaScript.<br>
+ Please enable it to proceed.
+ </div>
+</noscript>
+
+<div id="header">
+ <div id="header-container" class="container">
+ <div id="ipython_notebook" class="nav navbar-brand pull-left"><a href="{{default_url}}" title='dashboard'>{% block logo %}<img src='{{static_url("base/images/logo.png") }}' alt='Jupyter Notebook'/>{% endblock %}</a></div>
+
+ {% block header_buttons %}
+
+ {% block login_widget %}
+
+ <span id="login_widget">
+ {% if logged_in %}
+ <button id="logout" class="btn btn-sm navbar-btn">Logout</button>
+ {% elif login_available and not logged_in %}
+ <button id="login" class="btn btn-sm navbar-btn">Login</button>
+ {% endif %}
+ </span>
+
+ {% endblock %}
+
+ {% endblock header_buttons %}
+
+ {% block headercontainer %}
+ {% endblock %}
+ </div>
+ <div class="header-bar"></div>
+
+ {% block header %}
+ {% endblock %}
+</div>
+
+<div id="site">
+{% block site %}
+{% endblock %}
+</div>
+
+{% block after_site %}
+{% endblock %}
+
+{% block script %}
+{% endblock %}
+
+</body>
+
+</html>
diff --git a/notebook/templates/terminal.html b/notebook/templates/terminal.html
new file mode 100644
index 0000000..230b781
--- /dev/null
+++ b/notebook/templates/terminal.html
@@ -0,0 +1,64 @@
+{% extends "page.html" %}
+
+{% block title %}{{page_title}}{% endblock %}
+
+{% block bodyclasses %}terminal-app {{super()}}{% endblock %}
+
+{% block params %}
+
+data-base-url="{{base_url | urlencode}}"
+data-ws-path="{{ws_path}}"
+
+{% endblock %}
+
+{% block stylesheet %}
+{{super()}}
+
+<link rel="stylesheet" href="{{ static_url("terminal/css/override.css") }}" type="text/css" />
+{% endblock %}
+
+{% block site %}
+
+<div id="terminado-container" class="container"></div>
+
+{% endblock %}
+
+{% block script %}
+
+<!-- Hack: this needs to be outside the display:none block, so we can measure
+ its size in JS in setting up the page. It is still invisible. Putting in
+ the script block gets it outside the initially undisplayed region. -->
+<!-- test size: 25x80 -->
+<div style='position:absolute; left:-1000em'>
+<pre id="dummy-screen" style="border: solid 5px white;" class="terminal">0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+0
+1
+2
+3
+<span id="dummy-screen-rows" style="">01234567890123456789012345678901234567890123456789012345678901234567890123456789</span>
+</pre>
+</div>
+
+ {{super()}}
+
+<script src="{{ static_url("terminal/js/main.min.js") }}" type="text/javascript" charset="utf-8"></script>
+{% endblock %}
diff --git a/notebook/templates/tree.html b/notebook/templates/tree.html
new file mode 100644
index 0000000..2d444d6
--- /dev/null
+++ b/notebook/templates/tree.html
@@ -0,0 +1,175 @@
+{% extends "page.html" %}
+
+{% block title %}{{page_title}}{% endblock %}
+
+
+{% block params %}
+{{super()}}
+data-base-url="{{base_url | urlencode}}"
+data-notebook-path="{{notebook_path | urlencode}}"
+data-terminals-available="{{terminals_available}}"
+
+{% endblock %}
+
+
+{% block site %}
+
+ <div id="ipython-main-app" class="container">
+ <div id="tab_content" class="tabbable">
+ <ul id="tabs" class="nav nav-tabs">
+ <li class="active"><a href="#notebooks" data-toggle="tab">Files</a></li>
+ <li><a href="#running" data-toggle="tab">Running</a></li>
+ <li><a href="#clusters" data-toggle="tab" class="clusters_tab_link" >Clusters</a></li>
+ </ul>
+ <div class="tab-content">
+ <div id="notebooks" class="tab-pane active">
+ <div id="notebook_toolbar" class="row">
+ <div class="col-sm-8 no-padding">
+ <div class="dynamic-instructions">
+ Select items to perform actions on them.
+ </div>
+ <div class="dynamic-buttons">
+ <button title="Duplicate selected" class="duplicate-button btn btn-default btn-xs">Duplicate</button>
+ <button title="Rename selected" class="rename-button btn btn-default btn-xs">Rename</button>
+ <button title="Shutdown selected notebook(s)" class="shutdown-button btn btn-default btn-xs btn-warning">Shutdown</button>
+ <button title="Delete selected" class="delete-button btn btn-default btn-xs btn-danger"><i class="fa fa-trash"></i></button>
+ </div>
+ </div>
+ <div class="col-sm-4 no-padding tree-buttons">
+ <div class="pull-right">
+ <form id='alternate_upload' class='alternate_upload'>
+ <span id="notebook_list_info">
+ <span class="btn btn-xs btn-default btn-upload">
+ <input title="Click to browse for a file to upload." type="file" name="datafile" class="fileinput" multiple='multiple'>
+ Upload
+ </span>
+ </span>
+ </form>
+ <div id="new-buttons" class="btn-group">
+ <button class="dropdown-toggle btn btn-default btn-xs" data-toggle="dropdown">
+ <span>New</span>
+ <span class="caret"></span>
+ </button>
+ <ul id="new-menu" class="dropdown-menu">
+ <li role="presentation" id="new-file">
+ <a role="menuitem" tabindex="-1" href="#">Text File</a>
+ </li>
+ <li role="presentation" id="new-folder">
+ <a role="menuitem" tabindex="-1" href="#">Folder</a>
+ </li>
+ {% if terminals_available %}
+ <li role="presentation" id="new-terminal">
+ <a role="menuitem" tabindex="-1" href="#">Terminal</a>
+ </li>
+ {% else %}
+ <li role="presentation" id="new-terminal-disabled" class="disabled">
+ <a role="menuitem" tabindex="-1" href="#">Terminals Unavailable</a>
+ </li>
+ {% endif %}
+ <li role="presentation" class="divider"></li>
+ <li role="presentation" class="dropdown-header" id="notebook-kernels">Notebooks</li>
+ </ul>
+ </div>
+ <div class="btn-group">
+ <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="notebook_list">
+ <div id="notebook_list_header" class="row list_header">
+ <div class="btn-group dropdown" id="tree-selector">
+ <button title="Select All / None" type="button" class="btn btn-default btn-xs" id="button-select-all">
+ <input type="checkbox" class="pull-left tree-selector" id="select-all"><span id="counter-select-all">&nbsp;</span></input>
+ </button>
+ <button title="Select..." class="btn btn-default btn-xs dropdown-toggle" type="button" id="tree-selector-btn" data-toggle="dropdown" aria-expanded="true">
+ <span class="caret"></span>
+ <span class="sr-only">Toggle Dropdown</span>
+ </button>
+ <ul id='selector-menu' class="dropdown-menu" role="menu" aria-labelledby="tree-selector-btn">
+ <li role="presentation"><a id="select-folders" role="menuitem" tabindex="-1" href="#" title="Select All Folders"><i class="menu_icon folder_icon icon-fixed-width"></i> Folders</a></li>
+ <li role="presentation"><a id="select-notebooks" role="menuitem" tabindex="-1" href="#" title="Select All Notebooks"><i class="menu_icon notebook_icon icon-fixed-width"></i> All Notebooks</a></li>
+ <li role="presentation"><a id="select-running-notebooks" role="menuitem" tabindex="-1" href="#" title="Select Running Notebooks"><i class="menu_icon running_notebook_icon icon-fixed-width"></i> Running</a></li>
+ <li role="presentation"><a id="select-files" role="menuitem" tabindex="-1" href="#" title="Select All Files"><i class="menu_icon file_icon icon-fixed-width"></i> Files</a></li>
+ </ul>
+ </div>
+ <div id="project_name">
+ <ul class="breadcrumb">
+ <li><a href="{{breadcrumbs[0][0]}}"><i class="fa fa-home"></i></a></li>
+ {% for crumb in breadcrumbs[1:] %}
+ <li><a href="{{crumb[0]}}">{{crumb[1]}}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="running" class="tab-pane">
+ <div id="running_toolbar" class="row">
+ <div class="col-sm-8 no-padding">
+ <span id="running_list_info">Currently running Jupyter processes</span>
+ </div>
+ <div class="col-sm-4 no-padding tree-buttons">
+ <span id="running_buttons" class="pull-right">
+ <button id="refresh_running_list" title="Refresh running list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
+ </span>
+ </div>
+ </div>
+ <div class="panel-group" id="accordion" >
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <a data-toggle="collapse" data-target="#collapseOne" href="#">
+ Terminals
+ </a>
+ </div>
+ <div id="collapseOne" class=" collapse in">
+ <div class="panel-body">
+ <div id="terminal_list">
+ <div id="terminal_list_header" class="row list_placeholder">
+ {% if terminals_available %}
+ <div> There are no terminals running. </div>
+ {% else %}
+ <div> Terminals are unavailable. </div>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <a data-toggle="collapse" data-target="#collapseTwo" href="#">
+ Notebooks
+ </a>
+ </div>
+ <div id="collapseTwo" class=" collapse in">
+ <div class="panel-body">
+ <div id="running_list">
+ <div id="running_list_placeholder" class="row list_placeholder">
+ <div> There are no notebooks running. </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="clusters" class="tab-pane">
+ Clusters tab is now provided by IPython parallel.
+ See <a href="https://github.com/ipython/ipyparallel">IPython parallel</a> for installation details.
+ </div>
+ </div><!-- class:tab-content -->
+ </div><!-- id:tab_content -->
+ </div><!-- ipython-main-app -->
+
+{% endblock %}
+
+{% block script %}
+ {{super()}}
+
+{% if ignore_minified_js %}
+ <script src="{{ static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
+{% else %}
+ <script src="{{ static_url("tree/js/main.min.js") }}" type="text/javascript" charset="utf-8"></script>
+{% endif %}
+{% endblock %}
diff --git a/notebook/terminal/__init__.py b/notebook/terminal/__init__.py
new file mode 100644
index 0000000..9092e13
--- /dev/null
+++ b/notebook/terminal/__init__.py
@@ -0,0 +1,32 @@
+import os
+
+import terminado
+from ..utils import check_version
+
+if not check_version(terminado.__version__, '0.3.3'):
+ raise ImportError("terminado >= 0.3.3 required, found %s" % terminado.__version__)
+
+from terminado import NamedTermManager
+from tornado.log import app_log
+from notebook.utils import url_path_join as ujoin
+from .handlers import TerminalHandler, TermSocket
+from . import api_handlers
+
+def initialize(webapp, notebook_dir, connection_url):
+ shell = os.environ.get('SHELL') or 'sh'
+ terminal_manager = webapp.settings['terminal_manager'] = NamedTermManager(
+ shell_command=[shell],
+ extra_env={'JUPYTER_SERVER_ROOT': notebook_dir,
+ 'JUPYTER_SERVER_URL': connection_url,
+ },
+ )
+ terminal_manager.log = app_log
+ base_url = webapp.settings['base_url']
+ handlers = [
+ (ujoin(base_url, r"/terminals/(\w+)"), TerminalHandler),
+ (ujoin(base_url, r"/terminals/websocket/(\w+)"), TermSocket,
+ {'term_manager': terminal_manager}),
+ (ujoin(base_url, r"/api/terminals"), api_handlers.TerminalRootHandler),
+ (ujoin(base_url, r"/api/terminals/(\w+)"), api_handlers.TerminalHandler),
+ ]
+ webapp.add_handlers(".*$", handlers)
diff --git a/notebook/terminal/api_handlers.py b/notebook/terminal/api_handlers.py
new file mode 100644
index 0000000..a7be0ae
--- /dev/null
+++ b/notebook/terminal/api_handlers.py
@@ -0,0 +1,44 @@
+import json
+from tornado import web, gen
+from ..base.handlers import APIHandler, json_errors
+from ..utils import url_path_join
+
+class TerminalRootHandler(APIHandler):
+ @web.authenticated
+ @json_errors
+ def get(self):
+ tm = self.terminal_manager
+ terms = [{'name': name} for name in tm.terminals]
+ self.finish(json.dumps(terms))
+
+ @web.authenticated
+ @json_errors
+ def post(self):
+ """POST /terminals creates a new terminal and redirects to it"""
+ name, _ = self.terminal_manager.new_named_terminal()
+ self.finish(json.dumps({'name': name}))
+
+
+class TerminalHandler(APIHandler):
+ SUPPORTED_METHODS = ('GET', 'DELETE')
+
+ @web.authenticated
+ @json_errors
+ def get(self, name):
+ tm = self.terminal_manager
+ if name in tm.terminals:
+ self.finish(json.dumps({'name': name}))
+ else:
+ raise web.HTTPError(404, "Terminal not found: %r" % name)
+
+ @web.authenticated
+ @json_errors
+ @gen.coroutine
+ def delete(self, name):
+ tm = self.terminal_manager
+ if name in tm.terminals:
+ yield tm.terminate(name, force=True)
+ self.set_status(204)
+ self.finish()
+ else:
+ raise web.HTTPError(404, "Terminal not found: %r" % name)
diff --git a/notebook/terminal/handlers.py b/notebook/terminal/handlers.py
new file mode 100644
index 0000000..a1e123a
--- /dev/null
+++ b/notebook/terminal/handlers.py
@@ -0,0 +1,34 @@
+#encoding: utf-8
+"""Tornado handlers for the terminal emulator."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from tornado import web
+import terminado
+from ..base.handlers import IPythonHandler
+from ..base.zmqhandlers import WebSocketMixin
+
+
+class TerminalHandler(IPythonHandler):
+ """Render the terminal interface."""
+ @web.authenticated
+ def get(self, term_name):
+ self.write(self.render_template('terminal.html',
+ ws_path="terminals/websocket/%s" % term_name))
+
+
+class TermSocket(WebSocketMixin, IPythonHandler, terminado.TermSocket):
+
+ def origin_check(self):
+ """Terminado adds redundant origin_check
+
+ Tornado already calls check_origin, so don't do anything here.
+ """
+ return True
+
+ def get(self, *args, **kwargs):
+ if not self.get_current_user():
+ raise web.HTTPError(403)
+ return super(TermSocket, self).get(*args, **kwargs)
+
diff --git a/notebook/tests/README.md b/notebook/tests/README.md
new file mode 100644
index 0000000..b5c04c4
--- /dev/null
+++ b/notebook/tests/README.md
@@ -0,0 +1,28 @@
+# IPython Notebook JavaScript Tests
+
+This directory includes regression tests for the web notebook. These tests
+depend on [CasperJS](http://casperjs.org/), which in turn requires a recent
+version of [PhantomJS](http://phantomjs.org/).
+
+The JavaScript tests are organized into subdirectories that match those in
+`static` (`base', `notebook`, `services`, `tree`, etc.).
+
+To run all of the JavaScript tests do:
+
+```
+iptest js
+```
+
+To run the JavaScript tests in a single subdirectory (`notebook` in this
+case) do:
+
+```
+iptest js/notebook
+```
+
+The file `util.js` contains utility functions for tests, including a path to
+a running notebook server on localhost (http://127.0.0.1) with the port
+number specified as a command line argument to the test suite. Port 8888 is
+used if `--port=` is not specified. When you run these tests using `iptest`
+you do not, however, have to start a notebook server yourself; that is done
+automatically.
diff --git a/notebook/tests/__init__.py b/notebook/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/tests/__init__.py
diff --git a/notebook/tests/base/highlight.js b/notebook/tests/base/highlight.js
new file mode 100644
index 0000000..155d13f
--- /dev/null
+++ b/notebook/tests/base/highlight.js
@@ -0,0 +1,58 @@
+casper.notebook_test(function () {
+ this.on('remote.callback', function(data){
+ if(data.error_expected){
+ that.test.assertEquals(
+ data.error,
+ true,
+ "!highlight: " + data.provided + " errors"
+ );
+ }else{
+ that.test.assertEquals(
+ data.observed,
+ data.expected,
+ "highlight: " + data.provided + " as " + data.expected
+ );
+ }
+ });
+
+ var that = this;
+ // syntax highlighting
+ [
+ {to: "gfm"},
+ {to: "python"},
+ {to: "ipython"},
+ {to: "ipythongfm"},
+ {to: "text/x-markdown", from: [".md"]},
+ {to: "text/x-python", from: [".py", "Python"]},
+ {to: "application/json", from: ["json", "JSON"]},
+ {to: "text/x-ruby", from: [".rb", "ruby", "Ruby"]},
+ {to: "application/ld+json", from: ["json-ld", "JSON-LD"]},
+ {from: [".pyc"], error: true},
+ {from: ["../"], error: true},
+ {from: ["//"], error: true},
+ ].map(function (mode) {
+ (mode.from || []).concat(mode.to || []).map(function(from){
+ casper.evaluate(function(from, expected, error_expected){
+ IPython.utils.requireCodeMirrorMode(from, function(observed){
+ window.callPhantom({
+ provided: from,
+ expected: expected,
+ observed: observed,
+ error_expected: error_expected
+ });
+ }, function(error){
+ window.callPhantom({
+ provided: from,
+ expected: expected,
+ error: true,
+ error_expected: error_expected
+ });
+ });
+ }, {
+ from: from,
+ expected: mode.to,
+ error_expected: mode.error
+ });
+ });
+ });
+}); \ No newline at end of file
diff --git a/notebook/tests/base/keyboard.js b/notebook/tests/base/keyboard.js
new file mode 100644
index 0000000..f29a358
--- /dev/null
+++ b/notebook/tests/base/keyboard.js
@@ -0,0 +1,91 @@
+
+
+var normalized_shortcuts = [
+ 'ctrl-shift-m',
+ 'alt-meta-p',
+];
+
+var to_normalize = [
+ ['shift-%', 'shift-5'],
+ ['ShiFT-MeTa-CtRl-AlT-m', 'alt-ctrl-meta-shift-m'],
+];
+
+var unshifted = "` 1 2 3 4 5 6 7 8 9 0 - = q w e r t y u i o p [ ] \\ a s d f g h j k l ; ' z x c v b n m , . /";
+// shifted = '~ ! @ # $ % ^ & * ( ) _ + Q W E R T Y U I O P { } | A S D F G H J K L : " Z X C V B N M < > ?';
+
+var ambiguous_expect = function(ch){
+ // `-` is ambiguous in shortcut context as a separator, so map it to `minus`
+ if(ch === '-'){
+ return 'minus';
+ }
+ return ch;
+};
+
+casper.notebook_test(function () {
+ var that = this;
+
+ this.then(function () {
+ this.each(unshifted.split(' '), function (self, item) {
+ var result = this.evaluate(function (sc) {
+ var e = IPython.keyboard.shortcut_to_event(sc);
+ var sc2 = IPython.keyboard.event_to_shortcut(e);
+ return sc2;
+ }, item);
+ this.test.assertEquals(result, ambiguous_expect(item), 'Shortcut to event roundtrip: '+item);
+ });
+ });
+
+ this.then(function () {
+ this.each(to_normalize, function (self, item) {
+ var result = this.evaluate(function (pair) {
+ return IPython.keyboard.normalize_shortcut(pair[0]);
+ }, item);
+ this.test.assertEquals(result, item[1], 'Normalize shortcut: '+item[0]);
+ });
+ });
+
+ this.then(function () {
+ this.each(normalized_shortcuts, function (self, item) {
+ var result = this.evaluate(function (sc) {
+ var e = IPython.keyboard.shortcut_to_event(sc);
+ var sc2 = IPython.keyboard.event_to_shortcut(e);
+ return sc2;
+ }, item);
+ this.test.assertEquals(result, item, 'Shortcut to event roundtrip: '+item);
+ });
+ });
+
+ this.then(function(){
+
+ var shortcuts_test = {
+ 'i,e,e,e,e,e' : '[[5E]]',
+ 'i,d,d,q,d' : '[[TEST1]]',
+ 'i,d,d' : '[[TEST1 WILL BE SHADOWED]]',
+ 'i,d,k' : '[[SHOULD SHADOW TEST2]]',
+ 'i,d,k,r,q' : '[[TEST2 NOT SHADOWED]]',
+ ';,up,down,up,down,left,right,left,right,b,a' : '[[KONAMI]]',
+ 'ctrl-x,meta-c,meta-b,u,t,t,e,r,f,l,y' : '[[XKCD]]'
+
+ };
+
+ that.msgs = [];
+ that.on('remote.message', function(msg) {
+ that.msgs.push(msg);
+ })
+ that.evaluate(function (obj) {
+ for(var k in obj){
+ IPython.keyboard_manager.command_shortcuts.add_shortcut(k, function(){console.log(obj[k])});
+ }
+ }, shortcuts_test);
+
+ var longer_first = false;
+ var longer_last = false;
+ for(var m in that.msgs){
+ longer_first = longer_first||(that.msgs[m].match(/you are overriting/)!= null);
+ longer_last = longer_last ||(that.msgs[m].match(/will be shadowed/) != null);
+ }
+ this.test.assert(longer_first, 'no warning if registering shorter shortut');
+ this.test.assert(longer_last , 'no warning if registering longer shortut');
+ })
+
+});
diff --git a/notebook/tests/base/misc.js b/notebook/tests/base/misc.js
new file mode 100644
index 0000000..311566c
--- /dev/null
+++ b/notebook/tests/base/misc.js
@@ -0,0 +1,21 @@
+
+//
+// Miscellaneous javascript tests
+//
+casper.notebook_test(function () {
+ var jsver = this.evaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text('import notebook; print(notebook.__version__)');
+ cell.execute();
+ return IPython.version;
+ });
+
+ this.wait_for_output(0);
+
+ // refactor this into just a get_output(0)
+ this.then(function () {
+ var result = this.get_output_cell(0);
+ this.test.assertEquals(result.text.trim(), jsver, 'IPython.version in JS matches server-side.');
+ });
+
+});
diff --git a/notebook/tests/base/security.js b/notebook/tests/base/security.js
new file mode 100644
index 0000000..af23e66
--- /dev/null
+++ b/notebook/tests/base/security.js
@@ -0,0 +1,57 @@
+safe_tests = [
+ "<p>Hi there</p>",
+ '<h1 class="foo">Hi There!</h1>',
+ '<a data-cite="foo">citation</a>',
+ '<div><span>Hi There</span></div>',
+];
+
+unsafe_tests = [
+ "<script>alert(999);</script>",
+ '<a onmouseover="alert(999)">999</a>',
+ '<a onmouseover=alert(999)>999</a>',
+ '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
+ '<IMG SRC=# onmouseover="alert(999)">',
+ '<<SCRIPT>alert(999);//<</SCRIPT>',
+ '<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >',
+ '<META HTTP-EQUIV="refresh" CONTENT="0;url=data:text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">',
+ '<META HTTP-EQUIV="refresh" CONTENT="0; URL=http://;URL=javascript:alert(999);">',
+ '<IFRAME SRC="javascript:alert(999);"></IFRAME>',
+ '<IFRAME SRC=# onmouseover="alert(document.cookie)"></IFRAME>',
+ '<EMBED SRC="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==" type="image/svg+xml" AllowScriptAccess="always"></EMBED>',
+ // CSS is scrubbed
+ '<style src="http://untrusted/style.css"></style>',
+ '<style>div#notebook { background-color: alert-red; }</style>',
+ '<div style="background-color: alert-red;"></div>',
+];
+
+var truncate = function (s, n) {
+ // truncate a string with an ellipsis
+ if (s.length > n) {
+ return s.substr(0, n-3) + '...';
+ } else {
+ return s;
+ }
+};
+
+casper.notebook_test(function () {
+ this.each(safe_tests, function (self, item) {
+ var sanitized = self.evaluate(function (item) {
+ return IPython.security.sanitize_html(item);
+ }, item);
+
+ // string equality may be too strict, but it works for now
+ this.test.assertEquals(sanitized, item, "Safe: '" + truncate(item, 32) + "'");
+ });
+
+ this.each(unsafe_tests, function (self, item) {
+ var sanitized = self.evaluate(function (item) {
+ return IPython.security.sanitize_html(item);
+ }, item);
+
+ this.test.assertNotEquals(sanitized, item,
+ "Sanitized: '" + truncate(item, 32) +
+ "' => '" + truncate(sanitized, 32) + "'"
+ );
+ this.test.assertEquals(sanitized.indexOf("alert"), -1, "alert removed");
+ });
+}); \ No newline at end of file
diff --git a/notebook/tests/base/utils.js b/notebook/tests/base/utils.js
new file mode 100644
index 0000000..525b608
--- /dev/null
+++ b/notebook/tests/base/utils.js
@@ -0,0 +1,44 @@
+casper.notebook_test(function () {
+ // Note, \033 is the octal notation of \u001b
+ var input = [
+ "\033[0m[\033[0minfo\033[0m] \033[0mtext\033[0m",
+ "\033[0m[\033[33mwarn\033[0m] \033[0m\tmore text\033[0m",
+ "\033[0m[\033[33mwarn\033[0m] \033[0m https://some/url/to/a/file.ext\033[0m",
+ "\033[0m[\033[31merror\033[0m] \033[0m\033[0m",
+ "\033[0m[\033[31merror\033[0m] \033[0m\teven more text\033[0m",
+ "\u001b[?25hBuilding wheels for collected packages: scipy",
+ "\x1b[38;5;28;01mtry\x1b[39;00m",
+ "\033[0m[\033[31merror\033[0m] \033[0m\t\tand more more text\033[0m"].join("\n");
+
+ var output = [
+ "[info] text",
+ "[<span class=\"ansiyellow\">warn</span>] \tmore text",
+ "[<span class=\"ansiyellow\">warn</span>] https://some/url/to/a/file.ext",
+ "[<span class=\"ansired\">error</span>] ",
+ "[<span class=\"ansired\">error</span>] \teven more text",
+ "Building wheels for collected packages: scipy",
+ '<span style="color: rgb(0,102,0);" class="ansibold">try</span>',
+ "[<span class=\"ansired\">error</span>] \t\tand more more text"].join("\n");
+
+ var result = this.evaluate(function (input) {
+ return IPython.utils.fixConsole(input);
+ }, input);
+
+ this.test.assertEquals(result, output, "IPython.utils.fixConsole() handles [0m correctly");
+
+ this.thenEvaluate(function() {
+ define('nbextensions/a', [], function() { window.a = true; });
+ define('nbextensions/c', [], function() { window.c = true; });
+ require(['base/js/utils'], function(utils) {
+ utils.load_extensions('a', 'b', 'c');
+ });
+ }).then(function() {
+ this.waitFor(function() {
+ return this.evaluate(function() { return window.a; });
+ });
+
+ this.waitFor(function() {
+ return this.evaluate(function() { return window.a; });
+ });
+ });
+});
diff --git a/notebook/tests/launchnotebook.py b/notebook/tests/launchnotebook.py
new file mode 100644
index 0000000..1986518
--- /dev/null
+++ b/notebook/tests/launchnotebook.py
@@ -0,0 +1,161 @@
+"""Base class for notebook tests."""
+
+from __future__ import print_function
+
+import os
+import sys
+import time
+import requests
+from contextlib import contextmanager
+from threading import Thread, Event
+from unittest import TestCase
+
+pjoin = os.path.join
+
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch #py2
+
+from tornado.ioloop import IOLoop
+import zmq
+
+import jupyter_core.paths
+from ..notebookapp import NotebookApp
+from ipython_genutils.tempdir import TemporaryDirectory
+
+MAX_WAITTIME = 30 # seconds to wait for notebook server to start
+POLL_INTERVAL = 0.1 # time between attempts
+
+# TimeoutError is a builtin on Python 3. This can be removed when we stop
+# supporting Python 2.
+class TimeoutError(Exception):
+ pass
+
+class NotebookTestBase(TestCase):
+ """A base class for tests that need a running notebook.
+
+ This create some empty config and runtime directories
+ and then starts the notebook server with them.
+ """
+
+ port = 12341
+ config = None
+ # run with a base URL that would be escaped,
+ # to test that we don't double-escape URLs
+ url_prefix = '/a%40b/'
+
+ @classmethod
+ def wait_until_alive(cls):
+ """Wait for the server to be alive"""
+ url = cls.base_url() + 'api/contents'
+ for _ in range(int(MAX_WAITTIME/POLL_INTERVAL)):
+ try:
+ requests.get(url)
+ except Exception as e:
+ if not cls.notebook_thread.is_alive():
+ raise RuntimeError("The notebook server failed to start")
+ time.sleep(POLL_INTERVAL)
+ else:
+ return
+
+ raise TimeoutError("The notebook server didn't start up correctly.")
+
+ @classmethod
+ def wait_until_dead(cls):
+ """Wait for the server process to terminate after shutdown"""
+ cls.notebook_thread.join(timeout=MAX_WAITTIME)
+ if cls.notebook_thread.is_alive():
+ raise TimeoutError("Undead notebook server")
+
+ @classmethod
+ def setup_class(cls):
+ cls.home_dir = TemporaryDirectory()
+ data_dir = TemporaryDirectory()
+ cls.env_patch = patch.dict('os.environ', {
+ 'HOME': cls.home_dir.name,
+ 'IPYTHONDIR': pjoin(cls.home_dir.name, '.ipython'),
+ 'JUPYTER_DATA_DIR' : data_dir.name
+ })
+ cls.env_patch.start()
+ cls.path_patch = patch.object(jupyter_core.paths, 'SYSTEM_JUPYTER_PATH', [])
+ cls.path_patch.start()
+ cls.config_dir = TemporaryDirectory()
+ cls.data_dir = data_dir
+ cls.runtime_dir = TemporaryDirectory()
+ cls.notebook_dir = TemporaryDirectory()
+
+ started = Event()
+ def start_thread():
+ app = cls.notebook = NotebookApp(
+ port=cls.port,
+ port_retries=0,
+ open_browser=False,
+ config_dir=cls.config_dir.name,
+ data_dir=cls.data_dir.name,
+ runtime_dir=cls.runtime_dir.name,
+ notebook_dir=cls.notebook_dir.name,
+ base_url=cls.url_prefix,
+ config=cls.config,
+ )
+ # don't register signal handler during tests
+ app.init_signal = lambda : None
+ # clear log handlers and propagate to root for nose to capture it
+ # needs to be redone after initialize, which reconfigures logging
+ app.log.propagate = True
+ app.log.handlers = []
+ app.initialize(argv=[])
+ app.log.propagate = True
+ app.log.handlers = []
+ loop = IOLoop.current()
+ loop.add_callback(started.set)
+ try:
+ app.start()
+ finally:
+ # set the event, so failure to start doesn't cause a hang
+ started.set()
+ app.session_manager.close()
+ cls.notebook_thread = Thread(target=start_thread)
+ cls.notebook_thread.start()
+ started.wait()
+ cls.wait_until_alive()
+
+ @classmethod
+ def teardown_class(cls):
+ cls.notebook.stop()
+ cls.wait_until_dead()
+ cls.home_dir.cleanup()
+ cls.config_dir.cleanup()
+ cls.data_dir.cleanup()
+ cls.runtime_dir.cleanup()
+ cls.notebook_dir.cleanup()
+ cls.env_patch.stop()
+ cls.path_patch.stop()
+ # cleanup global zmq Context, to ensure we aren't leaving dangling sockets
+ def cleanup_zmq():
+ zmq.Context.instance().term()
+ t = Thread(target=cleanup_zmq)
+ t.daemon = True
+ t.start()
+ t.join(5) # give it a few seconds to clean up (this should be immediate)
+ # if term never returned, there's zmq stuff still open somewhere, so shout about it.
+ if t.is_alive():
+ raise RuntimeError("Failed to teardown zmq Context, open sockets likely left lying around.")
+
+ @classmethod
+ def base_url(cls):
+ return 'http://localhost:%i%s' % (cls.port, cls.url_prefix)
+
+
+@contextmanager
+def assert_http_error(status, msg=None):
+ try:
+ yield
+ except requests.HTTPError as e:
+ real_status = e.response.status_code
+ assert real_status == status, \
+ "Expected status %d, got %d" % (status, real_status)
+ if msg:
+ assert msg in str(e), e
+ else:
+ assert False, "Expected HTTP error status" \ No newline at end of file
diff --git a/notebook/tests/mockextension/index.js b/notebook/tests/mockextension/index.js
new file mode 100644
index 0000000..0475609
--- /dev/null
+++ b/notebook/tests/mockextension/index.js
@@ -0,0 +1 @@
+console.log('z');
diff --git a/notebook/tests/notebook/buffering.js b/notebook/tests/notebook/buffering.js
new file mode 100644
index 0000000..34f3224
--- /dev/null
+++ b/notebook/tests/notebook/buffering.js
@@ -0,0 +1,56 @@
+//
+// Test buffering for execution requests.
+//
+casper.notebook_test(function () {
+ this.then(function() {
+ // make sure there are at least three cells for the tests below.
+ this.append_cell();
+ this.append_cell();
+ this.append_cell();
+ })
+
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.stop_channels();
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text('a=10; print(a)');
+ IPython.notebook.execute_cells([0]);
+ IPython.notebook.kernel.reconnect(1);
+ });
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var result = this.get_output_cell(0);
+ this.test.assertEquals(result.text, '10\n', 'kernels buffer messages if connection is down');
+ });
+
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ var cellplus = IPython.notebook.get_cell(1);
+ var cellprint = IPython.notebook.get_cell(2);
+ cell.set_text('k=1');
+ cellplus.set_text('k+=1');
+ cellprint.set_text('k*=2')
+
+ IPython.notebook.kernel.stop_channels();
+
+ // Repeated execution of cell queued up in the kernel executes
+ // each execution request in order.
+ IPython.notebook.execute_cells([0]);
+ IPython.notebook.execute_cells([2]);
+ IPython.notebook.execute_cells([1]);
+ IPython.notebook.execute_cells([1]);
+ IPython.notebook.execute_cells([1]);
+ cellprint.set_text('print(k)')
+ IPython.notebook.execute_cells([2]);
+
+ IPython.notebook.kernel.reconnect(1);
+ });
+
+ this.wait_for_output(2);
+
+ this.then(function () {
+ var result = this.get_output_cell(2);
+ this.test.assertEquals(result.text, '5\n', 'kernels send buffered messages in order');
+ });
+});
diff --git a/notebook/tests/notebook/clipboard_multiselect.js b/notebook/tests/notebook/clipboard_multiselect.js
new file mode 100644
index 0000000..422693e
--- /dev/null
+++ b/notebook/tests/notebook/clipboard_multiselect.js
@@ -0,0 +1,41 @@
+
+
+// Test
+casper.notebook_test(function () {
+ this.append_cell('1');
+ this.append_cell('2');
+ this.append_cell('3');
+ this.append_cell('4');
+ this.append_cell('a5');
+ this.append_cell('b6');
+ this.append_cell('c7');
+ this.append_cell('d8');
+
+
+
+ this.then(function () {
+ // Copy/paste/cut
+ this.select_cell(1);
+ this.select_cell(3, false);
+
+ this.trigger_keydown('c'); // Copy
+
+ this.select_cell(6)
+ this.select_cell(7,false)
+
+ this.evaluate(function () {
+ $("#paste_cell_replace").click();
+ });
+
+ var expected_state = ['', '1', '2', '3', '4', 'a5', '1' ,'2' ,'3', 'd8'];
+
+ for (var i=1; i<expected_state.length; i++){
+ this.test.assertEquals(this.get_cell_text(i), expected_state[i],
+ 'Verify that cell `' + i + '` has for content: `'+ expected_state[i] + '` found : ' + this.get_cell_text(i)
+ );
+ }
+
+ this.validate_notebook_state('paste-replace', 'command', 8)
+
+ });
+});
diff --git a/notebook/tests/notebook/deletecell.js b/notebook/tests/notebook/deletecell.js
new file mode 100644
index 0000000..6c98f28
--- /dev/null
+++ b/notebook/tests/notebook/deletecell.js
@@ -0,0 +1,107 @@
+
+// Test
+casper.notebook_test(function () {
+ var that = this;
+ var cell_is_deletable = function (index) {
+ // Get the deletable status of a cell.
+ return that.evaluate(function (index) {
+ var cell = IPython.notebook.get_cell(index);
+ return cell.is_deletable();
+ }, index);
+ };
+
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+
+ var b = 'print("b")';
+ index = this.append_cell(b);
+
+ var c = 'print("c")';
+ index = this.append_cell(c);
+
+ this.thenEvaluate(function() {
+ IPython.notebook.get_cell(1).metadata.deletable = false;
+ IPython.notebook.get_cell(2).metadata.deletable = 0; // deletable only when exactly false
+ IPython.notebook.get_cell(3).metadata.deletable = true;
+ });
+
+ this.then(function () {
+ // Check deletable status of the cells
+ this.test.assert(cell_is_deletable(0), 'Cell 0 is deletable');
+ this.test.assert(!cell_is_deletable(1), 'Cell 1 is not deletable');
+ this.test.assert(cell_is_deletable(2), 'Cell 2 is deletable');
+ this.test.assert(cell_is_deletable(3), 'Cell 3 is deletable');
+ });
+
+ // Try to delete cell 0 (should succeed)
+ this.then(function () {
+ this.select_cell(0);
+ this.trigger_keydown('esc');
+ this.trigger_keydown('d', 'd');
+ this.test.assertEquals(this.get_cells_length(), 3, 'Delete cell 0: There are now 3 cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 0: Cell 1 is now cell 0');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Delete cell 0: Cell 2 is now cell 1');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Delete cell 0: Cell 3 is now cell 2');
+ this.validate_notebook_state('dd', 'command', 0);
+ });
+
+ // Try to delete cell 0 (should fail)
+ this.then(function () {
+ this.select_cell(0);
+ this.trigger_keydown('d', 'd');
+ this.test.assertEquals(this.get_cells_length(), 3, 'Delete cell 0: There are still 3 cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 0: Cell 0 was not deleted');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Delete cell 0: Cell 1 was not affected');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Delete cell 0: Cell 2 was not affected');
+ this.validate_notebook_state('dd', 'command', 0);
+ });
+
+ // Try to delete cell 1 (should succeed)
+ this.then(function () {
+ this.select_cell(1);
+ this.trigger_keydown('d', 'd');
+ this.test.assertEquals(this.get_cells_length(), 2, 'Delete cell 1: There are now 2 cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 1: Cell 0 was not affected');
+ this.test.assertEquals(this.get_cell_text(1), c, 'Delete cell 1: Cell 1 was not affected');
+ this.validate_notebook_state('dd', 'command', 1);
+ });
+
+ // Try to delete cell 1 (should succeed)
+ this.then(function () {
+ this.select_cell(1);
+ this.trigger_keydown('d', 'd');
+ this.test.assertEquals(this.get_cells_length(), 1, 'Delete cell 1: There is now 1 cell');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 2: Cell 0 was not affected');
+ this.validate_notebook_state('dd', 'command', 0);
+ });
+
+ // Change the deletable status of the last cells
+ this.thenEvaluate(function() {
+ IPython.notebook.get_cell(0).metadata.deletable = true;
+ });
+
+ this.then(function () {
+ // Check deletable status of the cell
+ this.test.assert(cell_is_deletable(0), 'Cell 0 is deletable');
+
+ // Try to delete the last cell (should succeed)
+ this.select_cell(0);
+ this.trigger_keydown('d', 'd');
+ this.test.assertEquals(this.get_cells_length(), 1, 'Delete last cell: There is still 1 cell');
+ this.test.assertEquals(this.get_cell_text(0), "", 'Delete last cell: Cell 0 was deleted');
+ this.validate_notebook_state('dd', 'command', 0);
+ });
+
+ // Make sure copied cells are deletable
+ this.thenEvaluate(function() {
+ IPython.notebook.get_cell(0).metadata.deletable = false;
+ });
+ this.then(function () {
+ this.select_cell(0);
+ this.trigger_keydown('c', 'v');
+ this.test.assertEquals(this.get_cells_length(), 2, 'Copy cell: There are 2 cells');
+ this.test.assert(!cell_is_deletable(0), 'Cell 0 is not deletable');
+ this.test.assert(cell_is_deletable(1), 'Cell 1 is deletable');
+ this.validate_notebook_state('cv', 'command', 1);
+ });
+});
diff --git a/notebook/tests/notebook/display_image.js b/notebook/tests/notebook/display_image.js
new file mode 100644
index 0000000..1e980e6
--- /dev/null
+++ b/notebook/tests/notebook/display_image.js
@@ -0,0 +1,63 @@
+//
+// Test display of images
+//
+// The effect of shape metadata is validated,
+// using Image(retina=True)
+//
+
+
+// 2x2 black square in b64 jpeg and png
+b64_image_data = {
+ "image/png" : "b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAC0lEQVR4nGNgQAYAAA4AAamRc7EA\\nAAAASUVORK5CYII='",
+ "image/jpeg" : "b'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a\\nHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy\\nMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAACAAIDASIA\\nAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA\\nAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3\\nODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm\\np6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA\\nAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx\\nBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK\\nU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3\\nuLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5/ooo\\noA//2Q=='"
+}
+
+
+casper.notebook_test(function () {
+ this.test_img_shape = function(fmt, retina) {
+ this.thenEvaluate(function (b64data, retina) {
+ IPython.notebook.insert_cell_at_index("code", 0);
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text([
+ "import base64",
+ "from IPython.display import display, Image",
+ "data = base64.decodestring(" + b64data + ")",
+ "retina = bool(" + retina + ")",
+ "display(Image(data, retina=retina))"
+ ].join("\n"));
+ cell.execute();
+ }, {b64data : b64_image_data[fmt], retina : retina ? 1:0 });
+
+ this.wait_for_output(0);
+
+ this.then(function() {
+ var img = this.evaluate(function() {
+ // get a summary of the image that was just displayed
+ var cell = IPython.notebook.get_cell(0);
+ var img = $(cell.output_area.element.find("img")[0]);
+ return {
+ src : img.attr("src"),
+ width : img.width(),
+ height : img.height(),
+ width_attr : img.attr("width"),
+ height_attr : img.attr("height")
+ };
+ });
+ var prefix = "Image('" + fmt + "', retina=" + retina + ") ";
+ this.test.assertType(img, "object", prefix + "img was displayed");
+ this.test.assertEquals(img.src.split(',')[0], "data:" + fmt + ";base64",
+ prefix + "data-uri prefix"
+ );
+ var sz = retina ? 1 : 2;
+ var sz_attr = retina ? "1" : undefined;
+ this.test.assertEquals(img.height, sz, prefix + "measured height");
+ this.test.assertEquals(img.width, sz, prefix + "measured width");
+ this.test.assertEquals(img.height_attr, sz_attr, prefix + "height attr");
+ this.test.assertEquals(img.width_attr, sz_attr, prefix + "width attr");
+ });
+ };
+ this.test_img_shape("image/png", false);
+ this.test_img_shape("image/png", true);
+ this.test_img_shape("image/jpeg", false);
+ this.test_img_shape("image/jpeg", true);
+});
diff --git a/notebook/tests/notebook/dualmode.js b/notebook/tests/notebook/dualmode.js
new file mode 100644
index 0000000..ee577c7
--- /dev/null
+++ b/notebook/tests/notebook/dualmode.js
@@ -0,0 +1,116 @@
+// Test the notebook dual mode feature.
+
+// Test
+casper.notebook_test(function () {
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+ this.execute_cell_then(index);
+
+ var b = 'print("b")';
+ index = this.append_cell(b);
+ this.execute_cell_then(index);
+
+ var c = 'print("c")';
+ index = this.append_cell(c);
+ this.execute_cell_then(index);
+
+ this.then(function () {
+ if (this.slimerjs) {
+ // When running in xvfb, the Slimer window doesn't always have focus
+ // immediately. By clicking on a new element on the page we can force
+ // it to gain focus.
+ this.click_cell_editor(1);
+ this.click_cell_editor(0);
+ }
+
+ this.validate_notebook_state('initial state', 'edit', 0);
+ this.trigger_keydown('esc');
+ this.validate_notebook_state('esc', 'command', 0);
+ this.trigger_keydown('down');
+ this.validate_notebook_state('down', 'command', 1);
+ this.trigger_keydown('enter');
+ this.validate_notebook_state('enter', 'edit', 1);
+ this.trigger_keydown('j');
+ this.validate_notebook_state('j in edit mode', 'edit', 1);
+ this.trigger_keydown('esc');
+ this.validate_notebook_state('esc', 'command', 1);
+ this.trigger_keydown('j');
+ this.validate_notebook_state('j in command mode', 'command', 2);
+ this.click_cell_editor(0);
+ this.validate_notebook_state('click cell 0', 'edit', 0);
+ this.click_cell_editor(3);
+ this.validate_notebook_state('click cell 3', 'edit', 3);
+ this.trigger_keydown('esc');
+ this.validate_notebook_state('esc', 'command', 3);
+
+ // Open keyboard help
+ this.evaluate(function(){
+ $('#keyboard_shortcuts a').click();
+ }, {});
+ });
+
+ // Wait for the dialog to fade in completely.
+ this.waitForSelector('div.modal', function() {
+ this.evaluate(function(){
+ IPython.modal_shown = false;
+ $('div.modal').on('shown.bs.modal', function (){
+ IPython.modal_shown = true;
+ });
+ $('div.modal').on('hidden.bs.modal', function (){
+ IPython.modal_shown = false;
+ });
+ });
+
+ });
+
+ this.waitFor(function () {
+ return this.evaluate(function(){
+ return IPython.modal_shown;
+ });
+ },
+ function() {
+ this.trigger_keydown('k');
+ this.validate_notebook_state('k in command mode while keyboard help is up', 'command', 3);
+
+ // Close keyboard help
+ this.evaluate(function(){
+ $('div.modal-footer button.btn-default').click();
+ }, {});
+ });
+
+ // Wait for the dialog to fade out completely.
+ this.waitFor(function () {
+ return this.evaluate(function(){
+ return !IPython.modal_shown;
+ });
+ },
+ function() {
+
+ this.trigger_keydown('k');
+ this.validate_notebook_state('k in command mode', 'command', 2);
+ this.click_cell_editor(0);
+ this.validate_notebook_state('click cell 0', 'edit', 0);
+ this.focus_notebook();
+ this.validate_notebook_state('focus #notebook', 'command', 0);
+ this.click_cell_editor(0);
+ this.validate_notebook_state('click cell 0', 'edit', 0);
+ this.focus_notebook();
+ this.validate_notebook_state('focus #notebook', 'command', 0);
+ this.click_cell_editor(3);
+ this.validate_notebook_state('click cell 3', 'edit', 3);
+
+ // Cell deletion
+ this.trigger_keydown('esc', 'd', 'd');
+ this.test.assertEquals(this.get_cells_length(), 3, 'dd actually deletes a cell');
+ this.validate_notebook_state('dd', 'command', 2);
+
+ // Make sure that if the time between d presses is too long, nothing gets removed.
+ this.trigger_keydown('d');
+ });
+ this.wait(1000);
+ this.then(function () {
+ this.trigger_keydown('d');
+ this.test.assertEquals(this.get_cells_length(), 3, "d, 1 second wait, d doesn't delete a cell");
+ this.validate_notebook_state('d, 1 second wait, d', 'command', 2);
+ });
+});
diff --git a/notebook/tests/notebook/dualmode_arrows.js b/notebook/tests/notebook/dualmode_arrows.js
new file mode 100644
index 0000000..034929b
--- /dev/null
+++ b/notebook/tests/notebook/dualmode_arrows.js
@@ -0,0 +1,51 @@
+
+// Test
+casper.notebook_test(function () {
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+ this.execute_cell_then(index);
+
+ var b = 'print("b")';
+ index = this.append_cell(b);
+ this.execute_cell_then(index);
+
+ var c = 'print("c")';
+ index = this.append_cell(c);
+ this.execute_cell_then(index);
+
+ this.then(function () {
+
+ // Up and down in command mode
+ this.select_cell(3);
+ this.trigger_keydown('j');
+ this.validate_notebook_state('j at end of notebook', 'command', 3);
+ this.trigger_keydown('down');
+ this.validate_notebook_state('down at end of notebook', 'command', 3);
+ this.trigger_keydown('up');
+ this.validate_notebook_state('up', 'command', 2);
+ this.select_cell(0);
+ this.validate_notebook_state('select 0', 'command', 0);
+ this.trigger_keydown('k');
+ this.validate_notebook_state('k at top of notebook', 'command', 0);
+ this.trigger_keydown('up');
+ this.validate_notebook_state('up at top of notebook', 'command', 0);
+ this.trigger_keydown('down');
+ this.validate_notebook_state('down', 'command', 1);
+
+ // Up and down in edit mode
+ this.click_cell_editor(3);
+ this.validate_notebook_state('click cell 3', 'edit', 3);
+ this.trigger_keydown('down');
+ this.validate_notebook_state('down at end of notebook', 'edit', 3);
+ this.set_cell_editor_cursor(3, 0, 0);
+ this.trigger_keydown('up');
+ this.validate_notebook_state('up', 'edit', 2);
+ this.click_cell_editor(0);
+ this.validate_notebook_state('click 0', 'edit', 0);
+ this.trigger_keydown('up');
+ this.validate_notebook_state('up at top of notebook', 'edit', 0);
+ this.set_cell_editor_cursor(0, 0, 10);
+ this.trigger_keydown('down');
+ this.validate_notebook_state('down', 'edit', 1);
+ });
+});
diff --git a/notebook/tests/notebook/dualmode_cellinsert.js b/notebook/tests/notebook/dualmode_cellinsert.js
new file mode 100644
index 0000000..f066b16
--- /dev/null
+++ b/notebook/tests/notebook/dualmode_cellinsert.js
@@ -0,0 +1,82 @@
+
+// Test
+casper.notebook_test(function () {
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+ this.execute_cell_then(index);
+
+ var b = 'print("b")';
+ index = this.append_cell(b);
+ this.execute_cell_then(index);
+
+ var c = 'print("c")';
+ index = this.append_cell(c);
+ this.execute_cell_then(index);
+
+ this.thenEvaluate(function() {
+ IPython.notebook.default_cell_type = 'code';
+ });
+
+ this.then(function () {
+ // Cell insertion
+ this.select_cell(2);
+ this.trigger_keydown('m'); // Make it markdown
+ this.trigger_keydown('a'); // Creates one cell
+ this.test.assertEquals(this.get_cell_text(2), '', 'a; New cell 2 text is empty');
+ this.test.assertEquals(this.get_cell(2).cell_type, 'code', 'a; inserts a code cell');
+ this.validate_notebook_state('a', 'command', 2);
+ this.trigger_keydown('b'); // Creates one cell
+ this.test.assertEquals(this.get_cell_text(2), '', 'b; Cell 2 text is still empty');
+ this.test.assertEquals(this.get_cell_text(3), '', 'b; New cell 3 text is empty');
+ this.test.assertEquals(this.get_cell(3).cell_type, 'code', 'b; inserts a code cell');
+ this.validate_notebook_state('b', 'command', 3);
+ });
+
+ this.thenEvaluate(function() {
+ IPython.notebook.class_config.set('default_cell_type', 'selected');
+ });
+
+ this.then(function () {
+ this.select_cell(2);
+ this.trigger_keydown('m'); // switch it to markdown for the next test
+ this.test.assertEquals(this.get_cell(2).cell_type, 'markdown', 'test cell is markdown');
+ this.trigger_keydown('a'); // new cell above
+ this.test.assertEquals(this.get_cell(2).cell_type, 'markdown', 'a; inserts a markdown cell when markdown selected');
+ this.trigger_keydown('b'); // new cell below
+ this.test.assertEquals(this.get_cell(3).cell_type, 'markdown', 'b; inserts a markdown cell when markdown selected');
+ });
+
+ this.thenEvaluate(function() {
+ IPython.notebook.class_config.set('default_cell_type', 'above');
+ });
+
+ this.then(function () {
+ this.select_cell(2);
+ this.trigger_keydown('y'); // switch it to code for the next test
+ this.test.assertEquals(this.get_cell(2).cell_type, 'code', 'test cell is code');
+ this.trigger_keydown('b'); // new cell below
+ this.test.assertEquals(this.get_cell(3).cell_type, 'code', 'b; inserts a code cell below code cell');
+ this.trigger_keydown('a'); // new cell above
+ this.test.assertEquals(this.get_cell(3).cell_type, 'code', 'a; inserts a code cell above code cell');
+ });
+
+ this.then(function () {
+ this.set_cell_text(1, 'cell1');
+ this.select_cell(1);
+ this.select_cell(2, false);
+ this.trigger_keydown('a');
+ this.test.assertEquals(this.get_cell_text(1), '', 'a; New cell 1 text is empty');
+ this.test.assertEquals(this.get_cell_text(2), 'cell1', 'a; Cell 2 text is old cell 1');
+
+ this.set_cell_text(1, 'cell1');
+ this.set_cell_text(2, 'cell2');
+ this.set_cell_text(3, 'cell3');
+ this.select_cell(1);
+ this.select_cell(2, false);
+ this.trigger_keydown('b');
+ this.test.assertEquals(this.get_cell_text(1), 'cell1', 'b; Cell 1 remains');
+ this.test.assertEquals(this.get_cell_text(2), 'cell2', 'b; Cell 2 remains');
+ this.test.assertEquals(this.get_cell_text(3), '', 'b; Cell 3 is new');
+ this.test.assertEquals(this.get_cell_text(4), 'cell3', 'b; Cell 4 text is old cell 3');
+ });
+});
diff --git a/notebook/tests/notebook/dualmode_cellmode.js b/notebook/tests/notebook/dualmode_cellmode.js
new file mode 100644
index 0000000..3701792
--- /dev/null
+++ b/notebook/tests/notebook/dualmode_cellmode.js
@@ -0,0 +1,41 @@
+// Test keyboard shortcuts that change the cell's mode.
+
+// Test
+casper.notebook_test(function () {
+ this.then(function () {
+ // Cell mode change
+ var index = 0;
+ this.select_cell(index);
+ var a = 'hello\nmulti\nline';
+ this.set_cell_text(index, a);
+ this.trigger_keydown('esc','r');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'raw', 'r; cell is raw');
+ this.trigger_keydown('1');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', '1; cell is markdown');
+ this.test.assertEquals(this.get_cell_text(index), '# ' + a, '1; markdown heading');
+ this.trigger_keydown('2');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', '2; cell is markdown');
+ this.test.assertEquals(this.get_cell_text(index), '## ' + a, '2; markdown heading');
+ this.trigger_keydown('3');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', '3; cell is markdown');
+ this.test.assertEquals(this.get_cell_text(index), '### ' + a, '3; markdown heading');
+ this.trigger_keydown('4');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', '4; cell is markdown');
+ this.test.assertEquals(this.get_cell_text(index), '#### ' + a, '4; markdown heading');
+ this.trigger_keydown('5');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', '5; cell is markdown');
+ this.test.assertEquals(this.get_cell_text(index), '##### ' + a, '5; markdown heading');
+ this.trigger_keydown('6');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', '6; cell is markdown');
+ this.test.assertEquals(this.get_cell_text(index), '###### ' + a, '6; markdown heading');
+ this.trigger_keydown('m');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', 'm; cell is markdown');
+ this.test.assertEquals(this.get_cell_text(index), '###### ' + a, 'm; still markdown heading');
+ this.trigger_keydown('y');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'code', 'y; cell is code');
+ this.test.assertEquals(this.get_cell_text(index), '###### ' + a, 'y; still has hashes');
+ this.trigger_keydown('1');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', '1; cell is markdown');
+ this.test.assertEquals(this.get_cell_text(index), '# ' + a, '1; markdown heading');
+ });
+}); \ No newline at end of file
diff --git a/notebook/tests/notebook/dualmode_clipboard.js b/notebook/tests/notebook/dualmode_clipboard.js
new file mode 100644
index 0000000..b7fe985
--- /dev/null
+++ b/notebook/tests/notebook/dualmode_clipboard.js
@@ -0,0 +1,55 @@
+
+
+// Test
+casper.notebook_test(function () {
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+ this.execute_cell_then(index);
+
+ var b = 'print("b")';
+ index = this.append_cell(b);
+ this.execute_cell_then(index);
+
+ var c = 'print("c")';
+ index = this.append_cell(c);
+ this.execute_cell_then(index);
+
+ this.then(function () {
+ // Copy/paste/cut
+ var num_cells = this.get_cells_length();
+ this.test.assertEquals(this.get_cell_text(1), a, 'Verify that cell 1 is a');
+ this.select_cell(1);
+ this.trigger_keydown('x'); // Cut
+ this.validate_notebook_state('x', 'command', 1);
+ this.test.assertEquals(this.get_cells_length(), num_cells-1, 'Verify that a cell was removed.');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Verify that cell 2 is now where cell 1 was.');
+ this.select_cell(2);
+ this.trigger_keydown('v'); // Paste
+ this.validate_notebook_state('v', 'command', 3); // Selection should move to pasted cell, below current cell.
+ this.test.assertEquals(this.get_cell_text(3), a, 'Verify that cell 3 has the cut contents.');
+ this.test.assertEquals(this.get_cells_length(), num_cells, 'Verify a the cell was added.');
+ this.trigger_keydown('v'); // Paste
+ this.validate_notebook_state('v', 'command', 4); // Selection should move to pasted cell, below current cell.
+ this.test.assertEquals(this.get_cell_text(4), a, 'Verify that cell 4 has the cut contents.');
+ this.test.assertEquals(this.get_cells_length(), num_cells+1, 'Verify a the cell was added.');
+ this.select_cell(1);
+ this.trigger_keydown('c'); // Copy
+ this.validate_notebook_state('c', 'command', 1);
+ this.test.assertEquals(this.get_cell_text(1), b, 'Verify that cell 1 is b');
+ this.select_cell(2);
+ this.trigger_keydown('c'); // Copy
+ this.validate_notebook_state('c', 'command', 2);
+ this.test.assertEquals(this.get_cell_text(2), c, 'Verify that cell 2 is c');
+ this.select_cell(4);
+ this.trigger_keydown('v'); // Paste
+ this.validate_notebook_state('v', 'command', 5);
+ this.test.assertEquals(this.get_cell_text(2), c, 'Verify that cell 2 still has the copied contents.');
+ this.test.assertEquals(this.get_cell_text(5), c, 'Verify that cell 5 has the copied contents.');
+ this.test.assertEquals(this.get_cells_length(), num_cells+2, 'Verify a the cell was added.');
+ this.select_cell(0);
+ this.trigger_keydown('shift-v'); // Paste
+ this.validate_notebook_state('shift-v', 'command', 0);
+ this.test.assertEquals(this.get_cell_text(0), c, 'Verify that cell 0 has the copied contents.');
+ this.test.assertEquals(this.get_cells_length(), num_cells+3, 'Verify a the cell was added.');
+ });
+});
diff --git a/notebook/tests/notebook/dualmode_execute.js b/notebook/tests/notebook/dualmode_execute.js
new file mode 100644
index 0000000..f4cd954
--- /dev/null
+++ b/notebook/tests/notebook/dualmode_execute.js
@@ -0,0 +1,72 @@
+// Test keyboard invoked execution.
+
+// Test
+casper.notebook_test(function () {
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+ this.execute_cell_then(index);
+
+ var b = 'print("b")';
+ index = this.append_cell(b);
+ this.execute_cell_then(index);
+
+ var c = 'print("c")';
+ index = this.append_cell(c);
+ this.execute_cell_then(index);
+
+ this.then(function () {
+
+ // shift-enter
+ // last cell in notebook
+ var base_index = 3;
+ this.select_cell(base_index);
+ this.trigger_keydown('shift-enter'); // Creates one cell
+ this.validate_notebook_state('shift-enter (no cell below)', 'edit', base_index + 1);
+ // not last cell in notebook & starts in edit mode
+ this.click_cell_editor(base_index);
+ this.validate_notebook_state('click cell ' + base_index, 'edit', base_index);
+ this.trigger_keydown('shift-enter');
+ this.validate_notebook_state('shift-enter (cell exists below)', 'command', base_index + 1);
+ // starts in command mode
+ this.trigger_keydown('k');
+ this.validate_notebook_state('k in comand mode', 'command', base_index);
+ this.trigger_keydown('shift-enter');
+ this.validate_notebook_state('shift-enter (start in command mode)', 'command', base_index + 1);
+
+ // ctrl-enter
+ // last cell in notebook
+ base_index++;
+ this.trigger_keydown('ctrl-enter');
+ this.validate_notebook_state('ctrl-enter (no cell below)', 'command', base_index);
+ // not last cell in notebook & starts in edit mode
+ this.click_cell_editor(base_index-1);
+ this.validate_notebook_state('click cell ' + (base_index-1), 'edit', base_index-1);
+ this.trigger_keydown('ctrl-enter');
+ this.validate_notebook_state('ctrl-enter (cell exists below)', 'command', base_index-1);
+ // starts in command mode
+ this.trigger_keydown('j');
+ this.validate_notebook_state('j in comand mode', 'command', base_index);
+ this.trigger_keydown('ctrl-enter');
+ this.validate_notebook_state('ctrl-enter (start in command mode)', 'command', base_index);
+
+ // alt-enter
+ // last cell in notebook
+ this.trigger_keydown('alt-enter'); // Creates one cell
+ this.validate_notebook_state('alt-enter (no cell below)', 'edit', base_index + 1);
+ // not last cell in notebook & starts in edit mode
+ this.click_cell_editor(base_index);
+ this.validate_notebook_state('click cell ' + base_index, 'edit', base_index);
+ this.trigger_keydown('alt-enter'); // Creates one cell
+ this.validate_notebook_state('alt-enter (cell exists below)', 'edit', base_index + 1);
+ // starts in command mode
+ this.trigger_keydown('esc', 'k');
+ this.validate_notebook_state('k in comand mode', 'command', base_index);
+ this.trigger_keydown('alt-enter'); // Creates one cell
+ this.validate_notebook_state('alt-enter (start in command mode)', 'edit', base_index + 1);
+
+ // Notebook will now have 8 cells, the index of the last cell will be 7.
+ this.test.assertEquals(this.get_cells_length(), 8, '*-enter commands added cells where needed.');
+ this.select_cell(7);
+ this.validate_notebook_state('click cell ' + 7 + ' and esc', 'command', 7);
+ });
+}); \ No newline at end of file
diff --git a/notebook/tests/notebook/dualmode_markdown.js b/notebook/tests/notebook/dualmode_markdown.js
new file mode 100644
index 0000000..c9ecd6d
--- /dev/null
+++ b/notebook/tests/notebook/dualmode_markdown.js
@@ -0,0 +1,37 @@
+
+// Test
+casper.notebook_test(function () {
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+ this.execute_cell_then(index, function(index) {
+ // Markdown rendering / unredering
+ this.select_cell(index);
+ this.validate_notebook_state('select ' + index, 'command', index);
+ this.trigger_keydown('m');
+ this.test.assertEquals(this.get_cell(index).cell_type, 'markdown', 'm; cell is markdown');
+ this.test.assert(!this.is_cell_rendered(index), 'm; cell is unrendered');
+ this.trigger_keydown('enter');
+ this.test.assert(!this.is_cell_rendered(index), 'enter; cell is unrendered');
+ this.validate_notebook_state('enter', 'edit', index);
+ this.trigger_keydown('ctrl-enter');
+ this.test.assert(this.is_cell_rendered(index), 'ctrl-enter; cell is rendered');
+ this.validate_notebook_state('enter', 'command', index);
+ this.trigger_keydown('enter');
+ this.test.assert(!this.is_cell_rendered(index), 'enter; cell is unrendered');
+ this.select_cell(index-1);
+ this.test.assert(!this.is_cell_rendered(index), 'select ' + (index-1) + '; cell ' + index + ' is still unrendered');
+ this.validate_notebook_state('select ' + (index-1), 'command', index-1);
+ this.select_cell(index);
+ this.validate_notebook_state('select ' + index, 'command', index);
+ this.trigger_keydown('ctrl-enter');
+ this.test.assert(this.is_cell_rendered(index), 'ctrl-enter; cell is rendered');
+ this.select_cell(index-1);
+ this.validate_notebook_state('select ' + (index-1), 'command', index-1);
+ this.trigger_keydown('shift-enter');
+ this.validate_notebook_state('shift-enter', 'command', index);
+ this.test.assert(this.is_cell_rendered(index), 'shift-enter; cell is rendered');
+ this.trigger_keydown('shift-enter'); // Creates one cell
+ this.validate_notebook_state('shift-enter', 'edit', index+1);
+ this.test.assert(this.is_cell_rendered(index), 'shift-enter; cell is rendered');
+ });
+}); \ No newline at end of file
diff --git a/notebook/tests/notebook/dualmode_merge.js b/notebook/tests/notebook/dualmode_merge.js
new file mode 100644
index 0000000..0cd15aa
--- /dev/null
+++ b/notebook/tests/notebook/dualmode_merge.js
@@ -0,0 +1,198 @@
+
+// Test
+casper.notebook_test(function () {
+ var a = 'ab\n\ncd';
+ var b = 'print("b")';
+ var c = 'print("c")';
+ var d = '"d"';
+ var e = '"e"';
+ var f = '"f"';
+ var g = '"g"';
+ var N = 7;
+
+ var that = this;
+ var cell_is_mergeable = function (index) {
+ // Get the mergeable status of a cell.
+ return that.evaluate(function (index) {
+ var cell = IPython.notebook.get_cell(index);
+ return cell.is_mergeable();
+ }, index);
+ };
+
+ var cell_is_splittable = function (index) {
+ // Get the splittable status of a cell.
+ return that.evaluate(function (index) {
+ var cell = IPython.notebook.get_cell(index);
+ return cell.is_splittable();
+ }, index);
+ };
+
+ var close_dialog = function () {
+ this.evaluate(function(){
+ $('div.modal-footer button.btn-default').click();
+ }, {});
+ };
+
+ this.then(function () {
+ // Split and merge cells
+ this.select_cell(0);
+ this.trigger_keydown('a', 'enter'); // Create cell above and enter edit mode.
+ this.validate_notebook_state('a, enter', 'edit', 0);
+ this.set_cell_text(0, 'abcd');
+ this.set_cell_editor_cursor(0, 0, 2);
+ this.test.assertEquals(this.get_cell_text(0), 'abcd', 'Verify that cell 0 has the new contents.');
+ this.trigger_keydown('ctrl-shift--'); // Split
+ this.test.assertEquals(this.get_cell_text(0), 'ab', 'split; Verify that cell 0 has the first half.');
+ this.test.assertEquals(this.get_cell_text(1), 'cd', 'split; Verify that cell 1 has the second half.');
+ this.validate_notebook_state('split', 'edit', 1);
+ this.select_cell(0); // Move up to cell 0
+ this.evaluate(function() { IPython.notebook.extend_selection_by(1);});
+ this.trigger_keydown('shift-m'); // Merge
+ this.validate_notebook_state('merge', 'command', 0);
+ this.test.assertEquals(this.get_cell_text(0), a, 'merge; Verify that cell 0 has the merged contents.');
+ });
+
+ // add some more cells and test splitting/merging when a cell is not deletable
+ this.then(function () {
+ this.append_cell(b);
+ this.append_cell(c);
+ this.append_cell(d);
+ this.append_cell(e);
+ this.append_cell(f);
+ this.append_cell(g);
+ });
+
+ this.thenEvaluate(function() {
+ IPython.notebook.get_cell(1).metadata.deletable = false;
+ });
+
+ // Check that merge/split status are correct
+ this.then(function () {
+ this.test.assert(cell_is_splittable(0), 'Cell 0 is splittable');
+ this.test.assert(cell_is_mergeable(0), 'Cell 0 is mergeable');
+ this.test.assert(!cell_is_splittable(1), 'Cell 1 is not splittable');
+ this.test.assert(!cell_is_mergeable(1), 'Cell 1 is not mergeable');
+ this.test.assert(cell_is_splittable(2), 'Cell 2 is splittable');
+ this.test.assert(cell_is_mergeable(2), 'Cell 2 is mergeable');
+ });
+
+ // Try to merge cell 0 above, nothing should happen
+ this.then(function () {
+ this.select_cell(0);
+ });
+ this.thenEvaluate(function() {
+ IPython.notebook.merge_cell_above();
+ });
+ this.then(function() {
+ this.test.assertEquals(this.get_cells_length(), N, 'Merge cell 0 above: There are still '+N+' cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 0 above: Cell 0 is unchanged');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 0 above: Cell 1 is unchanged');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 0 above: Cell 2 is unchanged');
+ this.validate_notebook_state('merge up', 'command', 0);
+ });
+
+ // Try to merge cell 0 below with cell 1, should not work, as 1 is locked
+ this.then(function () {
+ this.trigger_keydown('esc');
+ this.select_cell(0);
+ this.select_cell(1,false);
+ this.trigger_keydown('shift-m');
+ this.trigger_keydown('esc');
+ this.test.assertEquals(this.get_cells_length(), N, 'Merge cell 0 down: There are still '+N+' cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 0 down: Cell 0 is unchanged');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 0 down: Cell 1 is unchanged');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 0 down: Cell 2 is unchanged');
+ this.validate_notebook_state('merge 0 with 1', 'command', 1);
+ });
+
+ // Try to merge cell 1 above with cell 0
+ this.then(function () {
+ this.select_cell(1);
+ });
+ this.thenEvaluate(function () {
+ IPython.notebook.merge_cell_above();
+ });
+ this.then(function () {
+ this.test.assertEquals(this.get_cells_length(), N, 'Merge cell 1 up: There are still '+N+' cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 1 up: Cell 0 is unchanged');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 1 up: Cell 1 is unchanged');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 1 up: Cell 2 is unchanged');
+ this.validate_notebook_state('merge up', 'command', 1);
+ });
+
+ // Try to split cell 1
+ this.then(function () {
+ this.select_cell(1);
+ this.trigger_keydown('enter');
+ this.set_cell_editor_cursor(1, 0, 2);
+ this.trigger_keydown('ctrl-shift--'); // Split
+ this.test.assertEquals(this.get_cells_length(), N, 'Split cell 1: There are still '+N+' cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Split cell 1: Cell 0 is unchanged');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Split cell 1: Cell 1 is unchanged');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Split cell 1: Cell 2 is unchanged');
+ this.validate_notebook_state('ctrl-shift--', 'edit', 1);
+ });
+
+ // Try to merge cell 1 down, should fail, as 1 is locked
+ this.then(function () {
+ this.trigger_keydown('esc');
+ this.select_cell(1);
+ this.select_cell(2, false); // extend selection
+ this.trigger_keydown('shift-m');
+ this.trigger_keydown('esc');
+ this.test.assertEquals(this.get_cells_length(), N, 'Merge cell 1 down: There are still '+N+' cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 1 down: Cell 0 is unchanged');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 1 down: Cell 1 is unchanged');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 1 down: Cell 2 is unchanged');
+ this.validate_notebook_state('Merge 1 with 2', 'command', 2);
+ });
+
+ // Try to merge cell 2 above with cell 1, should fail, 1 is locked
+ this.then(function () {
+ this.select_cell(2);
+ });
+ this.thenEvaluate(function () {
+ IPython.notebook.merge_cell_above();
+ });
+ this.then(function () {
+ this.test.assertEquals(this.get_cells_length(), N, 'Merge cell 2 up: There are still '+N+' cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 2 up: Cell 0 is unchanged');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 2 up: Cell 1 is unchanged');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 2 up: Cell 2 is unchanged');
+ this.validate_notebook_state('merge up', 'command', 2);
+ });
+
+ this.then(function () {
+ this.trigger_keydown('esc');
+ this.select_cell(3);
+ this.select_cell(4, false); // extend selection
+ this.trigger_keydown('shift-m');
+ this.trigger_keydown('esc');
+ this.test.assertEquals(this.get_cells_length(), N-1 , 'Merge cell 3 with 4: There are now '+(N-1)+' cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 3 with 4: Cell 0 is unchanged');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 3 with 4: Cell 1 is unchanged');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 3 with 4: Cell 2 is unchanged');
+ this.test.assertEquals(this.get_cell_text(3), d+'\n\n'+e, 'Merge cell 3 with 4: Cell 3 is merged');
+ this.test.assertEquals(this.get_cell_text(4), f, 'Merge cell 3 with 4: Cell 5 is now cell 4');
+ this.test.assertEquals(this.get_cell_text(5), g, 'Merge cell 3 with 4: Cell 6 is now cell 5');
+ this.validate_notebook_state('actual merge', 'command', 3);
+ });
+
+
+ this.then(function () {
+ this.trigger_keydown('esc');
+ this.select_cell(4);
+ // shift-m on single selection does nothing.
+ this.trigger_keydown('shift-m');
+ this.trigger_keydown('esc');
+ this.test.assertEquals(this.get_cells_length(), N-2 , 'Merge cell 4 with 5: There are now '+(N-2)+' cells');
+ this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 4 with 5: Cell 0 is unchanged');
+ this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 4 with 5: Cell 1 is unchanged');
+ this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 4 with 5: Cell 2 is unchanged');
+ this.test.assertEquals(this.get_cell_text(3), d+'\n\n'+e, 'Merge cell 4 with 5: Cell 3 is unchanged');
+ this.test.assertEquals(this.get_cell_text(4), f+'\n\n'+g, 'Merge cell 4 with 5: Cell 4 and 5 are merged');
+ this.validate_notebook_state('merge on single cell merge with below', 'command', 4);
+ });
+
+
+});
diff --git a/notebook/tests/notebook/empty_arrow_keys.js b/notebook/tests/notebook/empty_arrow_keys.js
new file mode 100644
index 0000000..a949ce5
--- /dev/null
+++ b/notebook/tests/notebook/empty_arrow_keys.js
@@ -0,0 +1,21 @@
+//
+// Check for errors with up and down arrow presses in an empty notebook.
+//
+casper.notebook_test(function () {
+ var result = this.evaluate(function() {
+ var ncells = IPython.notebook.ncells();
+ var i;
+
+ // Delete all cells.
+ for (i = 0; i < ncells; i++) {
+ IPython.notebook.delete_cell();
+ }
+
+ return true;
+ });
+
+ // Simulate the "up arrow" and "down arrow" keys.
+ this.trigger_keydown('up');
+ this.trigger_keydown('down');
+ this.test.assertTrue(result, 'Up/down arrow okay in empty notebook.');
+});
diff --git a/notebook/tests/notebook/execute_code.js b/notebook/tests/notebook/execute_code.js
new file mode 100644
index 0000000..9a5e933
--- /dev/null
+++ b/notebook/tests/notebook/execute_code.js
@@ -0,0 +1,115 @@
+//
+// Test code cell execution.
+//
+casper.notebook_test(function () {
+ this.evaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text('a=10; print(a)');
+ cell.execute();
+ });
+
+ this.wait_for_output(0);
+
+ // refactor this into just a get_output(0)
+ this.then(function () {
+ var result = this.get_output_cell(0);
+ this.test.assertEquals(result.text, '10\n', 'cell execute (using js)');
+ });
+
+
+ // do it again with the keyboard shortcut
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text('a=11; print(a)');
+ cell.clear_output();
+ });
+
+ this.then(function(){
+
+ this.trigger_keydown('shift-enter');
+ });
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var result = this.get_output_cell(0);
+ var num_cells = this.get_cells_length();
+ this.test.assertEquals(result.text, '11\n', 'cell execute (using ctrl-enter)');
+ this.test.assertEquals(num_cells, 2, 'shift-enter adds a new cell at the bottom')
+ });
+
+ // do it again with the keyboard shortcut
+ this.thenEvaluate(function () {
+ IPython.notebook.select(1);
+ IPython.notebook.delete_cell();
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text('a=12; print(a)');
+ cell.clear_output();
+ });
+
+ this.then(function(){
+ this.trigger_keydown('ctrl-enter');
+ });
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var result = this.get_output_cell(0);
+ var num_cells = this.get_cells_length();
+ this.test.assertEquals(result.text, '12\n', 'cell execute (using shift-enter)');
+ this.test.assertEquals(num_cells, 1, 'ctrl-enter adds no new cell at the bottom')
+ });
+
+ // press the "play" triangle button in the toolbar
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ IPython.notebook.select(0);
+ cell.clear_output();
+ cell.set_text('a=13; print(a)');
+ $("button[data-jupyter-action='jupyter-notebook:run-cell-and-select-next']")[0].click()
+ });
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var result = this.get_output_cell(0);
+ this.test.assertEquals(result.text, '13\n', 'cell execute (using "play" toolbar button)')
+ });
+
+ // run code with skip_exception
+ this.thenEvaluate(function () {
+ var cell0 = IPython.notebook.get_cell(0);
+ cell0.set_text('raise IOError');
+ IPython.notebook.insert_cell_below('code',0);
+ var cell1 = IPython.notebook.get_cell(1);
+ cell1.set_text('a=14; print(a)');
+ cell0.execute(false);
+ cell1.execute();
+ });
+
+ this.wait_for_output(1);
+
+ this.then(function () {
+ var result = this.get_output_cell(1);
+ this.test.assertEquals(result.text, '14\n', "cell execute, don't stop on error");
+ });
+
+ this.thenEvaluate(function () {
+ var cell0 = IPython.notebook.get_cell(0);
+ cell0.set_text('raise IOError');
+ IPython.notebook.insert_cell_below('code',0);
+ var cell1 = IPython.notebook.get_cell(1);
+ cell1.set_text('a=14; print(a)');
+ cell0.execute();
+ cell1.execute();
+ });
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var outputs = this.evaluate(function() {
+ return IPython.notebook.get_cell(1).output_area.outputs;
+ })
+ this.test.assertEquals(outputs.length, 0, 'cell execute, stop on error (default)');
+ });
+});
diff --git a/notebook/tests/notebook/execute_selected_cells.js b/notebook/tests/notebook/execute_selected_cells.js
new file mode 100644
index 0000000..5cb470a
--- /dev/null
+++ b/notebook/tests/notebook/execute_selected_cells.js
@@ -0,0 +1,178 @@
+//
+// Test that the correct cells are executed when there are marked cells.
+//
+casper.notebook_test(function () {
+ var that = this;
+ var assert_outputs = function (expected, msg_prefix) {
+ var msg, i;
+ msg_prefix = "(assert_outputs) "+(msg_prefix || 'no prefix')+": ";
+ for (i = 0; i < that.get_cells_length(); i++) {
+ if (expected[i] === undefined) {
+ msg = msg_prefix + 'cell ' + i + ' not executed';
+ that.test.assertFalse(that.cell_has_outputs(i), msg);
+
+ } else {
+ msg = msg_prefix + 'cell ' + i + ' executed';
+ var out = (that.get_output_cell(i, undefined, msg_prefix)||{test:'<no cells>'}).text
+ that.test.assertEquals(out, expected[i], msg + ', out is: '+out);
+ }
+ }
+ };
+
+ this.then(function () {
+ this.set_cell_text(0, 'print("a")');
+ this.append_cell('print("b")');
+ this.append_cell('print("c")');
+ this.append_cell('print("d")');
+ this.test.assertEquals(this.get_cells_length(), 4, "correct number of cells");
+ });
+
+ this.then(function () {
+ this.select_cell(1);
+ this.select_cell(2, false);
+ });
+
+ this.then(function () {
+ this.evaluate(function () {
+ IPython.notebook.clear_all_output();
+ });
+ })
+
+ this.then(function(){
+ this.select_cell(1);
+ this.validate_notebook_state('before execute 1', 'command', 1);
+ this.select_cell(1);
+ this.select_cell(2, false);
+ this.trigger_keydown('ctrl-enter');
+ });
+
+ this.wait_for_output(1);
+ this.wait_for_output(2);
+
+ this.then(function () {
+ assert_outputs([undefined, 'b\n', 'c\n', undefined], 'run selected 1');
+ this.validate_notebook_state('run selected cells 1', 'command', 2);
+ });
+
+
+ // execute and insert below when there are selected cells
+ this.then(function () {
+ this.evaluate(function () {
+ IPython.notebook.clear_all_output();
+ });
+
+ this.select_cell(1);
+ this.validate_notebook_state('before execute 2', 'command', 1);
+ this.evaluate(function () {
+ $("#run_cell_insert_below").click();
+ });
+ });
+
+ this.wait_for_output(1);
+
+ this.then(function () {
+ assert_outputs([undefined, 'b\n', undefined, undefined , undefined],'run selected cells 2');
+ this.validate_notebook_state('run selected cells 2', 'edit', 2);
+ });
+
+ // check that it doesn't affect run all above
+ this.then(function () {
+ this.evaluate(function () {
+ IPython.notebook.clear_all_output();
+ });
+
+ this.select_cell(1);
+ this.validate_notebook_state('before execute 3', 'command', 1);
+ this.evaluate(function () {
+ $("#run_all_cells_above").click();
+ });
+ });
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ assert_outputs(['a\n', undefined, undefined, undefined],'run cells above');
+ this.validate_notebook_state('run cells above', 'command', 0);
+ });
+
+ // check that it doesn't affect run all below
+ this.then(function () {
+ this.evaluate(function () {
+ IPython.notebook.clear_all_output();
+ });
+
+ this.select_cell(1);
+ this.validate_notebook_state('before execute 4', 'command', 1);
+ this.evaluate(function () {
+ $("#run_all_cells_below").click();
+ });
+ });
+
+ this.wait_for_output(1);
+ this.wait_for_output(2);
+ this.wait_for_output(3);
+
+ this.then(function () {
+ assert_outputs([undefined, 'b\n', undefined, 'c\n', 'd\n'],'run cells below');
+ this.validate_notebook_state('run cells below', 'command', 4);
+ });
+
+ // check that it doesn't affect run all
+ this.then(function () {
+ this.evaluate(function () {
+ IPython.notebook.clear_all_output();
+ });
+
+ this.select_cell(1);
+ this.validate_notebook_state('before execute 5', 'command', 1);
+ this.evaluate(function () {
+ $("#run_all_cells").click();
+ });
+ });
+
+ this.wait_for_output(0);
+ this.wait_for_output(1);
+ this.wait_for_output(2);
+ this.wait_for_output(3);
+
+ this.then(function () {
+ assert_outputs(['a\n', 'b\n', undefined, 'c\n', 'd\n'],'run all cells');
+ this.validate_notebook_state('run all cells', 'command', 4);
+ });
+
+ this.then(function(){
+ this.set_cell_text(0, 'print("x")');
+ this.set_cell_text(1, 'print("y")');
+
+ this.select_cell(0);
+ this.select_cell(1, false);
+ this.trigger_keydown('alt-enter');
+
+ });
+
+ this.wait_for_output(0);
+ this.wait_for_output(1);
+ this.then(function () {
+ assert_outputs(['x\n', 'y\n', undefined, undefined, 'c\n', 'd\n'],'run selection and insert below');
+ this.validate_notebook_state('run selection insert below', 'edit', 2);
+ });
+
+ this.then(function(){
+ this.set_cell_text(1, 'print("z")');
+ this.set_cell_text(2, 'print("a")');
+
+ this.select_cell(1);
+ this.select_cell(2, false);
+ this.evaluate(function () {
+ $("#run_cell_select_below").click();
+ });
+
+ });
+
+ this.wait_for_output(1);
+ this.wait_for_output(2);
+ this.then(function () {
+ assert_outputs(['x\n', 'z\n', 'a\n', undefined, 'c\n', 'd\n'],'run selection and select below');
+ this.validate_notebook_state('run selection select below', 'command', 3);
+ });
+});
diff --git a/notebook/tests/notebook/inject_js.js b/notebook/tests/notebook/inject_js.js
new file mode 100644
index 0000000..03757aa
--- /dev/null
+++ b/notebook/tests/notebook/inject_js.js
@@ -0,0 +1,23 @@
+//
+// Test robustness about JS injection in different place
+//
+// This assume malicious document arrive to the frontend.
+//
+
+casper.notebook_test(function () {
+ var messages = [];
+ this.on('remote.alert', function (msg) {
+ messages.push(msg);
+ });
+
+ this.evaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ var json = cell.toJSON();
+ json.execution_count = "<script> alert('hello from input prompts !')</script>";
+ cell.fromJSON(json);
+ });
+
+ this.then(function () {
+ this.test.assert(messages.length == 0, "Captured log message from script tag injection !");
+ });
+});
diff --git a/notebook/tests/notebook/interrupt.js b/notebook/tests/notebook/interrupt.js
new file mode 100644
index 0000000..24b6266
--- /dev/null
+++ b/notebook/tests/notebook/interrupt.js
@@ -0,0 +1,45 @@
+//
+// Test kernel interrupt
+//
+casper.notebook_test(function () {
+ this.evaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text(
+ 'import time'+
+ '\nfor x in range(3):'+
+ '\n time.sleep(1)'
+ );
+ cell.execute();
+ });
+
+ this.wait_for_busy();
+
+ // interrupt using menu item (Kernel -> Interrupt)
+ this.thenClick('li#int_kernel');
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var result = this.get_output_cell(0);
+ this.test.assertEquals(result.ename, 'KeyboardInterrupt', 'keyboard interrupt (mouseclick)');
+ });
+
+ // run cell 0 again, now interrupting using keyboard shortcut
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.clear_output();
+ cell.execute();
+ });
+
+ // interrupt using ii keyboard shortcut
+ this.then(function(){
+ this.trigger_keydown('esc', 'i', 'i');
+ });
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var result = this.get_output_cell(0);
+ this.test.assertEquals(result.ename, 'KeyboardInterrupt', 'keyboard interrupt (shortcut)');
+ });
+});
diff --git a/notebook/tests/notebook/isolated_svg.js b/notebook/tests/notebook/isolated_svg.js
new file mode 100644
index 0000000..ac29d49
--- /dev/null
+++ b/notebook/tests/notebook/isolated_svg.js
@@ -0,0 +1,90 @@
+//
+// Test display isolation
+// An object whose metadata contains an "isolated" tag must be isolated
+// from the rest of the document. In the case of inline SVGs, this means
+// that multiple SVGs have different scopes. This test checks that there
+// are no CSS leaks between two isolated SVGs.
+//
+
+casper.notebook_test(function () {
+ this.evaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text( "from IPython.core.display import SVG, display_svg\n" +
+ "s1 = '''<svg width='1cm' height='1cm' viewBox='0 0 1000 500'>" +
+ "<defs><style>rect {fill:red;}; </style></defs>" +
+ "<rect id='r1' x='200' y='100' width='600' height='300' /></svg>" +
+ "'''\n" +
+ "s2 = '''<svg width='1cm' height='1cm' viewBox='0 0 1000 500'>" +
+ "<rect id='r2' x='200' y='100' width='600' height='300' /></svg>" +
+ "'''\n" +
+ "display_svg(SVG(s1), metadata=dict(isolated=True))\n" +
+ "display_svg(SVG(s2), metadata=dict(isolated=True))\n"
+ );
+ cell.execute();
+ });
+
+ this.then(function() {
+ var fname=this.test.currentTestFile.split('/').pop().toLowerCase();
+ this.echo(fname);
+ this.echo(this.currentUrl);
+ this.evaluate(function (n) {
+ IPython.notebook.rename(n);
+ IPython.notebook.save_notebook();
+ }, {n : fname});
+ this.echo(this.currentUrl);
+ });
+
+ this.then(function() {
+
+ url = this.evaluate(function() {
+ IPython.notebook.rename("foo");
+ return document.location.href;
+ });
+ this.echo("renamed" + url);
+ this.echo(this.currentUrl);
+ });
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var colors = this.evaluate(function () {
+ var colors = [];
+ var ifr = __utils__.findAll("iframe");
+ var svg1 = ifr[0].contentWindow.document.getElementById('r1');
+ colors[0] = window.getComputedStyle(svg1).fill;
+ var svg2 = ifr[1].contentWindow.document.getElementById('r2');
+ colors[1] = window.getComputedStyle(svg2).fill;
+ return colors;
+ });
+ this.assert_colors_equal('#ff0000', colors && colors[0], 'display_svg() First svg should be red');
+ this.assert_colors_equal('#000000', colors && colors[1], 'display_svg() Second svg should be black');
+ });
+
+ // now ensure that we can pass the same metadata dict to plain old display()
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.clear_output();
+ cell.set_text( "from IPython.display import display\n" +
+ "display(SVG(s1), metadata=dict(isolated=True))\n" +
+ "display(SVG(s2), metadata=dict(isolated=True))\n"
+ );
+ cell.execute();
+ });
+
+ this.wait_for_output(0);
+
+ // same test as original
+ this.then(function () {
+ var colors = this.evaluate(function () {
+ var colors = [];
+ var ifr = __utils__.findAll("iframe");
+ var svg1 = ifr[0].contentWindow.document.getElementById('r1');
+ colors[0] = window.getComputedStyle(svg1).fill;
+ var svg2 = ifr[1].contentWindow.document.getElementById('r2');
+ colors[1] = window.getComputedStyle(svg2).fill;
+ return colors;
+ });
+ this.assert_colors_equal('#ff0000', colors && colors[0], 'display() First svg should be red');
+ this.assert_colors_equal('#000000', colors && colors[1], 'display() Second svg should be black');
+ });
+});
diff --git a/notebook/tests/notebook/markdown.js b/notebook/tests/notebook/markdown.js
new file mode 100644
index 0000000..344cfed
--- /dev/null
+++ b/notebook/tests/notebook/markdown.js
@@ -0,0 +1,105 @@
+//
+// Test that a Markdown cell is rendered to HTML.
+//
+casper.notebook_test(function () {
+ "use strict";
+ // Test JavaScript models.
+ var output = this.evaluate(function () {
+ IPython.notebook.to_markdown();
+ var cell = IPython.notebook.get_selected_cell();
+ cell.set_text('# Foo');
+ cell.render();
+ return cell.get_rendered();
+ });
+ this.test.assertEquals(output.trim(), '<h1 id=\"Foo\">Foo<a class=\"anchor-link\" href=\"#Foo\">¶</a></h1>', 'Markdown JS API works.');
+
+ // Test menubar entries.
+ output = this.evaluate(function () {
+ $('#to_code').mouseenter().click();
+ $('#to_markdown').mouseenter().click();
+ var cell = IPython.notebook.get_selected_cell();
+ cell.set_text('**Bar**');
+ $('#run_cell').mouseenter().click();
+ return cell.get_rendered();
+ });
+ this.test.assertEquals(output.trim(), '<p><strong>Bar</strong></p>', 'Markdown menubar items work.');
+
+ // Test toolbar buttons.
+ output = this.evaluate(function () {
+ $('#cell_type').val('code').change();
+ $('#cell_type').val('markdown').change();
+ var cell = IPython.notebook.get_selected_cell();
+ cell.set_text('*Baz*');
+ $("button[data-jupyter-action='jupyter-notebook:run-cell-and-select-next']")[0].click();
+ return cell.get_rendered();
+ });
+ this.test.assertEquals(output.trim(), '<p><em>Baz</em></p>', 'Markdown toolbar items work.');
+
+ // Test markdown headings
+
+ var text = 'multi\nline';
+
+ this.evaluate(function (text) {
+ var cell = IPython.notebook.insert_cell_at_index('markdown', 0);
+ cell.set_text(text);
+ }, {text: text});
+
+ var set_level = function (level) {
+ return casper.evaluate(function (level) {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_heading_level(level);
+ return cell.get_text();
+ }, {level: level});
+ };
+
+ var level_text;
+ var levels = [ 1, 2, 3, 4, 5, 6, 2, 1 ];
+ for (var idx=0; idx < levels.length; idx++) {
+ var level = levels[idx];
+ level_text = set_level(level);
+ var hashes = new Array(level + 1).join('#');
+ this.test.assertEquals(level_text, hashes + ' ' + text, 'markdown set_heading_level ' + level);
+ }
+
+ // Test markdown code blocks
+
+
+ function md_render_test (codeblock, result, message) {
+ // make a cell and trigger render
+ casper.thenEvaluate(function (text) {
+ var cell = Jupyter.notebook.insert_cell_at_bottom('markdown');
+ cell.set_text(text);
+ // signal window._rendered when cell render completes
+ window._rendered = null;
+ cell.events.one("rendered.MarkdownCell", function (event, data) {
+ window._rendered = data.cell.get_rendered();
+ });
+ cell.render();
+ }, {text: codeblock});
+ // wait for render to complete
+ casper.waitFor(function () {
+ return casper.evaluate(function () {
+ return window._rendered;
+ });
+ });
+ // test after waiting
+ casper.then(function () {
+ // get rendered result
+ var output = casper.evaluate(function () {
+ var rendered = window._rendered;
+ delete window._rendered;
+ return rendered;
+ });
+ // perform test
+ this.test.assertEquals(output.trim(), result, message);
+ });
+ };
+
+ var codeblock = '```\nx = 1\n```'
+ var result = '<pre><code>x = 1\n</code></pre>'
+ md_render_test(codeblock, result, 'Markdown code block no language');
+
+ codeblock = '```aaaa\nx = 1\n```'
+ result = '<pre><code class="cm-s-ipython language-aaaa">x = 1\n</code></pre>'
+ md_render_test(codeblock, result, 'Markdown code block unknown language');
+});
diff --git a/notebook/tests/notebook/merge_cells_api.js b/notebook/tests/notebook/merge_cells_api.js
new file mode 100644
index 0000000..665a9d1
--- /dev/null
+++ b/notebook/tests/notebook/merge_cells_api.js
@@ -0,0 +1,43 @@
+//
+// Test merging two notebook cells.
+//
+casper.notebook_test(function() {
+ var that = this;
+ var set_cells_text = function () {
+ that.evaluate(function() {
+ var cell_one = IPython.notebook.get_selected_cell();
+ cell_one.set_text('a = 5');
+ });
+
+ that.trigger_keydown('b');
+
+ that.evaluate(function() {
+ var cell_two = IPython.notebook.get_selected_cell();
+ cell_two.set_text('print(a)');
+ });
+ };
+
+ this.evaluate(function () {
+ IPython.notebook.command_mode();
+ });
+
+ // merge_cell_above()
+ set_cells_text();
+ var output_above = this.evaluate(function () {
+ IPython.notebook.merge_cell_above();
+ return IPython.notebook.get_selected_cell().get_text();
+ });
+
+ // merge_cell_below()
+ set_cells_text();
+ var output_below = this.evaluate(function() {
+ IPython.notebook.select(0);
+ IPython.notebook.merge_cell_below();
+ return IPython.notebook.get_selected_cell().get_text();
+ });
+
+ this.test.assertEquals(output_above, 'a = 5\n\nprint(a)',
+ 'Successful merge_cell_above().');
+ this.test.assertEquals(output_below, 'a = 5\n\nprint(a)',
+ 'Successful merge_cell_below().');
+});
diff --git a/notebook/tests/notebook/move_multiselection.js b/notebook/tests/notebook/move_multiselection.js
new file mode 100644
index 0000000..e2e1e61
--- /dev/null
+++ b/notebook/tests/notebook/move_multiselection.js
@@ -0,0 +1,64 @@
+
+
+// Test
+casper.notebook_test(function () {
+ this.append_cell('1');
+ this.append_cell('2');
+ this.append_cell('3');
+ this.append_cell('4');
+ this.append_cell('5');
+ this.append_cell('6');
+
+ function assert_order(that, pre_message, expected_state){
+
+ for (var i=1; i<expected_state.length; i++){
+ that.test.assertEquals(that.get_cell_text(i), expected_state[i],
+ pre_message+': Verify that cell `' + i + '` has for content: `'+ expected_state[i] + '` found : ' + that.get_cell_text(i)
+ );
+ }
+ }
+
+
+
+ this.then(function () {
+ // select 3 first cells
+ this.select_cell(0);
+ this.select_cell(2, false);
+
+ this.evaluate(function () {
+ Jupyter.notebook.move_selection_up();
+ });
+
+ // should not move up at top
+ assert_order(this, 'move up at top', ['', '1', '2', '3', '4', '5','6'])
+
+ // we do not need to reselect, move/up down should keep the selection.
+ this.evaluate(function () {
+ Jupyter.notebook.move_selection_down();
+ Jupyter.notebook.move_selection_down();
+ Jupyter.notebook.move_selection_down();
+ Jupyter.notebook.move_selection_down();
+ });
+
+ // 4 times down should move to the 3 cells to the bottom
+ assert_order(this, 'move down to bottom', [ '3', '4', '5','6', '', '1', '2'])
+
+ this.evaluate(function () {
+ Jupyter.notebook.move_selection_down();
+ });
+
+ // they can't go any further (test it)
+ assert_order(this, 'move at to top', [ '3', '4', '5','6', '', '1', '2'])
+
+ this.evaluate(function () {
+ Jupyter.notebook.move_selection_up();
+ Jupyter.notebook.move_selection_up();
+ Jupyter.notebook.move_selection_up();
+ Jupyter.notebook.move_selection_up();
+ });
+
+ // bring them back on top.
+ assert_order(this, 'move up to top', ['', '1', '2', '3', '4', '5','6'])
+
+ });
+});
diff --git a/notebook/tests/notebook/multiselect.js b/notebook/tests/notebook/multiselect.js
new file mode 100644
index 0000000..2a7e48e
--- /dev/null
+++ b/notebook/tests/notebook/multiselect.js
@@ -0,0 +1,100 @@
+
+// Test
+casper.notebook_test(function () {
+ var that = this;
+
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+
+ var b = 'print("b")';
+ index = this.append_cell(b);
+
+ var c = 'print("c")';
+ index = this.append_cell(c);
+
+ this.then(function () {
+ var selectedIndex = this.evaluate(function () {
+ Jupyter.notebook.select(0);
+ return Jupyter.notebook.get_selected_index();
+ });
+
+ this.test.assertEquals(this.evaluate(function() {
+ return Jupyter.notebook.get_selected_cells().length;
+ }), 1, 'only one cell is selected programmatically');
+
+ this.test.assertEquals(this.evaluate(function() {
+ return $('.cell.jupyter-soft-selected, .cell.selected').length;
+ }), 1, 'one cell is selected');
+
+ this.test.assertEquals(this.evaluate(function() {
+ Jupyter.notebook.extend_selection_by(1);
+ return Jupyter.notebook.get_selected_cells().length;
+ }), 2, 'extend selection by one');
+
+
+ this.test.assertEquals(this.evaluate(function() {
+ Jupyter.notebook.extend_selection_by(-1);
+ return Jupyter.notebook.get_selected_cells().length;
+ }), 1, 'contract selection by one');
+
+ this.test.assertEquals(this.evaluate(function() {
+ Jupyter.notebook.select(1);
+ Jupyter.notebook.extend_selection_by(-1);
+ return Jupyter.notebook.get_selected_cells().length;
+ }), 2, 'extend selection by one up');
+
+ // Test multiple markdown conversions.
+ var cell_types = this.evaluate(function() {
+ Jupyter.notebook.select(0);
+ Jupyter.notebook.extend_selection_by(2);
+ var indices = Jupyter.notebook.get_selected_cells_indices();
+ Jupyter.notebook.cells_to_markdown();
+ return indices.map(function(i) {
+ return Jupyter.notebook.get_cell(i).cell_type;
+ });
+ });
+ this.test.assert(cell_types.every(function(cell_type) {
+ return cell_type === 'markdown';
+ }), 'selected cells converted to markdown');
+
+ this.test.assertEquals(this.evaluate(function() {
+ return Jupyter.notebook.get_selected_cells_indices();
+ }).length, 1, 'one cell selected after convert');
+
+ // Test multiple raw conversions.
+ cell_types = this.evaluate(function() {
+ Jupyter.notebook.select(0);
+ Jupyter.notebook.extend_selection_by(2);
+ var indices = Jupyter.notebook.get_selected_cells_indices();
+ Jupyter.notebook.cells_to_raw();
+ return indices.map(function(i) {
+ return Jupyter.notebook.get_cell(i).cell_type;
+ });
+ });
+ this.test.assert(cell_types.every(function(cell_type) {
+ return cell_type === 'raw';
+ }), 'selected cells converted to raw');
+
+ this.test.assertEquals(this.evaluate(function() {
+ return Jupyter.notebook.get_selected_cells_indices();
+ }).length, 1, 'one cell selected after convert');
+
+ // Test multiple code conversions.
+ cell_types = this.evaluate(function() {
+ Jupyter.notebook.select(0);
+ Jupyter.notebook.extend_selection_by(2);
+ var indices = Jupyter.notebook.get_selected_cells_indices();
+ Jupyter.notebook.cells_to_code();
+ return indices.map(function(i) {
+ return Jupyter.notebook.get_cell(i).cell_type;
+ });
+ });
+ this.test.assert(cell_types.every(function(cell_type) {
+ return cell_type === 'code';
+ }), 'selected cells converted to code');
+
+ this.test.assertEquals(this.evaluate(function() {
+ return Jupyter.notebook.get_selected_cells_indices();
+ }).length, 1, 'one cell selected after convert');
+ });
+});
diff --git a/notebook/tests/notebook/multiselect_toggle.js b/notebook/tests/notebook/multiselect_toggle.js
new file mode 100644
index 0000000..1aff8cc
--- /dev/null
+++ b/notebook/tests/notebook/multiselect_toggle.js
@@ -0,0 +1,70 @@
+// Test
+casper.notebook_test(function () {
+ var that = this;
+
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+
+ var b = 'print("b")';
+ index = this.append_cell(b);
+
+ var c = 'print("c")';
+ index = this.append_cell(c);
+
+ this.then(function () {
+ /**
+ * Test that cells, which start off not collapsed, are collapsed after
+ * calling the multiselected cell toggle.
+ */
+ var cell_output_states = this.evaluate(function() {
+ Jupyter.notebook.select(0);
+ Jupyter.notebook.extend_selection_by(2);
+ var indices = Jupyter.notebook.get_selected_cells_indices();
+ Jupyter.notebook.toggle_cells_outputs();
+ return indices.map(function(index) {
+ return Jupyter.notebook.get_cell(index).collapsed;
+ });
+ });
+
+ this.test.assert(cell_output_states.every(function(cell_output_state) {
+ return cell_output_state == false;
+ }), "ensure that all cells are not collapsed");
+
+ /**
+ * Test that cells, which start off not scrolled are scrolled after
+ * calling the multiselected scroll toggle.
+ */
+ var cell_scrolled_states = this.evaluate(function() {
+ Jupyter.notebook.select(0);
+ Jupyter.notebook.extend_selection_by(2);
+ var indices = Jupyter.notebook.get_selected_cells_indices();
+ Jupyter.notebook.toggle_cells_outputs_scroll();
+ return indices.map(function(index) {
+ return Jupyter.notebook.get_cell(index).output_area.scroll_state;
+ });
+ });
+
+ this.test.assert(cell_scrolled_states.every(function(cell_scroll_state) {
+ return cell_scroll_state;
+ }), "ensure that all have scrolling enabled");
+
+ /**
+ * Test that cells, which start off not cleared are cleared after
+ * calling the multiselected scroll toggle.
+ */
+ var cell_outputs_cleared = this.evaluate(function() {
+ Jupyter.notebook.select(0);
+ Jupyter.notebook.extend_selection_by(2);
+ var indices = Jupyter.notebook.get_selected_cells_indices();
+ Jupyter.notebook.clear_cells_outputs();
+ return indices.map(function(index) {
+ return Jupyter.notebook.get_cell(index).output_area.element.html();
+ });
+ });
+
+ this.test.assert(cell_outputs_cleared.every(function(cell_output_state) {
+ return cell_output_state == "";
+ }), "ensure that all cells are cleared");
+
+ });
+});
diff --git a/notebook/tests/notebook/notifications.js b/notebook/tests/notebook/notifications.js
new file mode 100644
index 0000000..7366930
--- /dev/null
+++ b/notebook/tests/notebook/notifications.js
@@ -0,0 +1,116 @@
+// Test the notification area and widgets
+
+casper.notebook_test(function () {
+ var that = this;
+ var widget = function (name) {
+ return that.evaluate(function (name) {
+ return (IPython.notification_area.widget(name) !== undefined);
+ }, name);
+ };
+
+ var get_widget = function (name) {
+ return that.evaluate(function (name) {
+ return (IPython.notification_area.get_widget(name) !== undefined);
+ }, name);
+ };
+
+ var new_notification_widget = function (name) {
+ return that.evaluate(function (name) {
+ return (IPython.notification_area.new_notification_widget(name) !== undefined);
+ }, name);
+ };
+
+ var widget_has_class = function (name, class_name) {
+ return that.evaluate(function (name, class_name) {
+ var w = IPython.notification_area.get_widget(name);
+ return w.element.hasClass(class_name);
+ }, name, class_name);
+ };
+
+ var widget_message = function (name) {
+ return that.evaluate(function (name) {
+ var w = IPython.notification_area.get_widget(name);
+ return w.get_message();
+ }, name);
+ };
+
+ this.then(function () {
+ // check that existing widgets are there
+ this.test.assert(get_widget('kernel') && widget('kernel'), 'The kernel notification widget exists');
+ this.test.assert(get_widget('notebook') && widget('notbook'), 'The notebook notification widget exists');
+
+ // try getting a non-existant widget
+ this.test.assertRaises(get_widget, 'foo', 'get_widget: error is thrown');
+
+ // try creating a non-existant widget
+ this.test.assert(widget('bar'), 'widget: new widget is created');
+
+ // try creating a widget that already exists
+ this.test.assertRaises(new_notification_widget, 'kernel', 'new_notification_widget: error is thrown');
+ });
+
+ // test creating 'info' messages
+ this.thenEvaluate(function () {
+ var tnw = IPython.notification_area.widget('test');
+ tnw.info('test info');
+ });
+ this.waitUntilVisible('#notification_test', function () {
+ this.test.assert(widget_has_class('test', 'info'), 'info: class is correct');
+ this.test.assertEquals(widget_message('test'), 'test info', 'info: message is correct');
+ });
+
+ // test creating 'warning' messages
+ this.thenEvaluate(function () {
+ var tnw = IPython.notification_area.widget('test');
+ tnw.warning('test warning');
+ });
+ this.waitUntilVisible('#notification_test', function () {
+ this.test.assert(widget_has_class('test', 'warning'), 'warning: class is correct');
+ this.test.assertEquals(widget_message('test'), 'test warning', 'warning: message is correct');
+ });
+
+ // test creating 'danger' messages
+ this.thenEvaluate(function () {
+ var tnw = IPython.notification_area.widget('test');
+ tnw.danger('test danger');
+ });
+ this.waitUntilVisible('#notification_test', function () {
+ this.test.assert(widget_has_class('test', 'danger'), 'danger: class is correct');
+ this.test.assertEquals(widget_message('test'), 'test danger', 'danger: message is correct');
+ });
+
+ // test message timeout
+ this.thenEvaluate(function () {
+ var tnw = IPython.notification_area.widget('test');
+ tnw.set_message('test timeout', 1000);
+ });
+ this.waitUntilVisible('#notification_test', function () {
+ this.test.assertEquals(widget_message('test'), 'test timeout', 'timeout: message is correct');
+ });
+ this.waitWhileVisible('#notification_test', function () {
+ this.test.assertEquals(widget_message('test'), '', 'timeout: message was cleared');
+ });
+
+ // test click callback
+ this.thenEvaluate(function () {
+ var tnw = IPython.notification_area.widget('test');
+ tnw._clicked = false;
+ tnw.set_message('test click', undefined, function () {
+ tnw._clicked = true;
+ return true;
+ });
+ });
+ this.waitUntilVisible('#notification_test', function () {
+ this.test.assertEquals(widget_message('test'), 'test click', 'callback: message is correct');
+ this.click('#notification_test');
+ });
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython.notification_area.widget('test')._clicked;
+ });
+ }, function () {
+ this.waitWhileVisible('#notification_test', function () {
+ this.test.assertEquals(widget_message('test'), '', 'callback: message was cleared');
+ });
+ });
+});
diff --git a/notebook/tests/notebook/output.js b/notebook/tests/notebook/output.js
new file mode 100644
index 0000000..db6f276
--- /dev/null
+++ b/notebook/tests/notebook/output.js
@@ -0,0 +1,126 @@
+//
+// Various output tests
+//
+
+casper.notebook_test(function () {
+
+ this.test_coalesced_output = function (msg, code, expected) {
+ this.then(function () {
+ this.echo("Test coalesced output: " + msg);
+ });
+
+ this.thenEvaluate(function (code) {
+ IPython.notebook.insert_cell_at_index("code", 0);
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text(code);
+ cell.execute();
+ }, {code: code});
+
+ this.wait_for_output(0);
+
+ this.then(function () {
+ var results = this.evaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ return cell.output_area.outputs;
+ });
+ this.test.assertEquals(results.length, expected.length, "correct number of outputs");
+ for (var i = 0; i < results.length; i++) {
+ var r = results[i];
+ var ex = expected[i];
+ this.test.assertEquals(r.output_type, ex.output_type, "output " + i);
+ if (r.output_type === 'stream') {
+ this.test.assertEquals(r.name, ex.name, "stream " + i);
+ this.test.assertEquals(r.text, ex.text, "content " + i);
+ }
+ }
+ });
+
+ };
+
+ this.thenEvaluate(function () {
+ IPython.notebook.insert_cell_at_index("code", 0);
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text([
+ "from __future__ import print_function",
+ "import sys",
+ "from IPython.display import display"
+ ].join("\n")
+ );
+ cell.execute();
+ });
+
+ this.test_coalesced_output("stdout", [
+ "print(1)",
+ "sys.stdout.flush()",
+ "print(2)",
+ "sys.stdout.flush()",
+ "print(3)"
+ ].join("\n"), [{
+ output_type: "stream",
+ name: "stdout",
+ text: "1\n2\n3\n"
+ }]
+ );
+
+ this.test_coalesced_output("stdout+sdterr", [
+ "print(1)",
+ "sys.stdout.flush()",
+ "print(2)",
+ "print(3, file=sys.stderr)"
+ ].join("\n"), [{
+ output_type: "stream",
+ name: "stdout",
+ text: "1\n2\n"
+ },{
+ output_type: "stream",
+ name: "stderr",
+ text: "3\n"
+ }]
+ );
+
+ this.test_coalesced_output("display splits streams", [
+ "print(1)",
+ "sys.stdout.flush()",
+ "display(2)",
+ "print(3)"
+ ].join("\n"), [{
+ output_type: "stream",
+ name: "stdout",
+ text: "1\n"
+ },{
+ output_type: "display_data",
+ },{
+ output_type: "stream",
+ name: "stdout",
+ text: "3\n"
+ }]
+ );
+ this.test_coalesced_output("test nested svg", [
+ 'from IPython.display import SVG',
+ 'nested_svg="""',
+ '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" >',
+ ' <svg x="0">',
+ ' <rect x="10" y="10" height="80" width="80" style="fill: #0000ff"/>',
+ ' </svg>',
+ ' <svg x="100">',
+ ' <rect x="10" y="10" height="80" width="80" style="fill: #00cc00"/>',
+ ' </svg>',
+ '</svg>"""',
+ 'SVG(nested_svg)'
+ ].join("\n"), [{
+ output_type: "execute_result",
+ data: {
+ "text/plain" : "<IPython.core.display.SVG object>",
+ "image/svg+xml": [
+ '<svg height="200" width="100" xmlns="http://www.w3.org/2000/svg">',
+ ' <svg x="0">',
+ ' <rect height="80" style="fill: #0000ff" width="80" x="10" y="10"/>',
+ ' </svg>',
+ ' <svg x="100">',
+ ' <rect height="80" style="fill: #00cc00" width="80" x="10" y="10"/>',
+ ' </svg>',
+ '</svg>'].join("\n")
+ },
+ }]
+ );
+});
diff --git a/notebook/tests/notebook/prompt_numbers.js b/notebook/tests/notebook/prompt_numbers.js
new file mode 100644
index 0000000..21e6acd
--- /dev/null
+++ b/notebook/tests/notebook/prompt_numbers.js
@@ -0,0 +1,36 @@
+
+// Test
+casper.notebook_test(function () {
+
+ var that = this;
+ var set_prompt = function (i, val) {
+ that.evaluate(function (i, val) {
+ var cell = IPython.notebook.get_cell(i);
+ cell.set_input_prompt(val);
+ }, [i, val]);
+ };
+
+ var get_prompt = function (i) {
+ return that.evaluate(function (i) {
+ var elem = IPython.notebook.get_cell(i).element;
+ return elem.find('div.input_prompt').html();
+ }, [i]);
+ };
+
+ this.then(function () {
+ var a = 'print("a")';
+ var index = this.append_cell(a);
+
+ this.test.assertEquals(get_prompt(index), "In&nbsp;[&nbsp;]:", "prompt number is &nbsp; by default");
+ set_prompt(index, 2);
+ this.test.assertEquals(get_prompt(index), "In&nbsp;[2]:", "prompt number is 2");
+ set_prompt(index, 0);
+ this.test.assertEquals(get_prompt(index), "In&nbsp;[0]:", "prompt number is 0");
+ set_prompt(index, "*");
+ this.test.assertEquals(get_prompt(index), "In&nbsp;[*]:", "prompt number is *");
+ set_prompt(index, undefined);
+ this.test.assertEquals(get_prompt(index), "In&nbsp;[&nbsp;]:", "prompt number is &nbsp;");
+ set_prompt(index, null);
+ this.test.assertEquals(get_prompt(index), "In&nbsp;[&nbsp;]:", "prompt number is &nbsp;");
+ });
+});
diff --git a/notebook/tests/notebook/roundtrip.js b/notebook/tests/notebook/roundtrip.js
new file mode 100644
index 0000000..262a6b7
--- /dev/null
+++ b/notebook/tests/notebook/roundtrip.js
@@ -0,0 +1,247 @@
+// Test opening a rich notebook, saving it, and reopening it again.
+//
+//toJSON fromJSON toJSON and do a string comparison
+
+
+// this is just a copy of OutputArea.mime_mape_r in IPython/html/static/notebook/js/outputarea.js
+mime = {
+ "text" : "text/plain",
+ "html" : "text/html",
+ "svg" : "image/svg+xml",
+ "png" : "image/png",
+ "jpeg" : "image/jpeg",
+ "latex" : "text/latex",
+ "json" : "application/json",
+ "javascript" : "application/javascript",
+ };
+
+var black_dot_jpeg="u\"\"\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACodICUgGiolIiUvLSoyP2lEPzo6P4FcYUxpmYagnpaG\nk5GovfLNqLPltZGT0v/V5fr/////o8v///////L/////2wBDAS0vLz83P3xERHz/rpOu////////\n////////////////////////////////////////////////////////////wgARCAABAAEDAREA\nAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEA\nAhADEAAAARn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAA\nAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/AX//xAAUEAEA\nAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nIX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEA\nAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nEH//2Q==\"\"\"";
+var black_dot_png = 'u\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QA\\niAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AA\\nAAACAAHiIbwzAAAAAElFTkSuQmCC\"';
+var svg = "\"<svg width='1cm' height='1cm' viewBox='0 0 1000 500'><defs><style>rect {fill:red;}; </style></defs><rect id='r1' x='200' y='100' width='600' height='300' /></svg>\"";
+
+// helper function to ensure that the short_name is found in the toJSON
+// represetnation, while the original in-memory cell retains its long mimetype
+// name, and that fromJSON also gets its long mimetype name
+function assert_has(short_name, json, result, result2) {
+ var long_name = mime[short_name];
+ this.test.assertFalse(json[0].data.hasOwnProperty(short_name),
+ "toJSON() representation doesn't use " + short_name);
+ this.test.assertTrue(json[0].data.hasOwnProperty(long_name),
+ 'toJSON() representation uses ' + long_name);
+ this.test.assertTrue(result.data.hasOwnProperty(long_name),
+ 'toJSON() original embedded JSON keeps ' + long_name);
+ this.test.assertTrue(result2.data.hasOwnProperty(long_name),
+ 'fromJSON() embedded ' + short_name + ' gets mime key ' + long_name);
+}
+
+// helper function for checkout that the first two cells have a particular
+// output_type (either 'execute_result' or 'display_data'), and checks the to/fromJSON
+// for a set of mimetype keys, ensuring the old short names ('javascript', 'text',
+// 'png', etc) are not used.
+function check_output_area(output_type, keys) {
+ this.wait_for_output(0);
+ var json = this.evaluate(function() {
+ var json = IPython.notebook.get_cell(0).output_area.toJSON();
+ // appended cell will initially be empty, let's add some output
+ IPython.notebook.get_cell(1).output_area.fromJSON(json);
+ return json;
+ });
+ // The evaluate call above happens asynchronously: wait for cell[1] to have output
+ this.wait_for_output(1);
+ var result = this.get_output_cell(0);
+ var result2 = this.get_output_cell(1);
+ this.test.assertEquals(result.output_type, output_type,
+ 'testing ' + output_type + ' for ' + keys.join(' and '));
+
+ for (var idx in keys) {
+ assert_has.apply(this, [keys[idx], json, result, result2]);
+ }
+}
+
+
+// helper function to clear the first two cells, set the text of and execute
+// the first one
+function clear_and_execute(that, code) {
+ that.evaluate(function() {
+ IPython.notebook.get_cell(0).clear_output();
+ IPython.notebook.get_cell(1).clear_output();
+ });
+ that.then(function () {
+ that.set_cell_text(0, code);
+ that.execute_cell(0);
+ that.wait_for_idle();
+ });
+}
+
+casper.notebook_test(function () {
+ this.evaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ // "we have to make messes to find out who we are"
+ cell.set_text([
+ "%%javascript",
+ "IPython.notebook.insert_cell_below('code')"
+ ].join('\n')
+ );
+ });
+
+ this.execute_cell_then(0, function () {
+ var result = this.get_output_cell(0);
+ var num_cells = this.get_cells_length();
+ this.test.assertEquals(num_cells, 2, '%%javascript magic works');
+ this.test.assertTrue(result.data.hasOwnProperty('application/javascript'),
+ 'testing JS embedded with mime key');
+ });
+
+ //this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
+ this.then(function () {
+ clear_and_execute(this, [
+ "%%javascript",
+ "var a=5;"
+ ].join('\n'));
+ });
+
+
+ this.then(function () {
+ check_output_area.apply(this, ['display_data', ['javascript']]);
+
+ });
+
+ this.then(function() {
+ clear_and_execute(this, '%lsmagic');
+ });
+
+ this.then(function () {
+ check_output_area.apply(this, ['execute_result', ['text', 'json']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "x = %lsmagic\nfrom IPython.display import display; display(x)");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['display_data', ['text', 'json']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.display import Latex; Latex('$X^2$')");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['execute_result', ['text', 'latex']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.display import Latex, display; display(Latex('$X^2$'))");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['display_data', ['text', 'latex']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.display import HTML; HTML('<b>it works!</b>')");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['execute_result', ['text', 'html']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.display import HTML, display; display(HTML('<b>it works!</b>'))");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['display_data', ['text', 'html']]);
+ });
+
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.display import Image; Image(" + black_dot_png + ")");
+ });
+ this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['execute_result', ['text', 'png']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.display import Image, display; display(Image(" + black_dot_png + "))");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['display_data', ['text', 'png']]);
+ });
+
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.display import Image; Image(" + black_dot_jpeg + ", format='jpeg')");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['execute_result', ['text', 'jpeg']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.display import Image, display; display(Image(" + black_dot_jpeg + ", format='jpeg'))");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['display_data', ['text', 'jpeg']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.core.display import SVG; SVG(" + svg + ")");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['execute_result', ['text', 'svg']]);
+ });
+
+ this.then(function() {
+ clear_and_execute(this,
+ "from IPython.core.display import SVG, display; display(SVG(" + svg + "))");
+ });
+
+ this.then(function ( ) {
+ check_output_area.apply(this, ['display_data', ['text', 'svg']]);
+ });
+
+ this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
+
+ this.then(function() {
+ clear_and_execute(this, [
+ "from IPython.core.formatters import HTMLFormatter",
+ "x = HTMLFormatter()",
+ "x.format_type = 'text/superfancymimetype'",
+ "get_ipython().display_formatter.formatters['text/superfancymimetype'] = x",
+ "from IPython.display import HTML, display",
+ 'display(HTML("yo"))',
+ "HTML('hello')"].join('\n')
+ );
+
+ });
+
+ this.wait_for_output(0, 1);
+
+ this.then(function () {
+ var long_name = 'text/superfancymimetype';
+ var result = this.get_output_cell(0);
+ this.test.assertTrue(result.data.hasOwnProperty(long_name),
+ 'display_data custom mimetype ' + long_name);
+ result = this.get_output_cell(0, 1);
+ this.test.assertTrue(result.data.hasOwnProperty(long_name),
+ 'execute_result custom mimetype ' + long_name);
+
+ });
+
+});
diff --git a/notebook/tests/notebook/safe_append_output.js b/notebook/tests/notebook/safe_append_output.js
new file mode 100644
index 0000000..d151a21
--- /dev/null
+++ b/notebook/tests/notebook/safe_append_output.js
@@ -0,0 +1,32 @@
+//
+// Test validation in append_output
+//
+// Invalid output data is stripped and logged.
+//
+
+casper.notebook_test(function () {
+ // this.printLog();
+ var messages = [];
+ this.on('remote.message', function (msg) {
+ messages.push(msg);
+ });
+
+ this.evaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text( "dp = get_ipython().display_pub\n" +
+ "dp.publish({'text/plain' : '5', 'image/png' : 5})"
+ );
+ cell.execute();
+ });
+
+ this.wait_for_output(0);
+ this.on('remote.message', function () {});
+
+ this.then(function () {
+ var output = this.get_output_cell(0);
+ this.test.assert(messages.length > 0, "Captured log message");
+ this.test.assertEquals(messages[messages.length-1].substr(0,26), "Invalid type for image/png", "Logged Invalid type message");
+ this.test.assertEquals(output.data['image/png'], undefined, "Non-string png data was stripped");
+ this.test.assertEquals(output.data['text/plain'], '5', "text data is fine");
+ });
+});
diff --git a/notebook/tests/notebook/save.js b/notebook/tests/notebook/save.js
new file mode 100644
index 0000000..0e56652
--- /dev/null
+++ b/notebook/tests/notebook/save.js
@@ -0,0 +1,112 @@
+//
+// Test saving a notebook with escaped characters
+//
+
+casper.notebook_test(function () {
+ // don't use unicode with ambiguous composed/decomposed normalization
+ // because the filesystem may use a different normalization than literals.
+ // This causes no actual problems, but will break string comparison.
+ var nbname = "has#hash and space and unicø∂e.ipynb";
+
+ this.append_cell("s = '??'", 'code');
+
+ this.thenEvaluate(function (nbname) {
+ require(['base/js/events'], function (events) {
+ IPython.notebook.set_notebook_name(nbname);
+ IPython._save_success = IPython._save_failed = false;
+ events.on('notebook_saved.Notebook', function () {
+ IPython._save_success = true;
+ });
+ events.on('notebook_save_failed.Notebook',
+ function (event, error) {
+ IPython._save_failed = "save failed with " + error;
+ });
+ IPython.notebook.save_notebook();
+ });
+ }, {nbname:nbname});
+
+ this.waitFor(function () {
+ return this.evaluate(function(){
+ return IPython._save_failed || IPython._save_success;
+ });
+ });
+
+ this.then(function(){
+ var success_failure = this.evaluate(function(){
+ return [IPython._save_success, IPython._save_failed];
+ });
+ this.test.assertEquals(success_failure[1], false, "Save did not fail");
+ this.test.assertEquals(success_failure[0], true, "Save OK");
+
+ var current_name = this.evaluate(function(){
+ return IPython.notebook.notebook_name;
+ });
+ this.test.assertEquals(current_name, nbname, "Save with complicated name");
+ var current_path = this.evaluate(function(){
+ return IPython.notebook.notebook_path;
+ });
+ this.test.assertEquals(current_path, nbname, "path OK");
+ });
+
+ this.thenEvaluate(function(){
+ IPython._checkpoint_created = false;
+ require(['base/js/events'], function (events) {
+ events.on('checkpoint_created.Notebook', function (evt, data) {
+ IPython._checkpoint_created = true;
+ });
+ });
+ IPython.notebook.save_checkpoint();
+ });
+
+ this.waitFor(function () {
+ return this.evaluate(function(){
+ return IPython._checkpoint_created;
+ });
+ });
+
+ this.then(function(){
+ var checkpoints = this.evaluate(function(){
+ return IPython.notebook.checkpoints;
+ });
+ this.test.assertEquals(checkpoints.length, 1, "checkpoints OK");
+ });
+
+ this.then(function(){
+ this.open_dashboard();
+ });
+
+ this.then(function(){
+ var notebook_url = this.evaluate(function(nbname){
+ var escaped_name = encodeURIComponent(nbname);
+ var return_this_thing = null;
+ $("a.item_link").map(function (i,a) {
+ if (a.href.indexOf(escaped_name) >= 0) {
+ return_this_thing = a.href;
+ return;
+ }
+ });
+ return return_this_thing;
+ }, {nbname:nbname});
+ this.test.assertNotEquals(notebook_url, null, "Escaped URL in notebook list");
+ // open the notebook
+ this.open(notebook_url);
+ });
+
+ // wait for the notebook
+ this.waitFor(this.kernel_running);
+
+ this.waitFor(function() {
+ return this.evaluate(function () {
+ return IPython && IPython.notebook && true;
+ });
+ });
+
+ this.then(function(){
+ // check that the notebook name is correct
+ var notebook_name = this.evaluate(function(){
+ return IPython.notebook.notebook_name;
+ });
+ this.test.assertEquals(notebook_name, nbname, "Notebook name is correct");
+ });
+
+});
diff --git a/notebook/tests/notebook/shutdown.js b/notebook/tests/notebook/shutdown.js
new file mode 100644
index 0000000..3bdd38f
--- /dev/null
+++ b/notebook/tests/notebook/shutdown.js
@@ -0,0 +1,49 @@
+//
+// Test shutdown of a kernel.
+//
+casper.notebook_test(function () {
+ // XXX: test.begin allows named sections but requires casperjs 1.1.0-DEV.
+ // We will put it back into place when the next version of casper is
+ // released. Following that, all instances of this.test can be changed
+ // to just test.
+ //this.test.begin("shutdown tests (notebook)", 2, function(test) {
+
+ // Our shutdown test closes the browser window, which will delete the
+ // casper browser object, and the rest of the test suite will fail with
+ // errors that look like:
+ //
+ // "Error: cannot access member `evaluate' of deleted QObject"
+ //
+ // So what we do here is make a quick popup window, and run the test inside
+ // of it.
+ this.then(function() {
+ this.evaluate(function(url){
+ window.open(url);
+ }, {url : this.getCurrentUrl()});
+ })
+
+ this.waitForPopup('');
+ this.withPopup('', function () {
+ this.thenEvaluate(function () {
+ $('#kill_and_exit').click();
+ });
+
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text('a=10; print(a)');
+ cell.execute();
+ });
+
+ this.then(function () {
+ var outputs = this.evaluate(function() {
+ return IPython.notebook.get_cell(0).output_area.outputs;
+ })
+ this.test.assertEquals(outputs.length, 0, "after shutdown: no execution results");
+ this.test.assertNot(this.kernel_running(),
+ 'after shutdown: IPython.notebook.kernel.running is false ');
+ });
+ });
+
+//}); // end of test.begin
+});
+
diff --git a/notebook/tests/notebook/undelete.js b/notebook/tests/notebook/undelete.js
new file mode 100644
index 0000000..371c183
--- /dev/null
+++ b/notebook/tests/notebook/undelete.js
@@ -0,0 +1,118 @@
+//
+// Test undeleting cells.
+//
+casper.notebook_test(function () {
+ var that = this;
+
+ var assert_selected_cells = function (action, indices) {
+ var selected = that.evaluate(function () {
+ return IPython.notebook.get_selected_cells_indices();
+ });
+ that.test.assertEquals( selected, indices, action + "; verify selected cells");
+ };
+
+ var assert_cells = function (action, cells, index) {
+ var msg = action + "; there are " + cells.length + " cells";
+ that.test.assertEquals(that.get_cells_length(), cells.length, msg);
+
+ var i;
+ for (i = 0; i < cells.length; i++) {
+ msg = action + "; cell " + i + " has correct text";
+ that.test.assertEquals(that.get_cell_text(i), cells[i], msg);
+ }
+
+ that.validate_notebook_state(action, 'command', index);
+ assert_selected_cells(action, [index]);
+ };
+
+ var a = 'print("a")';
+ this.set_cell_text(0, a);
+
+ var b = 'print("b")';
+ this.append_cell(b);
+
+ var c = 'print("c")';
+ this.append_cell(c);
+
+ var d = 'print("d")';
+ this.append_cell(d);
+
+ // Verify initial state
+ this.select_cell(0);
+ this.trigger_keydown('esc');
+ assert_cells("initial state", [a, b, c, d], 0);
+
+ // Delete cell 1
+ this.select_cell(1);
+ this.trigger_keydown('esc');
+ this.trigger_keydown('d', 'd');
+ assert_cells("delete cell 1", [a, c, d], 1);
+
+ // Undelete cell 1
+ this.evaluate(function () {
+ IPython.notebook.undelete_cell();
+ });
+ assert_cells("undelete cell 1", [a, b, c, d], 2);
+
+ // Merge cells 1-2
+ var bc = b + "\n\n" + c;
+ this.select_cell(1);
+ this.trigger_keydown('esc');
+ this.trigger_keydown('shift-j');
+ assert_selected_cells("select cells 1-2", [1, 2]);
+ this.trigger_keydown('shift-m');
+ this.trigger_keydown('esc');
+ assert_cells("merge cells 1-2", [a, bc, d], 1);
+
+ // Undo merge
+ this.evaluate(function () {
+ IPython.notebook.undelete_cell();
+ });
+ assert_cells("undo merge", [a, bc, c, d], 1);
+
+ // Merge cells 3-2
+ var cd = c + "\n\n" + d;
+ this.select_cell(3);
+ this.trigger_keydown('esc');
+ this.trigger_keydown('shift-k');
+ assert_selected_cells("select cells 3-2", [2, 3]);
+ this.trigger_keydown('shift-m');
+ this.trigger_keydown('esc');
+ assert_cells("merge cells 3-2", [a, bc, cd], 2);
+
+ // Undo merge
+ this.evaluate(function () {
+ IPython.notebook.undelete_cell();
+ });
+ assert_cells("undo merge", [a, bc, cd, d], 2);
+
+ // Merge below
+ var abc = a + "\n\n" + bc;
+ this.select_cell(0);
+ this.trigger_keydown('esc');
+ this.evaluate(function () {
+ IPython.notebook.merge_cell_below();
+ });
+ assert_cells("merge cell below", [abc, cd, d], 0);
+
+ // Undo merge
+ this.evaluate(function () {
+ IPython.notebook.undelete_cell();
+ });
+ assert_cells("undo merge", [abc, bc, cd, d], 0);
+
+ // Merge above
+ var bccd = bc + "\n\n" + cd;
+ this.select_cell(2);
+ this.trigger_keydown('esc');
+ this.evaluate(function () {
+ IPython.notebook.merge_cell_above();
+ });
+ assert_cells("merge cell above", [abc, bccd, d], 1);
+
+ // Undo merge
+ this.evaluate(function () {
+ IPython.notebook.undelete_cell();
+ });
+ assert_cells("undo merge", [abc, bc, bccd, d], 2);
+});
diff --git a/notebook/tests/services/kernel.js b/notebook/tests/services/kernel.js
new file mode 100644
index 0000000..df69fdf
--- /dev/null
+++ b/notebook/tests/services/kernel.js
@@ -0,0 +1,325 @@
+
+//
+// Kernel tests
+//
+casper.notebook_test(function () {
+ // test that the kernel is running
+ this.then(function () {
+ this.test.assert(this.kernel_running(), 'kernel is running');
+ });
+
+ // test list
+ this.thenEvaluate(function () {
+ IPython._kernels = null;
+ IPython.notebook.kernel.list(function (data) {
+ IPython._kernels = data;
+ });
+ });
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._kernels !== null;
+ });
+ });
+ this.then(function () {
+ var num_kernels = this.evaluate(function () {
+ return IPython._kernels.length;
+ });
+ this.test.assertEquals(num_kernels, 1, 'one kernel running');
+ });
+
+ // test get_info
+ var kernel_info = this.evaluate(function () {
+ return {
+ name: IPython.notebook.kernel.name,
+ id: IPython.notebook.kernel.id
+ };
+ });
+ this.thenEvaluate(function () {
+ IPython._kernel_info = null;
+ IPython.notebook.kernel.get_info(function (data) {
+ IPython._kernel_info = data;
+ });
+ });
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._kernel_info !== null;
+ });
+ });
+ this.then(function () {
+ var new_kernel_info = this.evaluate(function () {
+ return IPython._kernel_info;
+ });
+ this.test.assertEquals(kernel_info.name, new_kernel_info.name, 'kernel: name correct');
+ this.test.assertEquals(kernel_info.id, new_kernel_info.id, 'kernel: id correct');
+ });
+
+ // test interrupt
+ this.thenEvaluate(function () {
+ IPython._interrupted = false;
+ IPython.notebook.kernel.interrupt(function () {
+ IPython._interrupted = true;
+ });
+ });
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._interrupted;
+ });
+ });
+ this.then(function () {
+ var interrupted = this.evaluate(function () {
+ return IPython._interrupted;
+ });
+ this.test.assert(interrupted, 'kernel was interrupted');
+ });
+
+ // test restart
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.restart();
+ });
+ this.waitFor(this.kernel_disconnected);
+ this.wait_for_kernel_ready();
+ this.then(function () {
+ this.test.assert(this.kernel_running(), 'kernel restarted');
+ });
+
+ // test reconnect
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.stop_channels();
+ });
+ this.waitFor(this.kernel_disconnected);
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.reconnect();
+ });
+ this.wait_for_kernel_ready();
+ this.then(function () {
+ this.test.assert(this.kernel_running(), 'kernel reconnected');
+ });
+
+ // test kernel_info_request
+ this.evaluate(function () {
+ IPython.notebook.kernel.kernel_info(
+ function(msg){
+ IPython._kernel_info_response = msg;
+ });
+ });
+ this.waitFor(
+ function () {
+ return this.evaluate(function(){
+ return IPython._kernel_info_response;
+ });
+ });
+ this.then(function () {
+ var kernel_info_response = this.evaluate(function(){
+ return IPython._kernel_info_response;
+ });
+ this.test.assertTrue( kernel_info_response.msg_type === 'kernel_info_reply', 'Kernel info request return kernel_info_reply');
+ this.test.assertTrue( kernel_info_response.content !== undefined, 'Kernel_info_reply is not undefined');
+ });
+
+ // test kill
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.kill();
+ });
+ this.waitFor(this.kernel_disconnected);
+ this.then(function () {
+ this.test.assert(!this.kernel_running(), 'kernel is not running');
+ });
+
+ // test start
+ var url, base_url;
+ this.then(function () {
+ base_url = this.evaluate(function () {
+ return IPython.notebook.base_url;
+ });
+ url = this.evaluate(function () {
+ return IPython.notebook.kernel.start();
+ });
+ });
+ this.then(function () {
+ this.test.assertEquals(url, base_url + "api/kernels", "start url is correct");
+ });
+ this.wait_for_kernel_ready();
+ this.then(function () {
+ this.test.assert(this.kernel_running(), 'kernel is running');
+ });
+
+ // test start with parameters
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.kill();
+ });
+ this.waitFor(this.kernel_disconnected);
+ this.then(function () {
+ url = this.evaluate(function () {
+ return IPython.notebook.kernel.start({foo: "bar"});
+ });
+ });
+ this.then(function () {
+ this.test.assertEquals(url, base_url + "api/kernels?foo=bar", "start url with params is correct");
+ });
+ this.wait_for_kernel_ready();
+ this.then(function () {
+ this.test.assert(this.kernel_running(), 'kernel is running');
+ });
+
+ // check for events in kill/start cycle
+ this.event_test(
+ 'kill/start',
+ [
+ 'kernel_killed.Kernel',
+ 'kernel_starting.Kernel',
+ 'kernel_created.Kernel',
+ 'kernel_connected.Kernel',
+ 'kernel_ready.Kernel'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.kill();
+ });
+ this.waitFor(this.kernel_disconnected);
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.start();
+ });
+ }
+ );
+ // wait for any last idle/busy messages to be handled
+ this.wait_for_kernel_ready();
+
+ // check for events in disconnect/connect cycle
+ this.event_test(
+ 'reconnect',
+ [
+ 'kernel_reconnecting.Kernel',
+ 'kernel_connected.Kernel',
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.stop_channels();
+ IPython.notebook.kernel.reconnect(1);
+ });
+ }
+ );
+ // wait for any last idle/busy messages to be handled
+ this.wait_for_kernel_ready();
+
+ // check for events in the restart cycle
+ this.event_test(
+ 'restart',
+ [
+ 'kernel_restarting.Kernel',
+ 'kernel_created.Kernel',
+ 'kernel_connected.Kernel',
+ 'kernel_ready.Kernel'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.restart();
+ });
+ }
+ );
+ // wait for any last idle/busy messages to be handled
+ this.wait_for_kernel_ready();
+
+ // check for events in the interrupt cycle
+ this.event_test(
+ 'interrupt',
+ [
+ 'kernel_interrupting.Kernel',
+ 'kernel_busy.Kernel',
+ 'kernel_idle.Kernel'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.interrupt();
+ });
+ }
+ );
+ this.wait_for_kernel_ready();
+
+ // check for events after ws close
+ this.event_test(
+ 'ws_closed_ok',
+ [
+ 'kernel_disconnected.Kernel',
+ 'kernel_reconnecting.Kernel',
+ 'kernel_connected.Kernel',
+ 'kernel_busy.Kernel',
+ 'kernel_idle.Kernel'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel._ws_closed("", false);
+ });
+ }
+ );
+ // wait for any last idle/busy messages to be handled
+ this.wait_for_kernel_ready();
+
+ // check for events after ws close (error)
+ this.event_test(
+ 'ws_closed_error',
+ [
+ 'kernel_disconnected.Kernel',
+ 'kernel_connection_failed.Kernel',
+ 'kernel_reconnecting.Kernel',
+ 'kernel_connected.Kernel',
+ 'kernel_busy.Kernel',
+ 'kernel_idle.Kernel'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel._ws_closed("", true);
+ });
+ }
+ );
+ // wait for any last idle/busy messages to be handled
+ this.wait_for_kernel_ready();
+
+ // start the kernel back up
+ this.thenEvaluate(function () {
+ IPython.notebook.kernel.restart();
+ });
+ this.waitFor(this.kernel_running);
+ this.wait_for_kernel_ready();
+
+ // test handling of autorestarting messages
+ this.event_test(
+ 'autorestarting',
+ [
+ 'kernel_restarting.Kernel',
+ 'kernel_autorestarting.Kernel',
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text('import os\n' + 'os._exit(1)');
+ cell.execute();
+ });
+ }
+ );
+ this.wait_for_kernel_ready();
+
+ // test handling of failed restart
+ this.event_test(
+ 'failed_restart',
+ [
+ 'kernel_restarting.Kernel',
+ 'kernel_autorestarting.Kernel',
+ 'kernel_dead.Kernel'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text("import os\n" +
+ "from IPython.kernel.connect import get_connection_file\n" +
+ "with open(get_connection_file(), 'w') as f:\n" +
+ " f.write('garbage')\n" +
+ "os._exit(1)");
+ cell.execute();
+ });
+ },
+
+ // need an extra-long timeout, because it needs to try
+ // restarting the kernel 5 times!
+ 20000
+ );
+});
diff --git a/notebook/tests/services/serialize.js b/notebook/tests/services/serialize.js
new file mode 100644
index 0000000..6d5045b
--- /dev/null
+++ b/notebook/tests/services/serialize.js
@@ -0,0 +1,127 @@
+//
+// Test binary messages on websockets.
+// Only works on slimer for now, due to old websocket impl in phantomjs.
+//
+
+casper.notebook_test(function () {
+ if (!this.slimerjs) {
+ console.log("Can't test binary websockets on phantomjs.");
+ return;
+ }
+ // create EchoBuffers target on js-side.
+ // it just captures and echos comm messages.
+ this.then(function () {
+ var success = this.evaluate(function () {
+ IPython._msgs = [];
+
+ var EchoBuffers = function(comm) {
+ this.comm = comm;
+ this.comm.on_msg($.proxy(this.on_msg, this));
+ };
+
+ EchoBuffers.prototype.on_msg = function (msg) {
+ IPython._msgs.push(msg);
+ this.comm.send(msg.content.data, {}, {}, msg.buffers);
+ };
+
+ IPython.notebook.kernel.comm_manager.register_target("echo", function (comm) {
+ return new EchoBuffers(comm);
+ });
+
+ return true;
+ });
+ this.test.assertEquals(success, true, "Created echo comm target");
+ });
+
+ // Create a similar comm that captures messages Python-side
+ this.then(function () {
+ var index = this.append_cell([
+ "import os",
+ "from IPython.kernel.comm import Comm",
+ "comm = Comm(target_name='echo')",
+ "msgs = []",
+ "def on_msg(msg):",
+ " msgs.append(msg)",
+ "comm.on_msg(on_msg)"
+ ].join('\n'), 'code');
+ this.execute_cell(index);
+ });
+
+ // send a message with binary data
+ this.then(function () {
+ var index = this.append_cell([
+ "buffers = [b'\\xFF\\x00', b'\\x00\\x01\\x02']",
+ "comm.send(data='message 0', buffers=buffers)",
+ "comm.send(data='message 1')",
+ "comm.send(data='message 2', buffers=buffers)",
+ ].join('\n'), 'code');
+ this.execute_cell(index);
+ });
+
+ // wait for capture
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._msgs.length >= 3;
+ });
+ });
+
+ // validate captured buffers js-side
+ this.then(function () {
+ var msgs = this.evaluate(function () {
+ return IPython._msgs;
+ });
+ this.test.assertEquals(msgs.length, 3, "Captured three comm messages");
+
+
+
+ // check the messages came in the right order
+ this.test.assertEquals(msgs[0].content.data, "message 0", "message 0 processed first");
+ this.test.assertEquals(msgs[0].buffers.length, 2, "comm message 0 has two buffers");
+ this.test.assertEquals(msgs[1].content.data, "message 1", "message 1 processed second");
+ this.test.assertEquals(msgs[1].buffers.length, 0, "comm message 1 has no buffers");
+ this.test.assertEquals(msgs[2].content.data, "message 2", "message 2 processed third");
+ this.test.assertEquals(msgs[2].buffers.length, 2, "comm message 2 has two buffers");
+
+ // extract attributes to test in evaluate,
+ // because the raw DataViews can't be passed across
+ var buf_info = function (message, index) {
+ var buf = IPython._msgs[message].buffers[index];
+ var data = {};
+ data.byteLength = buf.byteLength;
+ data.bytes = [];
+ for (var i = 0; i < data.byteLength; i++) {
+ data.bytes.push(buf.getUint8(i));
+ }
+ return data;
+ };
+
+ var msgs_with_buffers = [0, 2];
+ for (var i = 0; i < msgs_with_buffers.length; i++) {
+ msg_index = msgs_with_buffers[i];
+ buf0 = this.evaluate(buf_info, msg_index, 0);
+ buf1 = this.evaluate(buf_info, msg_index, 1);
+ this.test.assertEquals(buf0.byteLength, 2, 'buf[0] has correct size in message '+msg_index);
+ this.test.assertEquals(buf0.bytes, [255, 0], 'buf[0] has correct bytes in message '+msg_index);
+ this.test.assertEquals(buf1.byteLength, 3, 'buf[1] has correct size in message '+msg_index);
+ this.test.assertEquals(buf1.bytes, [0, 1, 2], 'buf[1] has correct bytes in message '+msg_index);
+ }
+ });
+
+ // validate captured buffers Python-side
+ this.then(function () {
+ var index = this.append_cell([
+ "assert len(msgs) == 3, len(msgs)",
+ "bufs = msgs[0]['buffers']",
+ "assert len(bufs) == len(buffers), bufs",
+ "assert bufs[0].tobytes() == buffers[0], bufs[0]",
+ "assert bufs[1].tobytes() == buffers[1], bufs[1]",
+ "1",
+ ].join('\n'), 'code');
+ this.execute_cell(index);
+ this.wait_for_output(index);
+ this.then(function () {
+ var out = this.get_output_cell(index);
+ this.test.assertEquals(out.data['text/plain'], '1', "Python received buffers");
+ });
+ });
+});
diff --git a/notebook/tests/services/session.js b/notebook/tests/services/session.js
new file mode 100644
index 0000000..375c3bc
--- /dev/null
+++ b/notebook/tests/services/session.js
@@ -0,0 +1,180 @@
+
+//
+// Tests for the Session object
+//
+
+casper.notebook_test(function () {
+ var that = this;
+ var get_info = function () {
+ return that.evaluate(function () {
+ return JSON.parse(JSON.stringify(IPython.notebook.session._get_model()));
+ });
+ };
+
+ // test that the kernel is running
+ this.then(function () {
+ this.test.assert(this.kernel_running(), 'session: kernel is running');
+ });
+
+ // test list
+ this.thenEvaluate(function () {
+ IPython._sessions = null;
+ IPython.notebook.session.list(function (data) {
+ IPython._sessions = data;
+ });
+ });
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._sessions !== null;
+ });
+ });
+ this.then(function () {
+ var num_sessions = this.evaluate(function () {
+ return IPython._sessions.length;
+ });
+ this.test.assertEquals(num_sessions, 1, 'one session running');
+ });
+
+ // test get_info
+ var session_info = get_info();
+ this.thenEvaluate(function () {
+ IPython._session_info = null;
+ IPython.notebook.session.get_info(function (data) {
+ IPython._session_info = data;
+ });
+ });
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._session_info !== null;
+ });
+ });
+ this.then(function () {
+ var new_session_info = this.evaluate(function () {
+ return IPython._session_info;
+ });
+ this.test.assertEquals(session_info.notebook.name, new_session_info.notebook.name, 'session: notebook name correct');
+ this.test.assertEquals(session_info.notebook.path, new_session_info.notebook.path, 'session: notebook path correct');
+ this.test.assertEquals(session_info.kernel.name, new_session_info.kernel.name, 'session: kernel name correct');
+ this.test.assertEquals(session_info.kernel.id, new_session_info.kernel.id, 'session: kernel id correct');
+ });
+
+ // test rename_notebook
+ //
+ // TODO: the PATCH request isn't supported by phantom, so this test always
+ // fails, see https://github.com/ariya/phantomjs/issues/11384
+ // when this is fixed we can properly run this test
+ //
+ // this.thenEvaluate(function () {
+ // IPython._renamed = false;
+ // IPython.notebook.session.rename_notebook(
+ // "foo",
+ // "bar",
+ // function (data) {
+ // IPython._renamed = true;
+ // }
+ // );
+ // });
+ // this.waitFor(function () {
+ // return this.evaluate(function () {
+ // return IPython._renamed;
+ // });
+ // });
+ // this.then(function () {
+ // var info = get_info();
+ // this.test.assertEquals(info.notebook.name, "foo", "notebook was renamed");
+ // this.test.assertEquals(info.notebook.path, "bar", "notebook path was changed");
+ // });
+
+ // test delete
+ this.thenEvaluate(function () {
+ IPython.notebook.session.delete();
+ });
+ this.waitFor(this.kernel_disconnected);
+ this.then(function () {
+ this.test.assert(!this.kernel_running(), 'session deletes kernel');
+ });
+
+ // check for events when starting the session
+ this.event_test(
+ 'start_session',
+ [
+ 'kernel_created.Session',
+ 'kernel_connected.Kernel',
+ 'kernel_ready.Kernel'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.session.start();
+ });
+ }
+ );
+ this.wait_for_kernel_ready();
+
+ // check for events when killing the session
+ this.event_test(
+ 'delete_session',
+ ['kernel_killed.Session'],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.session.delete();
+ });
+ }
+ );
+
+ // check for events when restarting the session
+ this.event_test(
+ 'restart_session',
+ [
+ 'kernel_killed.Session',
+ 'kernel_created.Session',
+ 'kernel_connected.Kernel',
+ 'kernel_ready.Kernel'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.session.restart();
+ });
+ }
+ );
+ this.wait_for_kernel_ready();
+
+ // test handling of failed restart
+ this.event_test(
+ 'failed_restart',
+ [
+ 'kernel_restarting.Kernel',
+ 'kernel_autorestarting.Kernel',
+ 'kernel_killed.Session',
+ 'kernel_dead.Kernel',
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ var cell = IPython.notebook.get_cell(0);
+ cell.set_text("import os\n" +
+ "from IPython.kernel.connect import get_connection_file\n" +
+ "with open(get_connection_file(), 'w') as f:\n" +
+ " f.write('garbage')\n" +
+ "os._exit(1)");
+ cell.execute();
+ });
+ },
+
+ // need an extra-long timeout, because it needs to try
+ // restarting the kernel 5 times!
+ 20000
+ );
+
+ // check for events when starting a nonexistant kernel
+ this.event_test(
+ 'bad_start_session',
+ [
+ 'kernel_killed.Session',
+ 'kernel_dead.Session'
+ ],
+ function () {
+ this.thenEvaluate(function () {
+ IPython.notebook.session.restart({kernel_name: 'foo'});
+ });
+ }
+ );
+});
diff --git a/notebook/tests/test_files.py b/notebook/tests/test_files.py
new file mode 100644
index 0000000..4c33ee3
--- /dev/null
+++ b/notebook/tests/test_files.py
@@ -0,0 +1,152 @@
+# coding: utf-8
+"""Test the /files/ handler."""
+
+import io
+import os
+from unicodedata import normalize
+
+pjoin = os.path.join
+
+import requests
+import json
+
+from nbformat import write
+from nbformat.v4 import (new_notebook,
+ new_markdown_cell, new_code_cell,
+ new_output)
+
+from notebook.utils import url_path_join
+from .launchnotebook import NotebookTestBase
+from ipython_genutils import py3compat
+
+
+class FilesTest(NotebookTestBase):
+ def test_hidden_files(self):
+ not_hidden = [
+ u'å b',
+ u'å b/ç. d',
+ ]
+ hidden = [
+ u'.å b',
+ u'å b/.ç d',
+ ]
+ dirs = not_hidden + hidden
+
+ nbdir = self.notebook_dir.name
+ for d in dirs:
+ path = pjoin(nbdir, d.replace('/', os.sep))
+ if not os.path.exists(path):
+ os.mkdir(path)
+ with open(pjoin(path, 'foo'), 'w') as f:
+ f.write('foo')
+ with open(pjoin(path, '.foo'), 'w') as f:
+ f.write('.foo')
+ url = self.base_url()
+
+ for d in not_hidden:
+ path = pjoin(nbdir, d.replace('/', os.sep))
+ r = requests.get(url_path_join(url, 'files', d, 'foo'))
+ r.raise_for_status()
+ self.assertEqual(r.text, 'foo')
+ r = requests.get(url_path_join(url, 'files', d, '.foo'))
+ self.assertEqual(r.status_code, 404)
+
+ for d in hidden:
+ path = pjoin(nbdir, d.replace('/', os.sep))
+ for foo in ('foo', '.foo'):
+ r = requests.get(url_path_join(url, 'files', d, foo))
+ self.assertEqual(r.status_code, 404)
+
+ def test_contents_manager(self):
+ "make sure ContentsManager returns right files (ipynb, bin, txt)."
+
+ nbdir = self.notebook_dir.name
+ base = self.base_url()
+
+ nb = new_notebook(
+ cells=[
+ new_markdown_cell(u'Created by test ³'),
+ new_code_cell("print(2*6)", outputs=[
+ new_output("stream", text="12"),
+ ])
+ ]
+ )
+
+ with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w',
+ encoding='utf-8') as f:
+ write(nb, f, version=4)
+
+ with io.open(pjoin(nbdir, 'test.bin'), 'wb') as f:
+ f.write(b'\xff' + os.urandom(5))
+ f.close()
+
+ with io.open(pjoin(nbdir, 'test.txt'), 'w') as f:
+ f.write(u'foobar')
+ f.close()
+
+ r = requests.get(url_path_join(base, 'files', 'testnb.ipynb'))
+ self.assertEqual(r.status_code, 200)
+ self.assertIn('print(2*6)', r.text)
+ json.loads(r.text)
+
+ r = requests.get(url_path_join(base, 'files', 'test.bin'))
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.headers['content-type'], 'application/octet-stream')
+ self.assertEqual(r.content[:1], b'\xff')
+ self.assertEqual(len(r.content), 6)
+
+ r = requests.get(url_path_join(base, 'files', 'test.txt'))
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.headers['content-type'], 'text/plain')
+ self.assertEqual(r.text, 'foobar')
+
+ def test_download(self):
+ nbdir = self.notebook_dir.name
+ base = self.base_url()
+
+ text = 'hello'
+ with open(pjoin(nbdir, 'test.txt'), 'w') as f:
+ f.write(text)
+
+ r = requests.get(url_path_join(base, 'files', 'test.txt'))
+ disposition = r.headers.get('Content-Disposition', '')
+ self.assertNotIn('attachment', disposition)
+
+ r = requests.get(url_path_join(base, 'files', 'test.txt') + '?download=1')
+ disposition = r.headers.get('Content-Disposition', '')
+ self.assertIn('attachment', disposition)
+ self.assertIn('filename="test.txt"', disposition)
+
+ def test_old_files_redirect(self):
+ """pre-2.0 'files/' prefixed links are properly redirected"""
+ nbdir = self.notebook_dir.name
+ base = self.base_url()
+
+ os.mkdir(pjoin(nbdir, 'files'))
+ os.makedirs(pjoin(nbdir, 'sub', 'files'))
+
+ for prefix in ('', 'sub'):
+ with open(pjoin(nbdir, prefix, 'files', 'f1.txt'), 'w') as f:
+ f.write(prefix + '/files/f1')
+ with open(pjoin(nbdir, prefix, 'files', 'f2.txt'), 'w') as f:
+ f.write(prefix + '/files/f2')
+ with open(pjoin(nbdir, prefix, 'f2.txt'), 'w') as f:
+ f.write(prefix + '/f2')
+ with open(pjoin(nbdir, prefix, 'f3.txt'), 'w') as f:
+ f.write(prefix + '/f3')
+
+ url = url_path_join(base, 'notebooks', prefix, 'files', 'f1.txt')
+ r = requests.get(url)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, prefix + '/files/f1')
+
+ url = url_path_join(base, 'notebooks', prefix, 'files', 'f2.txt')
+ r = requests.get(url)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, prefix + '/files/f2')
+
+ url = url_path_join(base, 'notebooks', prefix, 'files', 'f3.txt')
+ r = requests.get(url)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, prefix + '/f3')
+
diff --git a/notebook/tests/test_hist.sqlite b/notebook/tests/test_hist.sqlite
new file mode 100644
index 0000000..49ca431
--- /dev/null
+++ b/notebook/tests/test_hist.sqlite
Binary files differ
diff --git a/notebook/tests/test_nbextensions.py b/notebook/tests/test_nbextensions.py
new file mode 100644
index 0000000..baf7f10
--- /dev/null
+++ b/notebook/tests/test_nbextensions.py
@@ -0,0 +1,496 @@
+# coding: utf-8
+"""Test installation of notebook extensions"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import glob
+import os
+import sys
+import tarfile
+import zipfile
+from io import BytesIO, StringIO
+from os.path import basename, join as pjoin
+from traitlets.tests.utils import check_help_all_output
+from unittest import TestCase
+
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch # py2
+
+import ipython_genutils.testing.decorators as dec
+from ipython_genutils import py3compat
+from ipython_genutils.tempdir import TemporaryDirectory
+from notebook import nbextensions
+from notebook.nbextensions import (install_nbextension, check_nbextension,
+ enable_nbextension, disable_nbextension,
+ install_nbextension_python, uninstall_nbextension_python,
+ enable_nbextension_python, disable_nbextension_python, _get_config_dir,
+ validate_nbextension, validate_nbextension_python
+)
+
+from traitlets.config.manager import BaseJSONConfigManager
+
+
+def touch(file, mtime=None):
+ """ensure a file exists, and set its modification time
+
+ returns the modification time of the file
+ """
+ open(file, 'a').close()
+ # set explicit mtime
+ if mtime:
+ atime = os.stat(file).st_atime
+ os.utime(file, (atime, mtime))
+ return os.stat(file).st_mtime
+
+
+def test_help_output():
+ check_help_all_output('notebook.nbextensions')
+ check_help_all_output('notebook.nbextensions', ['enable'])
+ check_help_all_output('notebook.nbextensions', ['disable'])
+ check_help_all_output('notebook.nbextensions', ['install'])
+ check_help_all_output('notebook.nbextensions', ['uninstall'])
+
+
+class TestInstallNBExtension(TestCase):
+
+ def tempdir(self):
+ td = TemporaryDirectory()
+ self.tempdirs.append(td)
+ return py3compat.cast_unicode(td.name)
+
+ def setUp(self):
+ self.tempdirs = []
+ self.src = self.tempdir()
+ self.files = files = [
+ pjoin(u'ƒile'),
+ pjoin(u'∂ir', u'ƒile1'),
+ pjoin(u'∂ir', u'∂ir2', u'ƒile2'),
+ ]
+ for file in files:
+ fullpath = os.path.join(self.src, file)
+ parent = os.path.dirname(fullpath)
+ if not os.path.exists(parent):
+ os.makedirs(parent)
+ touch(fullpath)
+
+ self.test_dir = self.tempdir()
+ self.data_dir = os.path.join(self.test_dir, 'data')
+ self.config_dir = os.path.join(self.test_dir, 'config')
+ self.system_data_dir = os.path.join(self.test_dir, 'system_data')
+ self.system_path = [self.system_data_dir]
+ self.system_nbext = os.path.join(self.system_data_dir, 'nbextensions')
+ self.patch_env = patch.dict('os.environ', {
+ 'JUPYTER_CONFIG_DIR': self.config_dir,
+ 'JUPYTER_DATA_DIR': self.data_dir,
+ })
+ self.patch_env.start()
+ self.patch_system_path = patch.object(nbextensions,
+ 'SYSTEM_JUPYTER_PATH', self.system_path)
+ self.patch_system_path.start()
+
+ def tearDown(self):
+ self.patch_env.stop()
+ self.patch_system_path.stop()
+
+ def assert_dir_exists(self, path):
+ if not os.path.exists(path):
+ do_exist = os.listdir(os.path.dirname(path))
+ self.fail(u"%s should exist (found %s)" % (path, do_exist))
+
+ def assert_not_dir_exists(self, path):
+ if os.path.exists(path):
+ self.fail(u"%s should not exist" % path)
+
+ def assert_installed(self, relative_path, user=False):
+ if user:
+ nbext = pjoin(self.data_dir, u'nbextensions')
+ else:
+ nbext = self.system_nbext
+ self.assert_dir_exists(
+ pjoin(nbext, relative_path)
+ )
+
+ def assert_not_installed(self, relative_path, user=False):
+ if user:
+ nbext = pjoin(self.data_dir, u'nbextensions')
+ else:
+ nbext = self.system_nbext
+ self.assert_not_dir_exists(
+ pjoin(nbext, relative_path)
+ )
+
+ def test_create_data_dir(self):
+ """install_nbextension when data_dir doesn't exist"""
+ with TemporaryDirectory() as td:
+ data_dir = os.path.join(td, self.data_dir)
+ with patch.dict('os.environ', {
+ 'JUPYTER_DATA_DIR': data_dir,
+ }):
+ install_nbextension(self.src, user=True)
+ self.assert_dir_exists(data_dir)
+ for file in self.files:
+ self.assert_installed(
+ pjoin(basename(self.src), file),
+ user=True,
+ )
+
+ def test_create_nbextensions_user(self):
+ with TemporaryDirectory() as td:
+ install_nbextension(self.src, user=True)
+ self.assert_installed(
+ pjoin(basename(self.src), u'ƒile'),
+ user=True
+ )
+
+ def test_create_nbextensions_system(self):
+ with TemporaryDirectory() as td:
+ self.system_nbext = pjoin(td, u'nbextensions')
+ with patch.object(nbextensions, 'SYSTEM_JUPYTER_PATH', [td]):
+ install_nbextension(self.src, user=False)
+ self.assert_installed(
+ pjoin(basename(self.src), u'ƒile'),
+ user=False
+ )
+
+ def test_single_file(self):
+ file = self.files[0]
+ install_nbextension(pjoin(self.src, file))
+ self.assert_installed(file)
+
+ def test_single_dir(self):
+ d = u'∂ir'
+ install_nbextension(pjoin(self.src, d))
+ self.assert_installed(self.files[-1])
+
+
+ def test_destination_file(self):
+ file = self.files[0]
+ install_nbextension(pjoin(self.src, file), destination = u'ƒiledest')
+ self.assert_installed(u'ƒiledest')
+
+ def test_destination_dir(self):
+ d = u'∂ir'
+ install_nbextension(pjoin(self.src, d), destination = u'ƒiledest2')
+ self.assert_installed(pjoin(u'ƒiledest2', u'∂ir2', u'ƒile2'))
+
+ def test_install_nbextension(self):
+ with self.assertRaises(TypeError):
+ install_nbextension(glob.glob(pjoin(self.src, '*')))
+
+ def test_overwrite_file(self):
+ with TemporaryDirectory() as d:
+ fname = u'ƒ.js'
+ src = pjoin(d, fname)
+ with open(src, 'w') as f:
+ f.write('first')
+ mtime = touch(src)
+ dest = pjoin(self.system_nbext, fname)
+ install_nbextension(src)
+ with open(src, 'w') as f:
+ f.write('overwrite')
+ mtime = touch(src, mtime - 100)
+ install_nbextension(src, overwrite=True)
+ with open(dest) as f:
+ self.assertEqual(f.read(), 'overwrite')
+
+ def test_overwrite_dir(self):
+ with TemporaryDirectory() as src:
+ base = basename(src)
+ fname = u'ƒ.js'
+ touch(pjoin(src, fname))
+ install_nbextension(src)
+ self.assert_installed(pjoin(base, fname))
+ os.remove(pjoin(src, fname))
+ fname2 = u'∂.js'
+ touch(pjoin(src, fname2))
+ install_nbextension(src, overwrite=True)
+ self.assert_installed(pjoin(base, fname2))
+ self.assert_not_installed(pjoin(base, fname))
+
+ def test_update_file(self):
+ with TemporaryDirectory() as d:
+ fname = u'ƒ.js'
+ src = pjoin(d, fname)
+ with open(src, 'w') as f:
+ f.write('first')
+ mtime = touch(src)
+ install_nbextension(src)
+ self.assert_installed(fname)
+ dest = pjoin(self.system_nbext, fname)
+ os.stat(dest).st_mtime
+ with open(src, 'w') as f:
+ f.write('overwrite')
+ touch(src, mtime + 10)
+ install_nbextension(src)
+ with open(dest) as f:
+ self.assertEqual(f.read(), 'overwrite')
+
+ def test_skip_old_file(self):
+ with TemporaryDirectory() as d:
+ fname = u'ƒ.js'
+ src = pjoin(d, fname)
+ mtime = touch(src)
+ install_nbextension(src)
+ self.assert_installed(fname)
+ dest = pjoin(self.system_nbext, fname)
+ old_mtime = os.stat(dest).st_mtime
+
+ mtime = touch(src, mtime - 100)
+ install_nbextension(src)
+ new_mtime = os.stat(dest).st_mtime
+ self.assertEqual(new_mtime, old_mtime)
+
+ def test_quiet(self):
+ stdout = StringIO()
+ stderr = StringIO()
+ with patch.object(sys, 'stdout', stdout), \
+ patch.object(sys, 'stderr', stderr):
+ install_nbextension(self.src)
+ self.assertEqual(stdout.getvalue(), '')
+ self.assertEqual(stderr.getvalue(), '')
+
+ def test_install_zip(self):
+ path = pjoin(self.src, "myjsext.zip")
+ with zipfile.ZipFile(path, 'w') as f:
+ f.writestr("a.js", b"b();")
+ f.writestr("foo/a.js", b"foo();")
+ install_nbextension(path)
+ self.assert_installed("a.js")
+ self.assert_installed(pjoin("foo", "a.js"))
+
+ def test_install_tar(self):
+ def _add_file(f, fname, buf):
+ info = tarfile.TarInfo(fname)
+ info.size = len(buf)
+ f.addfile(info, BytesIO(buf))
+
+ for i,ext in enumerate((".tar.gz", ".tgz", ".tar.bz2")):
+ path = pjoin(self.src, "myjsext" + ext)
+ with tarfile.open(path, 'w') as f:
+ _add_file(f, "b%i.js" % i, b"b();")
+ _add_file(f, "foo/b%i.js" % i, b"foo();")
+ install_nbextension(path)
+ self.assert_installed("b%i.js" % i)
+ self.assert_installed(pjoin("foo", "b%i.js" % i))
+
+ def test_install_url(self):
+ def fake_urlretrieve(url, dest):
+ touch(dest)
+ save_urlretrieve = nbextensions.urlretrieve
+ nbextensions.urlretrieve = fake_urlretrieve
+ try:
+ install_nbextension("http://example.com/path/to/foo.js")
+ self.assert_installed("foo.js")
+ install_nbextension("https://example.com/path/to/another/bar.js")
+ self.assert_installed("bar.js")
+ install_nbextension("https://example.com/path/to/another/bar.js",
+ destination = 'foobar.js')
+ self.assert_installed("foobar.js")
+ finally:
+ nbextensions.urlretrieve = save_urlretrieve
+
+ def test_check_nbextension(self):
+ with TemporaryDirectory() as d:
+ f = u'ƒ.js'
+ src = pjoin(d, f)
+ touch(src)
+ install_nbextension(src, user=True)
+
+ assert check_nbextension(f, user=True)
+ assert check_nbextension([f], user=True)
+ assert not check_nbextension([f, pjoin('dne', f)], user=True)
+
+ @dec.skip_win32
+ def test_install_symlink(self):
+ with TemporaryDirectory() as d:
+ f = u'ƒ.js'
+ src = pjoin(d, f)
+ touch(src)
+ install_nbextension(src, symlink=True)
+ dest = pjoin(self.system_nbext, f)
+ assert os.path.islink(dest)
+ link = os.readlink(dest)
+ self.assertEqual(link, src)
+
+ @dec.skip_win32
+ def test_overwrite_broken_symlink(self):
+ with TemporaryDirectory() as d:
+ f = u'ƒ.js'
+ f2 = u'ƒ2.js'
+ src = pjoin(d, f)
+ src2 = pjoin(d, f2)
+ touch(src)
+ install_nbextension(src, symlink=True)
+ os.rename(src, src2)
+ install_nbextension(src2, symlink=True, overwrite=True, destination=f)
+ dest = pjoin(self.system_nbext, f)
+ assert os.path.islink(dest)
+ link = os.readlink(dest)
+ self.assertEqual(link, src2)
+
+ @dec.skip_win32
+ def test_install_symlink_destination(self):
+ with TemporaryDirectory() as d:
+ f = u'ƒ.js'
+ flink = u'ƒlink.js'
+ src = pjoin(d, f)
+ touch(src)
+ install_nbextension(src, symlink=True, destination=flink)
+ dest = pjoin(self.system_nbext, flink)
+ assert os.path.islink(dest)
+ link = os.readlink(dest)
+ self.assertEqual(link, src)
+
+ def test_install_symlink_bad(self):
+ with self.assertRaises(ValueError):
+ install_nbextension("http://example.com/foo.js", symlink=True)
+
+ with TemporaryDirectory() as d:
+ zf = u'ƒ.zip'
+ zsrc = pjoin(d, zf)
+ with zipfile.ZipFile(zsrc, 'w') as z:
+ z.writestr("a.js", b"b();")
+
+ with self.assertRaises(ValueError):
+ install_nbextension(zsrc, symlink=True)
+
+ def test_install_destination_bad(self):
+ with TemporaryDirectory() as d:
+ zf = u'ƒ.zip'
+ zsrc = pjoin(d, zf)
+ with zipfile.ZipFile(zsrc, 'w') as z:
+ z.writestr("a.js", b"b();")
+
+ with self.assertRaises(ValueError):
+ install_nbextension(zsrc, destination='foo')
+
+ def test_nbextension_enable(self):
+ with TemporaryDirectory() as d:
+ f = u'ƒ.js'
+ src = pjoin(d, f)
+ touch(src)
+ install_nbextension(src, user=True)
+ enable_nbextension(section='notebook', require=u'ƒ')
+
+ config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
+ cm = BaseJSONConfigManager(config_dir=config_dir)
+ enabled = cm.get('notebook').get('load_extensions', {}).get(u'ƒ', False)
+ assert enabled
+
+ def test_nbextension_disable(self):
+ self.test_nbextension_enable()
+ disable_nbextension(section='notebook', require=u'ƒ')
+
+ config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
+ cm = BaseJSONConfigManager(config_dir=config_dir)
+ enabled = cm.get('notebook').get('load_extensions', {}).get(u'ƒ', False)
+ assert not enabled
+
+
+ def _mock_extension_spec_meta(self, section='notebook'):
+ return {
+ 'section': section,
+ 'src': 'mockextension',
+ 'dest': '_mockdestination',
+ 'require': '_mockdestination/index'
+ }
+
+ def _inject_mock_extension(self, section='notebook'):
+ outer_file = __file__
+
+ meta = self._mock_extension_spec_meta(section)
+
+ class mock():
+ __file__ = outer_file
+
+ @staticmethod
+ def _jupyter_nbextension_paths():
+ return [meta]
+
+ import sys
+ sys.modules['mockextension'] = mock
+
+ def test_nbextensionpy_files(self):
+ self._inject_mock_extension()
+ install_nbextension_python('mockextension')
+
+ assert check_nbextension('_mockdestination/index.js')
+ assert check_nbextension(['_mockdestination/index.js'])
+
+ def test_nbextensionpy_user_files(self):
+ self._inject_mock_extension()
+ install_nbextension_python('mockextension', user=True)
+
+ assert check_nbextension('_mockdestination/index.js', user=True)
+ assert check_nbextension(['_mockdestination/index.js'], user=True)
+
+ def test_nbextensionpy_uninstall_files(self):
+ self._inject_mock_extension()
+ install_nbextension_python('mockextension', user=True)
+ uninstall_nbextension_python('mockextension', user=True)
+
+ assert not check_nbextension('_mockdestination/index.js')
+ assert not check_nbextension(['_mockdestination/index.js'])
+
+ def test_nbextensionpy_enable(self):
+ self._inject_mock_extension('notebook')
+ install_nbextension_python('mockextension', user=True)
+ enable_nbextension_python('mockextension')
+
+ config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
+ cm = BaseJSONConfigManager(config_dir=config_dir)
+ enabled = cm.get('notebook').get('load_extensions', {}).get('_mockdestination/index', False)
+ assert enabled
+
+ def test_nbextensionpy_disable(self):
+ self._inject_mock_extension('notebook')
+ install_nbextension_python('mockextension', user=True)
+ enable_nbextension_python('mockextension')
+ disable_nbextension_python('mockextension', user=True)
+
+ config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
+ cm = BaseJSONConfigManager(config_dir=config_dir)
+ enabled = cm.get('notebook').get('load_extensions', {}).get('_mockdestination/index', False)
+ assert not enabled
+
+ def test_nbextensionpy_validate(self):
+ self._inject_mock_extension('notebook')
+
+ paths = install_nbextension_python('mockextension', user=True)
+ enable_nbextension_python('mockextension')
+
+ meta = self._mock_extension_spec_meta()
+ warnings = validate_nbextension_python(meta, paths[0])
+ self.assertEqual([], warnings, warnings)
+
+ def test_nbextensionpy_validate_bad(self):
+ # Break the metadata (correct file will still be copied)
+ self._inject_mock_extension('notebook')
+
+ paths = install_nbextension_python('mockextension', user=True)
+
+ enable_nbextension_python('mockextension')
+
+ meta = self._mock_extension_spec_meta()
+ meta.update(require="bad-require")
+
+ warnings = validate_nbextension_python(meta, paths[0])
+ self.assertNotEqual([], warnings, warnings)
+
+ def test_nbextension_validate(self):
+ # Break the metadata (correct file will still be copied)
+ self._inject_mock_extension('notebook')
+
+ install_nbextension_python('mockextension', user=True)
+ enable_nbextension_python('mockextension')
+
+ warnings = validate_nbextension("_mockdestination/index")
+ self.assertEqual([], warnings, warnings)
+
+ def test_nbextension_validate_bad(self):
+ warnings = validate_nbextension("this-doesn't-exist")
+ self.assertNotEqual([], warnings, warnings)
+
diff --git a/notebook/tests/test_notebookapp.py b/notebook/tests/test_notebookapp.py
new file mode 100644
index 0000000..270ec1b
--- /dev/null
+++ b/notebook/tests/test_notebookapp.py
@@ -0,0 +1,119 @@
+"""Test NotebookApp"""
+
+
+import logging
+import os
+import re
+from tempfile import NamedTemporaryFile
+
+import nose.tools as nt
+
+from traitlets.tests.utils import check_help_all_output
+
+from jupyter_core.application import NoStart
+from ipython_genutils.tempdir import TemporaryDirectory
+from traitlets import TraitError
+from notebook import notebookapp, __version__
+NotebookApp = notebookapp.NotebookApp
+
+
+def test_help_output():
+ """ipython notebook --help-all works"""
+ check_help_all_output('notebook')
+
+def test_server_info_file():
+ td = TemporaryDirectory()
+ nbapp = NotebookApp(runtime_dir=td.name, log=logging.getLogger())
+ def get_servers():
+ return list(notebookapp.list_running_servers(nbapp.runtime_dir))
+ nbapp.initialize(argv=[])
+ nbapp.write_server_info_file()
+ servers = get_servers()
+ nt.assert_equal(len(servers), 1)
+ nt.assert_equal(servers[0]['port'], nbapp.port)
+ nt.assert_equal(servers[0]['url'], nbapp.connection_url)
+ nbapp.remove_server_info_file()
+ nt.assert_equal(get_servers(), [])
+
+ # The ENOENT error should be silenced.
+ nbapp.remove_server_info_file()
+
+def test_nb_dir():
+ with TemporaryDirectory() as td:
+ app = NotebookApp(notebook_dir=td)
+ nt.assert_equal(app.notebook_dir, td)
+
+def test_no_create_nb_dir():
+ with TemporaryDirectory() as td:
+ nbdir = os.path.join(td, 'notebooks')
+ app = NotebookApp()
+ with nt.assert_raises(TraitError):
+ app.notebook_dir = nbdir
+
+def test_missing_nb_dir():
+ with TemporaryDirectory() as td:
+ nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
+ app = NotebookApp()
+ with nt.assert_raises(TraitError):
+ app.notebook_dir = nbdir
+
+def test_invalid_nb_dir():
+ with NamedTemporaryFile() as tf:
+ app = NotebookApp()
+ with nt.assert_raises(TraitError):
+ app.notebook_dir = tf
+
+def test_nb_dir_with_slash():
+ with TemporaryDirectory(suffix="_slash/") as td:
+ app = NotebookApp(notebook_dir=td)
+ nt.assert_false(app.notebook_dir.endswith("/"))
+
+def test_nb_dir_root():
+ root = os.path.abspath(os.sep) # gets the right value on Windows, Posix
+ app = NotebookApp(notebook_dir=root)
+ nt.assert_equal(app.notebook_dir, root)
+
+def test_generate_config():
+ with TemporaryDirectory() as td:
+ app = NotebookApp(config_dir=td)
+ app.initialize(['--generate-config'])
+ with nt.assert_raises(NoStart):
+ app.start()
+ assert os.path.exists(os.path.join(td, 'jupyter_notebook_config.py'))
+
+
+#test if the version testin function works
+def test_pep440_version():
+
+ for version in [
+ '4.1.0.b1',
+ '4.1.b1',
+ '4.2',
+ 'X.y.z',
+ '1.2.3.dev1.post2',
+ ]:
+ def loc():
+ with nt.assert_raises(ValueError):
+ raise_on_bad_version(version)
+ yield loc
+
+ for version in [
+ '4.1.1',
+ '4.2.1b3',
+ ]:
+
+ yield (raise_on_bad_version, version)
+
+
+
+pep440re = re.compile('^(\d+)\.(\d+)\.(\d+((a|b|rc)\d+)?)(\.post\d+)?(\.dev\d*)?$')
+
+def raise_on_bad_version(version):
+ if not pep440re.match(version):
+ raise ValueError("Versions String does apparently not match Pep 440 specification, "
+ "which might lead to sdist and wheel being seen as 2 different release. "
+ "E.g: do not use dots for beta/alpha/rc markers.")
+
+
+def test_current_version():
+ raise_on_bad_version(__version__)
diff --git a/notebook/tests/test_paths.py b/notebook/tests/test_paths.py
new file mode 100644
index 0000000..0a2a334
--- /dev/null
+++ b/notebook/tests/test_paths.py
@@ -0,0 +1,40 @@
+
+import re
+import nose.tools as nt
+
+from notebook.base.handlers import path_regex
+
+try: # py3
+ assert_regex = nt.assert_regex
+ assert_not_regex = nt.assert_not_regex
+except AttributeError: # py2
+ assert_regex = nt.assert_regexp_matches
+ assert_not_regex = nt.assert_not_regexp_matches
+
+
+# build regexps that tornado uses:
+path_pat = re.compile('^' + '/x%s' % path_regex + '$')
+
+def test_path_regex():
+ for path in (
+ '/x',
+ '/x/',
+ '/x/foo',
+ '/x/foo.ipynb',
+ '/x/foo/bar',
+ '/x/foo/bar.txt',
+ ):
+ assert_regex(path, path_pat)
+
+def test_path_regex_bad():
+ for path in (
+ '/xfoo',
+ '/xfoo/',
+ '/xfoo/bar',
+ '/xfoo/bar/',
+ '/x/foo/bar/',
+ '/x//foo',
+ '/y',
+ '/y/x/foo',
+ ):
+ assert_not_regex(path, path_pat)
diff --git a/notebook/tests/test_serialize.py b/notebook/tests/test_serialize.py
new file mode 100644
index 0000000..600928b
--- /dev/null
+++ b/notebook/tests/test_serialize.py
@@ -0,0 +1,26 @@
+"""Test serialize/deserialize messages with buffers"""
+
+import os
+
+import nose.tools as nt
+
+from jupyter_client.session import Session
+from ..base.zmqhandlers import (
+ serialize_binary_message,
+ deserialize_binary_message,
+)
+
+def test_serialize_binary():
+ s = Session()
+ msg = s.msg('data_pub', content={'a': 'b'})
+ msg['buffers'] = [ memoryview(os.urandom(3)) for i in range(3) ]
+ bmsg = serialize_binary_message(msg)
+ nt.assert_is_instance(bmsg, bytes)
+
+def test_deserialize_binary():
+ s = Session()
+ msg = s.msg('data_pub', content={'a': 'b'})
+ msg['buffers'] = [ memoryview(os.urandom(2)) for i in range(3) ]
+ bmsg = serialize_binary_message(msg)
+ msg2 = deserialize_binary_message(bmsg)
+ nt.assert_equal(msg2, msg)
diff --git a/notebook/tests/test_serverextensions.py b/notebook/tests/test_serverextensions.py
new file mode 100644
index 0000000..4c5a68c
--- /dev/null
+++ b/notebook/tests/test_serverextensions.py
@@ -0,0 +1,89 @@
+import os
+from unittest import TestCase
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch # py2
+
+from ipython_genutils.tempdir import TemporaryDirectory
+from ipython_genutils import py3compat
+
+from traitlets.config.manager import BaseJSONConfigManager
+from traitlets.tests.utils import check_help_all_output
+
+from notebook.serverextensions import toggle_serverextension_python
+from notebook import nbextensions
+from notebook.nbextensions import _get_config_dir
+
+
+def test_help_output():
+ check_help_all_output('notebook.serverextensions')
+ check_help_all_output('notebook.serverextensions', ['enable'])
+ check_help_all_output('notebook.serverextensions', ['disable'])
+ check_help_all_output('notebook.serverextensions', ['install'])
+ check_help_all_output('notebook.serverextensions', ['uninstall'])
+
+
+class TestInstallServerExtension(TestCase):
+
+ def tempdir(self):
+ td = TemporaryDirectory()
+ self.tempdirs.append(td)
+ return py3compat.cast_unicode(td.name)
+
+ def setUp(self):
+ self.tempdirs = []
+
+ self.test_dir = self.tempdir()
+ self.data_dir = os.path.join(self.test_dir, 'data')
+ self.config_dir = os.path.join(self.test_dir, 'config')
+ self.system_data_dir = os.path.join(self.test_dir, 'system_data')
+ self.system_path = [self.system_data_dir]
+
+ self.patch_env = patch.dict('os.environ', {
+ 'JUPYTER_CONFIG_DIR': self.config_dir,
+ 'JUPYTER_DATA_DIR': self.data_dir,
+ })
+ self.patch_env.start()
+ self.patch_system_path = patch.object(nbextensions,
+ 'SYSTEM_JUPYTER_PATH', self.system_path)
+ self.patch_system_path.start()
+
+ def tearDown(self):
+ self.patch_env.stop()
+ self.patch_system_path.stop()
+
+ def _inject_mock_extension(self):
+ outer_file = __file__
+
+ class mock():
+ __file__ = outer_file
+
+ @staticmethod
+ def _jupyter_server_extension_paths():
+ return [{
+ 'module': '_mockdestination/index'
+ }]
+
+ import sys
+ sys.modules['mockextension'] = mock
+
+ def _get_config(self, user=True):
+ cm = BaseJSONConfigManager(config_dir=_get_config_dir(user))
+ data = cm.get("jupyter_notebook_config")
+ return data.get("NotebookApp", {}).get("nbserver_extensions", {})
+
+ def test_enable(self):
+ self._inject_mock_extension()
+ toggle_serverextension_python('mockextension', True)
+
+ config = self._get_config()
+ assert config['mockextension']
+
+ def test_disable(self):
+ self._inject_mock_extension()
+ toggle_serverextension_python('mockextension', True)
+ toggle_serverextension_python('mockextension', False)
+
+ config = self._get_config()
+ assert not config['mockextension']
diff --git a/notebook/tests/test_utils.py b/notebook/tests/test_utils.py
new file mode 100644
index 0000000..6952e04
--- /dev/null
+++ b/notebook/tests/test_utils.py
@@ -0,0 +1,81 @@
+"""Test HTML utils"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import ctypes
+import os
+
+import nose.tools as nt
+
+from traitlets.tests.utils import check_help_all_output
+from notebook.utils import url_escape, url_unescape, is_hidden
+from ipython_genutils.py3compat import cast_unicode
+from ipython_genutils.tempdir import TemporaryDirectory
+from ipython_genutils.testing.decorators import skip_if_not_win32
+
+
+def test_help_output():
+ """jupyter notebook --help-all works"""
+ # FIXME: will be notebook
+ check_help_all_output('notebook')
+
+
+def test_url_escape():
+
+ # changes path or notebook name with special characters to url encoding
+ # these tests specifically encode paths with spaces
+ path = url_escape('/this is a test/for spaces/')
+ nt.assert_equal(path, '/this%20is%20a%20test/for%20spaces/')
+
+ path = url_escape('notebook with space.ipynb')
+ nt.assert_equal(path, 'notebook%20with%20space.ipynb')
+
+ path = url_escape('/path with a/notebook and space.ipynb')
+ nt.assert_equal(path, '/path%20with%20a/notebook%20and%20space.ipynb')
+
+ path = url_escape('/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
+ nt.assert_equal(path,
+ '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
+
+def test_url_unescape():
+
+ # decodes a url string to a plain string
+ # these tests decode paths with spaces
+ path = url_unescape('/this%20is%20a%20test/for%20spaces/')
+ nt.assert_equal(path, '/this is a test/for spaces/')
+
+ path = url_unescape('notebook%20with%20space.ipynb')
+ nt.assert_equal(path, 'notebook with space.ipynb')
+
+ path = url_unescape('/path%20with%20a/notebook%20and%20space.ipynb')
+ nt.assert_equal(path, '/path with a/notebook and space.ipynb')
+
+ path = url_unescape(
+ '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
+ nt.assert_equal(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
+
+def test_is_hidden():
+ with TemporaryDirectory() as root:
+ subdir1 = os.path.join(root, 'subdir')
+ os.makedirs(subdir1)
+ nt.assert_equal(is_hidden(subdir1, root), False)
+ subdir2 = os.path.join(root, '.subdir2')
+ os.makedirs(subdir2)
+ nt.assert_equal(is_hidden(subdir2, root), True)
+ subdir34 = os.path.join(root, 'subdir3', '.subdir4')
+ os.makedirs(subdir34)
+ nt.assert_equal(is_hidden(subdir34, root), True)
+ nt.assert_equal(is_hidden(subdir34), True)
+
+@skip_if_not_win32
+def test_is_hidden_win32():
+ with TemporaryDirectory() as root:
+ root = cast_unicode(root)
+ subdir1 = os.path.join(root, u'subdir')
+ os.makedirs(subdir1)
+ assert not is_hidden(subdir1, root)
+ r = ctypes.windll.kernel32.SetFileAttributesW(subdir1, 0x02)
+ print(r)
+ assert is_hidden(subdir1, root)
+
diff --git a/notebook/tests/tree/dashboard_nav.js b/notebook/tests/tree/dashboard_nav.js
new file mode 100644
index 0000000..75ef161
--- /dev/null
+++ b/notebook/tests/tree/dashboard_nav.js
@@ -0,0 +1,47 @@
+
+
+casper.get_list_items = function () {
+ return this.evaluate(function () {
+ return $.makeArray($('.item_link').map(function () {
+ return {
+ link: $(this).attr('href'),
+ label: $(this).find('.item_name').text()
+ };
+ }));
+ });
+};
+
+casper.test_items = function (origin, prefix, visited) {
+ visited = visited || {};
+ casper.then(function () {
+ var items = casper.get_list_items();
+ var tree_link = RegExp('^' + (prefix + 'tree/').replace(/\//g, '\\/'));
+ casper.each(items, function (self, item) {
+ if (item.link.match(tree_link)) {
+ var followed_url = item.link;
+ if (!visited[followed_url]) {
+ visited[followed_url] = true;
+ casper.thenOpen(origin + followed_url, function () {
+ this.waitFor(this.page_loaded);
+ casper.wait_for_dashboard();
+ // getCurrentUrl is with host, and url-decoded,
+ // but item.link is without host, and url-encoded
+ var expected = origin + decodeURIComponent(item.link);
+ this.test.assertEquals(this.getCurrentUrl(), expected, 'Testing dashboard link: ' + expected);
+ casper.test_items(origin, prefix, visited);
+ this.back();
+ });
+ }
+ }
+ });
+ });
+};
+
+casper.dashboard_test(function () {
+ var baseUrl = this.get_notebook_server();
+ m = /(https?:\/\/[^\/]+)(.*)/.exec(baseUrl);
+ origin = m[1];
+ prefix = m[2];
+ casper.test_items(origin, prefix);
+});
+
diff --git a/notebook/tests/util.js b/notebook/tests/util.js
new file mode 100644
index 0000000..8a82c9a
--- /dev/null
+++ b/notebook/tests/util.js
@@ -0,0 +1,864 @@
+//
+// Utility functions for the HTML notebook's CasperJS tests.
+//
+casper.get_notebook_server = function () {
+ // Get the URL of a notebook server on which to run tests.
+ var port = casper.cli.get("port");
+ port = (typeof port === 'undefined') ? '8888' : port;
+ return casper.cli.get("url") || ('http://127.0.0.1:' + port);
+};
+
+casper.open_new_notebook = function () {
+ // Create and open a new notebook.
+ var baseUrl = this.get_notebook_server();
+ this.start(baseUrl);
+ this.waitFor(this.page_loaded);
+ this.waitForSelector('#kernel-python2 a, #kernel-python3 a');
+ this.thenClick('#kernel-python2 a, #kernel-python3 a');
+
+ this.waitForPopup('');
+
+ this.withPopup('', function () {this.waitForSelector('.CodeMirror-code');});
+ this.then(function () {
+ this.open(this.popups[0].url);
+ });
+ this.waitFor(this.page_loaded);
+
+ // Hook the log and error methods of the console, forcing them to
+ // serialize their arguments before printing. This allows the
+ // Objects to cross into the phantom/slimer regime for display.
+ this.thenEvaluate(function(){
+ var serialize_arguments = function(f, context) {
+ return function() {
+ var pretty_arguments = [];
+ for (var i = 0; i < arguments.length; i++) {
+ var value = arguments[i];
+ if (value instanceof Object) {
+ var name = value.name || 'Object';
+ // Print a JSON string representation of the object.
+ // If we don't do this, [Object object] gets printed
+ // by casper, which is useless. The long regular
+ // expression reduces the verbosity of the JSON.
+ pretty_arguments.push(name + ' {' + JSON.stringify(value, null, ' ')
+ .replace(/(\s+)?({)?(\s+)?(}(\s+)?,?)?(\s+)?(\s+)?\n/g, '\n')
+ .replace(/\n(\s+)?\n/g, '\n'));
+ } else {
+ pretty_arguments.push(value);
+ }
+ }
+ f.apply(context, pretty_arguments);
+ };
+ };
+ console.log = serialize_arguments(console.log, console);
+ console.error = serialize_arguments(console.error, console);
+ });
+
+ // Make sure the kernel has started
+ this.waitFor(this.kernel_running);
+ // track the IPython busy/idle state
+ this.thenEvaluate(function () {
+ require(['base/js/namespace', 'base/js/events'], function (IPython, events) {
+
+ events.on('kernel_idle.Kernel',function () {
+ IPython._status = 'idle';
+ });
+ events.on('kernel_busy.Kernel',function () {
+ IPython._status = 'busy';
+ });
+ });
+ });
+
+ // Because of the asynchronous nature of SlimerJS (Gecko), we need to make
+ // sure the notebook has actually been loaded into the IPython namespace
+ // before running any tests.
+ this.waitFor(function() {
+ return this.evaluate(function () {
+ return IPython.notebook;
+ });
+ });
+};
+
+casper.page_loaded = function() {
+ // Return whether or not the page has been loaded.
+ return this.evaluate(function() {
+ return typeof IPython !== "undefined" &&
+ IPython.page !== undefined;
+ });
+};
+
+casper.kernel_running = function() {
+ // Return whether or not the kernel is running.
+ return this.evaluate(function() {
+ return IPython &&
+ IPython.notebook &&
+ IPython.notebook.kernel &&
+ IPython.notebook.kernel.is_connected();
+ });
+};
+
+casper.kernel_disconnected = function() {
+ return this.evaluate(function() {
+ return IPython.notebook.kernel.is_fully_disconnected();
+ });
+};
+
+casper.wait_for_kernel_ready = function () {
+ this.waitFor(this.kernel_running);
+ this.thenEvaluate(function () {
+ IPython._kernel_ready = false;
+ IPython.notebook.kernel.kernel_info(
+ function () {
+ IPython._kernel_ready = true;
+ });
+ });
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._kernel_ready;
+ });
+ });
+};
+
+casper.shutdown_current_kernel = function () {
+ // Shut down the current notebook's kernel.
+ this.thenEvaluate(function() {
+ IPython.notebook.session.delete();
+ });
+ // We close the page right after this so we need to give it time to complete.
+ this.wait(1000);
+};
+
+casper.delete_current_notebook = function () {
+ // Delete created notebook.
+
+ // For some unknown reason, this doesn't work?!?
+ this.thenEvaluate(function() {
+ IPython.notebook.delete();
+ });
+};
+
+casper.wait_for_busy = function () {
+ // Waits for the notebook to enter a busy state.
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._status == 'busy';
+ });
+ });
+};
+
+casper.wait_for_idle = function () {
+ // Waits for the notebook to idle.
+ this.waitFor(function () {
+ return this.evaluate(function () {
+ return IPython._status == 'idle';
+ });
+ });
+};
+
+casper.wait_for_output = function (cell_num, out_num) {
+ // wait for the nth output in a given cell
+ this.wait_for_idle();
+ out_num = out_num || 0;
+ this.then(function() {
+ this.waitFor(function (c, o) {
+ return this.evaluate(function get_output(c, o) {
+ var cell = IPython.notebook.get_cell(c);
+ return cell.output_area.outputs.length > o;
+ },
+ // pass parameter from the test suite js to the browser code js
+ {c : cell_num, o : out_num});
+ },
+ function then() { },
+ function timeout() {
+ this.echo("wait_for_output timed out on cell "+cell_num+", waiting for "+out_num+" outputs .");
+ var pn = this.evaluate(function get_prompt(c) {
+ return (IPython.notebook.get_cell(c)|| {'input_prompt_number':'no cell'}).input_prompt_number;
+ });
+ this.echo("cell prompt was :'"+pn+"'.");
+ });
+ });
+};
+
+casper.wait_for_widget = function (widget_info) {
+ // wait for a widget msg que to reach 0
+ //
+ // Parameters
+ // ----------
+ // widget_info : object
+ // Object which contains info related to the widget. The model_id property
+ // is used to identify the widget.
+
+ // Clear the results of a previous query, if they exist. Make sure a
+ // dictionary exists to store the async results in.
+ this.thenEvaluate(function(model_id) {
+ if (window.pending_msgs === undefined) {
+ window.pending_msgs = {};
+ } else {
+ window.pending_msgs[model_id] = -1;
+ }
+ }, {model_id: widget_info.model_id});
+
+ // Wait for the pending messages to be 0.
+ this.waitFor(function () {
+ var pending = this.evaluate(function (model_id) {
+
+ // Get the model. Once the model is had, store it's pending_msgs
+ // count in the window's dictionary.
+ IPython.notebook.kernel.widget_manager.get_model(model_id)
+ .then(function(model) {
+ window.pending_msgs[model_id] = model.pending_msgs;
+ });
+
+ // Return the pending_msgs result.
+ return window.pending_msgs[model_id];
+ }, {model_id: widget_info.model_id});
+
+ if (pending === 0) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+};
+
+casper.cell_has_outputs = function (cell_num) {
+ var result = casper.evaluate(function (c) {
+ var cell = IPython.notebook.get_cell(c);
+ return cell.output_area.outputs.length;
+ },
+ {c : cell_num});
+ return result > 0;
+};
+
+casper.get_output_cell = function (cell_num, out_num, message) {
+ messsge = message+': ' ||'no category :'
+ // return an output of a given cell
+ out_num = out_num || 0;
+ var result = casper.evaluate(function (c, o) {
+ var cell = IPython.notebook.get_cell(c);
+ return cell.output_area.outputs[o];
+ },
+ {c : cell_num, o : out_num});
+ if (!result) {
+ var num_outputs = casper.evaluate(function (c) {
+ var cell = IPython.notebook.get_cell(c);
+ return cell.output_area.outputs.length;
+ },
+ {c : cell_num});
+ this.test.assertTrue(false,
+ message+"Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
+ );
+ } else {
+ return result;
+ }
+};
+
+casper.get_cells_length = function () {
+ // return the number of cells in the notebook
+ var result = casper.evaluate(function () {
+ return IPython.notebook.get_cells().length;
+ });
+ return result;
+};
+
+casper.set_cell_text = function(index, text){
+ // Set the text content of a cell.
+ this.evaluate(function (index, text) {
+ var cell = IPython.notebook.get_cell(index);
+ cell.set_text(text);
+ }, index, text);
+};
+
+casper.get_cell_text = function(index){
+ // Get the text content of a cell.
+ return this.evaluate(function (index) {
+ var cell = IPython.notebook.get_cell(index);
+ return cell.get_text();
+ }, index);
+};
+
+casper.insert_cell_at_bottom = function(cell_type){
+ // Inserts a cell at the bottom of the notebook
+ // Returns the new cell's index.
+ return this.evaluate(function (cell_type) {
+ var cell = IPython.notebook.insert_cell_at_bottom(cell_type);
+ return IPython.notebook.find_cell_index(cell);
+ }, cell_type);
+};
+
+casper.append_cell = function(text, cell_type) {
+ // Insert a cell at the bottom of the notebook and set the cells text.
+ // Returns the new cell's index.
+ var index = this.insert_cell_at_bottom(cell_type);
+ if (text !== undefined) {
+ this.set_cell_text(index, text);
+ }
+ return index;
+};
+
+casper.execute_cell = function(index, expect_failure){
+ // Asynchronously executes a cell by index.
+ // Returns the cell's index.
+
+ if (expect_failure === undefined) expect_failure = false;
+ var that = this;
+ this.then(function(){
+ that.evaluate(function (index) {
+ var cell = IPython.notebook.get_cell(index);
+ cell.execute();
+ }, index);
+ });
+ this.wait_for_idle();
+
+ this.then(function () {
+ var error = that.evaluate(function (index) {
+ var cell = IPython.notebook.get_cell(index);
+ var outputs = cell.output_area.outputs;
+ for (var i = 0; i < outputs.length; i++) {
+ if (outputs[i].output_type == 'error') {
+ return outputs[i];
+ }
+ }
+ return false;
+ }, index);
+ if (error === null) {
+ this.test.fail("Failed to check for error output");
+ }
+ if (expect_failure && error === false) {
+ this.test.fail("Expected error while running cell");
+ } else if (!expect_failure && error !== false) {
+ this.test.fail("Error running cell:\n" + error.traceback.join('\n'));
+ }
+ });
+ return index;
+};
+
+casper.execute_cell_then = function(index, then_callback, expect_failure) {
+ // Synchronously executes a cell by index.
+ // Optionally accepts a then_callback parameter. then_callback will get called
+ // when the cell has finished executing.
+ // Returns the cell's index.
+ var return_val = this.execute_cell(index, expect_failure);
+
+ this.wait_for_idle();
+
+ var that = this;
+ this.then(function(){
+ if (then_callback!==undefined) {
+ then_callback.apply(that, [index]);
+ }
+ });
+
+ return return_val;
+};
+
+casper.append_cell_execute_then = function(text, then_callback, expect_failure) {
+ // Append a code cell and execute it, optionally calling a then_callback
+ var c = this.append_cell(text);
+ return this.execute_cell_then(c, then_callback, expect_failure);
+};
+
+casper.assert_output_equals = function(text, output_text, message) {
+ // Append a code cell with the text, then assert the output is equal to output_text
+ this.append_cell_execute_then(text, function(index) {
+ this.test.assertEquals(this.get_output_cell(index).text.trim(), output_text, message);
+ });
+};
+
+casper.wait_for_element = function(index, selector){
+ // Utility function that allows us to easily wait for an element
+ // within a cell. Uses JQuery selector to look for the element.
+ var that = this;
+ this.waitFor(function() {
+ return that.cell_element_exists(index, selector);
+ });
+};
+
+casper.cell_element_exists = function(index, selector){
+ // Utility function that allows us to easily check if an element exists
+ // within a cell. Uses JQuery selector to look for the element.
+ return casper.evaluate(function (index, selector) {
+ var $cell = IPython.notebook.get_cell(index).element;
+ return $cell.find(selector).length > 0;
+ }, index, selector);
+};
+
+casper.cell_element_function = function(index, selector, function_name, function_args){
+ // Utility function that allows us to execute a jQuery function on an
+ // element within a cell.
+ return casper.evaluate(function (index, selector, function_name, function_args) {
+ var $cell = IPython.notebook.get_cell(index).element;
+ var $el = $cell.find(selector);
+ return $el[function_name].apply($el, function_args);
+ }, index, selector, function_name, function_args);
+};
+
+casper.validate_notebook_state = function(message, mode, cell_index) {
+ // Validate the entire dual mode state of the notebook. Make sure no more than
+ // one cell is selected, focused, in edit mode, etc...
+ // General tests.
+ this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(),
+ message + '; keyboard and notebook modes match');
+ // Is the selected cell the only cell that is selected?
+ if (cell_index!==undefined) {
+ this.test.assert(this.is_only_cell_selected(cell_index),
+ message + '; expecting cell ' + cell_index + ' to be the only cell selected. Got selected cell(s):'+
+ (function(){
+ return casper.evaluate(function(){
+ return IPython.notebook.get_selected_cells_indices();
+ })
+ })()
+ );
+ }
+
+ // Mode specific tests.
+ if (mode==='command') {
+ // Are the notebook and keyboard manager in command mode?
+ this.test.assertEquals(this.get_keyboard_mode(), 'command',
+ message + '; in command mode');
+ // Make sure there isn't a single cell in edit mode.
+ this.test.assert(this.is_only_cell_edit(null),
+ message + '; all cells in command mode');
+ this.test.assert(this.is_cell_editor_focused(null),
+ message + '; no cell editors are focused while in command mode');
+
+ } else if (mode==='edit') {
+ // Are the notebook and keyboard manager in edit mode?
+ this.test.assertEquals(this.get_keyboard_mode(), 'edit',
+ message + '; in edit mode');
+ if (cell_index!==undefined) {
+ // Is the specified cell the only cell in edit mode?
+ this.test.assert(this.is_only_cell_edit(cell_index),
+ message + '; cell ' + cell_index + ' is the only cell in edit mode '+ this.cells_modes());
+ // Is the specified cell the only cell with a focused code mirror?
+ this.test.assert(this.is_cell_editor_focused(cell_index),
+ message + '; cell ' + cell_index + '\'s editor is appropriately focused');
+ }
+
+ } else {
+ this.test.assert(false, message + '; ' + mode + ' is an unknown mode');
+ }
+};
+
+casper.select_cell = function(index, moveanchor) {
+ // Select a cell in the notebook.
+ this.evaluate(function (i, moveanchor) {
+ IPython.notebook.select(i, moveanchor);
+ }, {i: index, moveanchor: moveanchor});
+};
+
+casper.click_cell_editor = function(index) {
+ // Emulate a click on a cell's editor.
+
+ // Code Mirror does not play nicely with emulated brower events.
+ // Instead of trying to emulate a click, here we run code similar to
+ // the code used in Code Mirror that handles the mousedown event on a
+ // region of codemirror that the user can focus.
+ this.evaluate(function (i) {
+ var cm = IPython.notebook.get_cell(i).code_mirror;
+ if (cm.options.readOnly != "nocursor" && (document.activeElement != cm.display.input)){
+ cm.display.input.focus();
+ }
+ }, {i: index});
+};
+
+casper.set_cell_editor_cursor = function(index, line_index, char_index) {
+ // Set the Code Mirror instance cursor's location.
+ this.evaluate(function (i, l, c) {
+ IPython.notebook.get_cell(i).code_mirror.setCursor(l, c);
+ }, {i: index, l: line_index, c: char_index});
+};
+
+casper.focus_notebook = function() {
+ // Focus the notebook div.
+ this.evaluate(function (){
+ $('#notebook').focus();
+ }, {});
+};
+
+casper.trigger_keydown = function() {
+ // Emulate a keydown in the notebook.
+ for (var i = 0; i < arguments.length; i++) {
+ this.evaluate(function (k) {
+ var element = $(document);
+ var event = IPython.keyboard.shortcut_to_event(k, 'keydown');
+ element.trigger(event);
+ }, {k: arguments[i]});
+ }
+};
+
+casper.get_keyboard_mode = function() {
+ // Get the mode of the keyboard manager.
+ return this.evaluate(function() {
+ return IPython.keyboard_manager.mode;
+ }, {});
+};
+
+casper.get_notebook_mode = function() {
+ // Get the mode of the notebook.
+ return this.evaluate(function() {
+ return IPython.notebook.mode;
+ }, {});
+};
+
+casper.get_cell = function(index) {
+ // Get a single cell.
+ //
+ // Note: Handles to DOM elements stored in the cell will be useless once in
+ // CasperJS context.
+ return this.evaluate(function(i) {
+ var cell = IPython.notebook.get_cell(i);
+ if (cell) {
+ return cell;
+ }
+ return null;
+ }, {i : index});
+};
+
+casper.is_cell_editor_focused = function(index) {
+ // Make sure a cell's editor is the only editor focused on the page.
+ return this.evaluate(function(i) {
+ var focused_textarea = $('#notebook .CodeMirror-focused textarea');
+ if (focused_textarea.length > 1) { throw 'More than one Code Mirror editor is focused at once!'; }
+ if (i === null) {
+ return focused_textarea.length === 0;
+ } else {
+ var cell = IPython.notebook.get_cell(i);
+ if (cell) {
+ return cell.code_mirror.getInputField() == focused_textarea[0];
+ }
+ }
+ return false;
+ }, {i : index});
+};
+
+casper.is_only_cell_selected = function(index) {
+ // Check if a cell is the only cell selected.
+ // Pass null as the index to check if no cells are selected.
+ return this.is_only_cell_on(index, 'selected', 'unselected');
+};
+
+casper.is_only_cell_edit = function(index) {
+ // Check if a cell is the only cell in edit mode.
+ // Pass null as the index to check if all of the cells are in command mode.
+ var cells_length = this.get_cells_length();
+ for (var j = 0; j < cells_length; j++) {
+ if (j === index) {
+ if (!this.cell_mode_is(j, 'edit')) {
+ return false;
+ }
+ } else {
+ if (this.cell_mode_is(j, 'edit')) {
+ return false;
+ }
+ }
+ }
+ return true;
+};
+
+casper.is_only_cell_on = function(i, on_class, off_class) {
+ // Check if a cell is the only cell with the `on_class` DOM class applied to it.
+ // All of the other cells are checked for the `off_class` DOM class.
+ // Pass null as the index to check if all of the cells have the `off_class`.
+ var cells_length = this.get_cells_length();
+ for (var j = 0; j < cells_length; j++) {
+ if (j === i) {
+ if (this.cell_has_class(j, off_class) || !this.cell_has_class(j, on_class)) {
+ return false;
+ }
+ } else {
+ if (!this.cell_has_class(j, off_class) || this.cell_has_class(j, on_class)) {
+ return false;
+ }
+ }
+ }
+ return true;
+};
+
+casper.cells_modes = function(){
+ return this.evaluate(function(){
+ return IPython.notebook.get_cells().map(function(x,c){return x.mode})
+ }, {});
+};
+
+casper.cell_mode_is = function(index, mode) {
+ // Check if a cell is in a specific mode
+ return this.evaluate(function(i, m) {
+ var cell = IPython.notebook.get_cell(i);
+ if (cell) {
+ return cell.mode === m;
+ }
+ return false;
+ }, {i : index, m: mode});
+};
+
+
+casper.cell_has_class = function(index, classes) {
+ // Check if a cell has a class.
+ return this.evaluate(function(i, c) {
+ var cell = IPython.notebook.get_cell(i);
+ if (cell) {
+ return cell.element.hasClass(c);
+ }
+ return false;
+ }, {i : index, c: classes});
+};
+
+casper.is_cell_rendered = function (index) {
+ return this.evaluate(function(i) {
+ return !!IPython.notebook.get_cell(i).rendered;
+ }, {i:index});
+};
+
+casper.assert_colors_equal = function (hex_color, local_color, msg) {
+ // Tests to see if two colors are equal.
+ //
+ // Parameters
+ // hex_color: string
+ // Hexadecimal color code, with or without preceeding hash character.
+ // local_color: string
+ // Local color representation. Can either be hexadecimal (default for
+ // phantom) or rgb (default for slimer).
+
+ // Remove parentheses, hashes, semi-colons, and space characters.
+ hex_color = hex_color.replace(/[\(\); #]/, '');
+ local_color = local_color.replace(/[\(\); #]/, '');
+
+ // If the local color is rgb, clean it up and replace
+ if (local_color.substr(0,3).toLowerCase() == 'rgb') {
+ var components = local_color.substr(3).split(',');
+ local_color = '';
+ for (var i = 0; i < components.length; i++) {
+ var part = parseInt(components[i]).toString(16);
+ while (part.length < 2) part = '0' + part;
+ local_color += part;
+ }
+ }
+
+ this.test.assertEquals(hex_color.toUpperCase(), local_color.toUpperCase(), msg);
+};
+
+casper.notebook_test = function(test) {
+ // Wrap a notebook test to reduce boilerplate.
+ this.open_new_notebook();
+
+ // Echo whether or not we are running this test using SlimerJS
+ if (this.evaluate(function(){
+ return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+
+ })) {
+ console.log('This test is running in SlimerJS.');
+ this.slimerjs = true;
+ }
+
+ // Make sure to remove the onbeforeunload callback. This callback is
+ // responsible for the "Are you sure you want to quit?" type messages.
+ // PhantomJS ignores these prompts, SlimerJS does not which causes hangs.
+ this.then(function(){
+ this.evaluate(function(){
+ window.onbeforeunload = function(){};
+ });
+ });
+
+ this.then(test);
+
+ // Kill the kernel and delete the notebook.
+ this.shutdown_current_kernel();
+ // This is still broken but shouldn't be a problem for now.
+ // this.delete_current_notebook();
+
+ // This is required to clean up the page we just finished with. If we don't call this
+ // casperjs will leak file descriptors of all the open WebSockets in that page. We
+ // have to set this.page=null so that next time casper.start runs, it will create a
+ // new page from scratch.
+ this.then(function () {
+ this.page.close();
+ this.page = null;
+ });
+
+ // Run the browser automation.
+ this.run(function() {
+ this.test.done();
+ });
+};
+
+casper.wait_for_dashboard = function () {
+ // Wait for the dashboard list to load.
+ casper.waitForSelector('.list_item');
+};
+
+casper.open_dashboard = function () {
+ // Start casper by opening the dashboard page.
+ var baseUrl = this.get_notebook_server();
+ this.start(baseUrl);
+ this.waitFor(this.page_loaded);
+ this.wait_for_dashboard();
+};
+
+casper.dashboard_test = function (test) {
+ // Open the dashboard page and run a test.
+ this.open_dashboard();
+ this.then(test);
+
+ this.then(function () {
+ this.page.close();
+ this.page = null;
+ });
+
+ // Run the browser automation.
+ this.run(function() {
+ this.test.done();
+ });
+};
+
+// note that this will only work for UNIQUE events -- if you want to
+// listen for the same event twice, this will not work!
+casper.event_test = function (name, events, action, timeout) {
+
+ // set up handlers to listen for each of the events
+ this.thenEvaluate(function (events) {
+ var make_handler = function (event) {
+ return function () {
+ IPython._events_triggered.push(event);
+ IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
+ delete IPython._event_handlers[event];
+ };
+ };
+ IPython._event_handlers = {};
+ IPython._events_triggered = [];
+ for (var i=0; i < events.length; i++) {
+ IPython._event_handlers[events[i]] = make_handler(events[i]);
+ IPython.notebook.events.on(events[i], IPython._event_handlers[events[i]]);
+ }
+ }, [events]);
+
+ // execute the requested action
+ this.then(action);
+
+ // wait for all the events to be triggered
+ this.waitFor(function () {
+ return this.evaluate(function (events) {
+ return IPython._events_triggered.length >= events.length;
+ }, [events]);
+ }, undefined, undefined, timeout);
+
+ // test that the events were triggered in the proper order
+ this.then(function () {
+ var triggered = this.evaluate(function () {
+ return IPython._events_triggered;
+ });
+ var handlers = this.evaluate(function () {
+ return Object.keys(IPython._event_handlers);
+ });
+ this.test.assertEquals(triggered.length, events.length, name + ': ' + events.length + ' events were triggered');
+ this.test.assertEquals(handlers.length, 0, name + ': all handlers triggered');
+ for (var i=0; i < events.length; i++) {
+ this.test.assertEquals(triggered[i], events[i], name + ': ' + events[i] + ' was triggered');
+ }
+ });
+
+ // turn off any remaining event listeners
+ this.thenEvaluate(function () {
+ for (var event in IPython._event_handlers) {
+ IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
+ delete IPython._event_handlers[event];
+ }
+ });
+};
+
+casper.options.waitTimeout=10000;
+casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
+ this.echo("Timeout for " + casper.get_notebook_server());
+ this.echo("Is the notebook server running?");
+});
+
+casper.print_log = function () {
+ // Pass `console.log` calls from page JS to casper.
+ this.on('remote.message', function(msg) {
+ this.echo('Remote message caught: ' + msg);
+ });
+};
+
+casper.on("page.error", function onError(msg, trace) {
+ // show errors in the browser
+ this.echo("Page Error");
+ this.echo(" Message: " + msg.split('\n').join('\n '));
+ this.echo(" Call stack:");
+ var local_path = this.get_notebook_server();
+ for (var i = 0; i < trace.length; i++) {
+ var frame = trace[i];
+ var file = frame.file;
+ // shorten common phantomjs evaluate url
+ // this will have a different value on slimerjs
+ if (file === "phantomjs://webpage.evaluate()") {
+ file = "evaluate";
+ }
+ // remove the version tag from the path
+ file = file.replace(/(\?v=[0-9abcdef]+)/, '');
+ // remove the local address from the beginning of the path
+ if (file.indexOf(local_path) === 0) {
+ file = file.substr(local_path.length);
+ }
+ var frame_text = (frame.function.length > 0) ? " in " + frame.function : "";
+ this.echo(" line " + frame.line + " of " + file + frame_text);
+ }
+});
+
+
+casper.capture_log = function () {
+ // show captured errors
+ var captured_log = [];
+ var seen_errors = 0;
+ this.on('remote.message', function(msg) {
+ captured_log.push(msg);
+ });
+
+ var that = this;
+ this.test.on("test.done", function (result) {
+ // test.done runs per-file,
+ // but suiteResults is per-suite (directory)
+ var current_errors;
+ if (this.suiteResults) {
+ // casper 1.1 has suiteResults
+ current_errors = this.suiteResults.countErrors() + this.suiteResults.countFailed();
+ } else {
+ // casper 1.0 has testResults instead
+ current_errors = this.testResults.failed;
+ }
+
+ if (current_errors > seen_errors && captured_log.length > 0) {
+ casper.echo("\nCaptured console.log:");
+ for (var i = 0; i < captured_log.length; i++) {
+ var output = String(captured_log[i]).split('\n');
+ for (var j = 0; j < output.length; j++) {
+ casper.echo(" " + output[j]);
+ }
+ }
+ }
+
+ seen_errors = current_errors;
+ captured_log = [];
+ });
+};
+
+casper.interact = function() {
+ // Start an interactive Javascript console.
+ var system = require('system');
+ system.stdout.writeLine('JS interactive console.');
+ system.stdout.writeLine('Type `exit` to quit.');
+
+ function read_line() {
+ system.stdout.writeLine('JS: ');
+ var line = system.stdin.readLine();
+ return line;
+ }
+
+ var input = read_line();
+ while (input.trim() != 'exit') {
+ var output = this.evaluate(function(code) {
+ return String(eval(code));
+ }, {code: input});
+ system.stdout.writeLine('\nOut: ' + output);
+ input = read_line();
+ }
+};
+
+casper.capture_log();
diff --git a/notebook/tree/__init__.py b/notebook/tree/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/tree/__init__.py
diff --git a/notebook/tree/handlers.py b/notebook/tree/handlers.py
new file mode 100644
index 0000000..35b6d0a
--- /dev/null
+++ b/notebook/tree/handlers.py
@@ -0,0 +1,74 @@
+"""Tornado handlers for the tree view."""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from tornado import web
+from ..base.handlers import IPythonHandler, path_regex
+from ..utils import url_path_join, url_escape
+
+
+class TreeHandler(IPythonHandler):
+ """Render the tree view, listing notebooks, etc."""
+
+ def generate_breadcrumbs(self, path):
+ breadcrumbs = [(url_path_join(self.base_url, 'tree'), '')]
+ parts = path.split('/')
+ for i in range(len(parts)):
+ if parts[i]:
+ link = url_path_join(self.base_url, 'tree',
+ url_escape(url_path_join(*parts[:i+1])),
+ )
+ breadcrumbs.append((link, parts[i]))
+ return breadcrumbs
+
+ def generate_page_title(self, path):
+ comps = path.split('/')
+ if len(comps) > 3:
+ for i in range(len(comps)-2):
+ comps.pop(0)
+ page_title = url_path_join(*comps)
+ if page_title:
+ return page_title+'/'
+ else:
+ return 'Home'
+
+ @web.authenticated
+ def get(self, path=''):
+ path = path.strip('/')
+ cm = self.contents_manager
+ if cm.dir_exists(path=path):
+ if cm.is_hidden(path):
+ self.log.info("Refusing to serve hidden directory, via 404 Error")
+ raise web.HTTPError(404)
+ breadcrumbs = self.generate_breadcrumbs(path)
+ page_title = self.generate_page_title(path)
+ self.write(self.render_template('tree.html',
+ page_title=page_title,
+ notebook_path=path,
+ breadcrumbs=breadcrumbs,
+ terminals_available=self.settings['terminals_available'],
+ ))
+ elif cm.file_exists(path):
+ # it's not a directory, we have redirecting to do
+ model = cm.get(path, content=False)
+ # redirect to /api/notebooks if it's a notebook, otherwise /api/files
+ service = 'notebooks' if model['type'] == 'notebook' else 'files'
+ url = url_path_join(
+ self.base_url, service, url_escape(path),
+ )
+ self.log.debug("Redirecting %s to %s", self.request.path, url)
+ self.redirect(url)
+ else:
+ raise web.HTTPError(404)
+
+
+#-----------------------------------------------------------------------------
+# URL to handler mappings
+#-----------------------------------------------------------------------------
+
+
+default_handlers = [
+ (r"/tree%s" % path_regex, TreeHandler),
+ (r"/tree", TreeHandler),
+ ]
diff --git a/notebook/tree/tests/__init__.py b/notebook/tree/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notebook/tree/tests/__init__.py
diff --git a/notebook/tree/tests/test_tree_handler.py b/notebook/tree/tests/test_tree_handler.py
new file mode 100644
index 0000000..e2ea9e3
--- /dev/null
+++ b/notebook/tree/tests/test_tree_handler.py
@@ -0,0 +1,32 @@
+"""Test the /tree handlers"""
+import os
+import io
+from notebook.utils import url_path_join
+from nbformat import write
+from nbformat.v4 import new_notebook
+
+import requests
+
+from notebook.tests.launchnotebook import NotebookTestBase
+
+class TreeTest(NotebookTestBase):
+ def setUp(self):
+ nbdir = self.notebook_dir.name
+ d = os.path.join(nbdir, 'foo')
+ os.mkdir(d)
+
+ with io.open(os.path.join(d, 'bar.ipynb'), 'w', encoding='utf-8') as f:
+ nb = new_notebook()
+ write(nb, f, version=4)
+
+ with io.open(os.path.join(d, 'baz.txt'), 'w', encoding='utf-8') as f:
+ f.write(u'flamingo')
+
+ self.base_url()
+
+ def test_redirect(self):
+ r = requests.get(url_path_join(self.base_url(), 'tree/foo/bar.ipynb'))
+ self.assertEqual(r.url, self.base_url() + 'notebooks/foo/bar.ipynb')
+
+ r = requests.get(url_path_join(self.base_url(), 'tree/foo/baz.txt'))
+ self.assertEqual(r.url, url_path_join(self.base_url(), 'files/foo/baz.txt'))
diff --git a/notebook/utils.py b/notebook/utils.py
new file mode 100644
index 0000000..7580187
--- /dev/null
+++ b/notebook/utils.py
@@ -0,0 +1,207 @@
+"""Notebook related utilities"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from __future__ import print_function
+
+import ctypes
+import errno
+import os
+import stat
+import sys
+from distutils.version import LooseVersion
+
+try:
+ from urllib.parse import quote, unquote, urlparse
+except ImportError:
+ from urllib import quote, unquote
+ from urlparse import urlparse
+
+from ipython_genutils import py3compat
+
+# UF_HIDDEN is a stat flag not defined in the stat module.
+# It is used by BSD to indicate hidden files.
+UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
+
+
+def url_path_join(*pieces):
+ """Join components of url into a relative url
+
+ Use to prevent double slash when joining subpath. This will leave the
+ initial and final / in place
+ """
+ initial = pieces[0].startswith('/')
+ final = pieces[-1].endswith('/')
+ stripped = [s.strip('/') for s in pieces]
+ result = '/'.join(s for s in stripped if s)
+ if initial: result = '/' + result
+ if final: result = result + '/'
+ if result == '//': result = '/'
+ return result
+
+def url_is_absolute(url):
+ """Determine whether a given URL is absolute"""
+ return urlparse(url).path.startswith("/")
+
+def path2url(path):
+ """Convert a local file path to a URL"""
+ pieces = [ quote(p) for p in path.split(os.sep) ]
+ # preserve trailing /
+ if pieces[-1] == '':
+ pieces[-1] = '/'
+ url = url_path_join(*pieces)
+ return url
+
+def url2path(url):
+ """Convert a URL to a local file path"""
+ pieces = [ unquote(p) for p in url.split('/') ]
+ path = os.path.join(*pieces)
+ return path
+
+def url_escape(path):
+ """Escape special characters in a URL path
+
+ Turns '/foo bar/' into '/foo%20bar/'
+ """
+ parts = py3compat.unicode_to_str(path, encoding='utf8').split('/')
+ return u'/'.join([quote(p) for p in parts])
+
+def url_unescape(path):
+ """Unescape special characters in a URL path
+
+ Turns '/foo%20bar/' into '/foo bar/'
+ """
+ return u'/'.join([
+ py3compat.str_to_unicode(unquote(p), encoding='utf8')
+ for p in py3compat.unicode_to_str(path, encoding='utf8').split('/')
+ ])
+
+_win32_FILE_ATTRIBUTE_HIDDEN = 0x02
+
+def is_hidden(abs_path, abs_root=''):
+ """Is a file hidden or contained in a hidden directory?
+
+ This will start with the rightmost path element and work backwards to the
+ given root to see if a path is hidden or in a hidden directory. Hidden is
+ determined by either name starting with '.' or the UF_HIDDEN flag as
+ reported by stat.
+
+ Parameters
+ ----------
+ abs_path : unicode
+ The absolute path to check for hidden directories.
+ abs_root : unicode
+ The absolute path of the root directory in which hidden directories
+ should be checked for.
+ """
+ if not abs_root:
+ abs_root = abs_path.split(os.sep, 1)[0] + os.sep
+ inside_root = abs_path[len(abs_root):]
+ if any(part.startswith('.') for part in inside_root.split(os.sep)):
+ return True
+
+ # check that dirs can be listed
+ if os.path.isdir(abs_path):
+ if sys.platform == 'win32':
+ # can't trust os.access on Windows because it seems to always return True
+ try:
+ os.stat(abs_path)
+ except OSError:
+ # stat may fail on Windows junctions or non-user-readable dirs
+ return True
+ else:
+ # use x-access, not actual listing, in case of slow/large listings
+ if not os.access(abs_path, os.X_OK | os.R_OK):
+ return True
+
+ # check UF_HIDDEN on any location up to root
+ path = abs_path
+ while path and path.startswith(abs_root) and path != abs_root:
+ if not os.path.exists(path):
+ path = os.path.dirname(path)
+ continue
+ try:
+ # may fail on Windows junctions
+ st = os.stat(path)
+ except OSError:
+ return True
+ if getattr(st, 'st_flags', 0) & UF_HIDDEN:
+ return True
+ path = os.path.dirname(path)
+
+ if sys.platform == 'win32':
+ try:
+ attrs = ctypes.windll.kernel32.GetFileAttributesW(py3compat.cast_unicode(abs_path))
+ except AttributeError:
+ pass
+ else:
+ if attrs > 0 and attrs & _win32_FILE_ATTRIBUTE_HIDDEN:
+ return True
+
+ return False
+
+def to_os_path(path, root=''):
+ """Convert an API path to a filesystem path
+
+ If given, root will be prepended to the path.
+ root must be a filesystem path already.
+ """
+ parts = path.strip('/').split('/')
+ parts = [p for p in parts if p != ''] # remove duplicate splits
+ path = os.path.join(root, *parts)
+ return path
+
+def to_api_path(os_path, root=''):
+ """Convert a filesystem path to an API path
+
+ If given, root will be removed from the path.
+ root must be a filesystem path already.
+ """
+ if os_path.startswith(root):
+ os_path = os_path[len(root):]
+ parts = os_path.strip(os.path.sep).split(os.path.sep)
+ parts = [p for p in parts if p != ''] # remove duplicate splits
+ path = '/'.join(parts)
+ return path
+
+
+def check_version(v, check):
+ """check version string v >= check
+
+ If dev/prerelease tags result in TypeError for string-number comparison,
+ it is assumed that the dependency is satisfied.
+ Users on dev branches are responsible for keeping their own packages up to date.
+ """
+ try:
+ return LooseVersion(v) >= LooseVersion(check)
+ except TypeError:
+ return True
+
+
+# Copy of IPython.utils.process.check_pid:
+
+def _check_pid_win32(pid):
+ import ctypes
+ # OpenProcess returns 0 if no such process (of ours) exists
+ # positive int otherwise
+ return bool(ctypes.windll.kernel32.OpenProcess(1,0,pid))
+
+def _check_pid_posix(pid):
+ """Copy of IPython.utils.process.check_pid"""
+ try:
+ os.kill(pid, 0)
+ except OSError as err:
+ if err.errno == errno.ESRCH:
+ return False
+ elif err.errno == errno.EPERM:
+ # Don't have permission to signal the process - probably means it exists
+ return True
+ raise
+ else:
+ return True
+
+if sys.platform == 'win32':
+ check_pid = _check_pid_win32
+else:
+ check_pid = _check_pid_posix
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..603923b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "jupyter-notebook-deps",
+ "version": "4.0.0",
+ "description": "Jupyter Notebook nodejs dependencies",
+ "author": "Jupyter Developers",
+ "license": "BSD-3-Clause",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/jupyter/notebook.git"
+ },
+ "scripts": {
+ "bower": "bower install",
+ "build": "python setup.py js css"
+ },
+ "devDependencies": {
+ "bower": "*",
+ "less": "~2",
+ "requirejs": "^2.1.17"
+ }
+}
diff --git a/scripts/jupyter-nbextension b/scripts/jupyter-nbextension
new file mode 100644
index 0000000..298ecb0
--- /dev/null
+++ b/scripts/jupyter-nbextension
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+from notebook.nbextensions import main
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/jupyter-notebook b/scripts/jupyter-notebook
new file mode 100644
index 0000000..cc3b500
--- /dev/null
+++ b/scripts/jupyter-notebook
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+from notebook.notebookapp import main
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/jupyter-serverextension b/scripts/jupyter-serverextension
new file mode 100644
index 0000000..79b0b38
--- /dev/null
+++ b/scripts/jupyter-serverextension
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+from notebook.serverextensions import main
+
+if __name__ == '__main__':
+ main()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..3c6e79c
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=1
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..9cd515a
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""Setup script for Jupyter Notebook"""
+
+#-----------------------------------------------------------------------------
+# Copyright (c) 2015-, Jupyter Development Team.
+# Copyright (c) 2008-2015, IPython Development Team.
+#
+# Distributed under the terms of the Modified BSD License.
+#
+# The full license is in the file COPYING.md, distributed with this software.
+#-----------------------------------------------------------------------------
+
+from __future__ import print_function
+
+name = "notebook"
+
+#-----------------------------------------------------------------------------
+# Minimal Python version sanity check
+#-----------------------------------------------------------------------------
+
+import sys
+
+v = sys.version_info
+if v[:2] < (2,7) or (v[0] >= 3 and v[:2] < (3,3)):
+ error = "ERROR: %s requires Python version 2.7 or 3.3 or above." % name
+ print(error, file=sys.stderr)
+ sys.exit(1)
+
+PY3 = (sys.version_info[0] >= 3)
+
+# At least we're on the python version we need, move on.
+
+
+#-------------------------------------------------------------------------------
+# Imports
+#-------------------------------------------------------------------------------
+
+import os
+
+from glob import glob
+
+# BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
+# update it when the contents of directories change.
+if os.path.exists('MANIFEST'): os.remove('MANIFEST')
+
+from distutils.core import setup
+
+# Our own imports
+
+from setupbase import (
+ version,
+ find_packages,
+ find_package_data,
+ check_package_data_first,
+ CompileCSS,
+ CompileJS,
+ Bower,
+ JavascriptVersion,
+ css_js_prerelease,
+)
+
+isfile = os.path.isfile
+pjoin = os.path.join
+
+setup_args = dict(
+ name = name,
+ description = "A web-based notebook environment for interactive computing",
+ long_description = """
+The Jupyter Notebook is a web application that allows you to create and
+share documents that contain live code, equations, visualizations, and
+explanatory text. The Notebook has support for multiple programming
+languages, sharing, and interactive widgets.
+
+Read `the documentation <https://jupyter-notebook.readthedocs.org>`_
+for more information.
+ """,
+ version = version,
+ scripts = glob(pjoin('scripts', '*')),
+ packages = find_packages(),
+ package_data = find_package_data(),
+ author = 'Jupyter Development Team',
+ author_email = 'jupyter@googlegroups.com',
+ url = 'http://jupyter.org',
+ license = 'BSD',
+ platforms = "Linux, Mac OS X, Windows",
+ keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
+ classifiers = [
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: System Administrators',
+ 'Intended Audience :: Science/Research',
+ 'License :: OSI Approved :: BSD License',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ ],
+)
+
+
+
+#---------------------------------------------------------------------------
+# Find all the packages, package data, and data_files
+#---------------------------------------------------------------------------
+
+packages = find_packages()
+package_data = find_package_data()
+
+#---------------------------------------------------------------------------
+# custom distutils commands
+#---------------------------------------------------------------------------
+# imports here, so they are after setuptools import if there was one
+from distutils.command.build_py import build_py
+from distutils.command.sdist import sdist
+
+
+setup_args['cmdclass'] = {
+ 'build_py': css_js_prerelease(
+ check_package_data_first(build_py)),
+ 'sdist' : css_js_prerelease(sdist, strict=True),
+ 'css' : CompileCSS,
+ 'js' : CompileJS,
+ 'jsdeps' : Bower,
+ 'jsversion' : JavascriptVersion,
+}
+
+
+
+#---------------------------------------------------------------------------
+# Handle scripts, dependencies, and setuptools specific things
+#---------------------------------------------------------------------------
+
+if any(arg.startswith('bdist') for arg in sys.argv):
+ import setuptools
+
+# This dict is used for passing extra arguments that are setuptools
+# specific to setup
+setuptools_extra_args = {}
+
+# setuptools requirements
+
+pyzmq = 'pyzmq>=13'
+
+setup_args['scripts'] = glob(pjoin('scripts', '*'))
+
+install_requires = [
+ 'jinja2',
+ 'tornado>=4',
+ 'ipython_genutils',
+ 'traitlets',
+ 'jupyter_core',
+ 'jupyter_client',
+ 'nbformat',
+ 'nbconvert',
+ 'ipykernel', # bless IPython kernel for now
+]
+extras_require = {
+ ':sys_platform != "win32"': ['terminado>=0.3.3'],
+ 'doc': ['Sphinx>=1.1'],
+ 'test:python_version == "2.7"': ['mock'],
+ 'test': ['nose', 'requests'],
+}
+
+if 'setuptools' in sys.modules:
+ # setup.py develop should check for submodules
+ from setuptools.command.develop import develop
+ setup_args['cmdclass']['develop'] = css_js_prerelease(develop)
+
+ try:
+ from wheel.bdist_wheel import bdist_wheel
+ except ImportError:
+ pass
+ else:
+ setup_args['cmdclass']['bdist_wheel'] = css_js_prerelease(bdist_wheel)
+
+ setuptools_extra_args['zip_safe'] = False
+ setup_args['extras_require'] = extras_require
+ requires = setup_args['install_requires'] = install_requires
+
+ setup_args['entry_points'] = {
+ 'console_scripts': [
+ 'jupyter-notebook = notebook.notebookapp:main',
+ 'jupyter-nbextension = notebook.nbextensions:main',
+ 'jupyter-serverextension = notebook.serverextensions:main',
+ ]
+ }
+ setup_args.pop('scripts', None)
+
+#---------------------------------------------------------------------------
+# Do the actual setup now
+#---------------------------------------------------------------------------
+
+setup_args.update(setuptools_extra_args)
+
+def main():
+ setup(**setup_args)
+
+if __name__ == '__main__':
+ main()
diff --git a/setupbase.py b/setupbase.py
new file mode 100644
index 0000000..62d4e63
--- /dev/null
+++ b/setupbase.py
@@ -0,0 +1,563 @@
+# encoding: utf-8
+"""
+This module defines the things that are used in setup.py for building the notebook
+
+This includes:
+
+ * Functions for finding things like packages, package data, etc.
+ * A function for checking dependencies.
+"""
+
+# Copyright (c) IPython Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+from __future__ import print_function
+
+import os
+import sys
+
+import pipes
+from distutils import log
+from distutils.cmd import Command
+from fnmatch import fnmatch
+from glob import glob
+from multiprocessing.pool import ThreadPool
+from subprocess import check_call
+
+if sys.platform == 'win32':
+ from subprocess import list2cmdline
+else:
+ def list2cmdline(cmd_list):
+ return ' '.join(map(pipes.quote, cmd_list))
+
+#-------------------------------------------------------------------------------
+# Useful globals and utility functions
+#-------------------------------------------------------------------------------
+
+# A few handy globals
+isfile = os.path.isfile
+pjoin = os.path.join
+repo_root = os.path.dirname(os.path.abspath(__file__))
+is_repo = os.path.isdir(pjoin(repo_root, '.git'))
+
+def oscmd(s):
+ print(">", s)
+ os.system(s)
+
+# Py3 compatibility hacks, without assuming IPython itself is installed with
+# the full py3compat machinery.
+
+try:
+ execfile
+except NameError:
+ def execfile(fname, globs, locs=None):
+ locs = locs or globs
+ exec(compile(open(fname).read(), fname, "exec"), globs, locs)
+
+
+#---------------------------------------------------------------------------
+# Basic project information
+#---------------------------------------------------------------------------
+
+name = 'notebook'
+
+# release.py contains version, authors, license, url, keywords, etc.
+version_ns = {}
+execfile(pjoin(repo_root, name, '_version.py'), version_ns)
+
+version = version_ns['__version__']
+
+
+#---------------------------------------------------------------------------
+# Find packages
+#---------------------------------------------------------------------------
+
+def find_packages():
+ """
+ Find all of the packages.
+ """
+ packages = []
+ for dir,subdirs,files in os.walk(name):
+ package = dir.replace(os.path.sep, '.')
+ if '__init__.py' not in files:
+ # not a package
+ continue
+ packages.append(package)
+ return packages
+
+#---------------------------------------------------------------------------
+# Find package data
+#---------------------------------------------------------------------------
+
+def find_package_data():
+ """
+ Find package_data.
+ """
+ # This is not enough for these things to appear in an sdist.
+ # We need to muck with the MANIFEST to get this to work
+
+ # exclude components and less from the walk;
+ # we will build the components separately
+ excludes = [
+ pjoin('static', 'components'),
+ pjoin('static', '*', 'less'),
+ ]
+
+ # walk notebook resources:
+ cwd = os.getcwd()
+ os.chdir('notebook')
+ static_data = []
+ for parent, dirs, files in os.walk('static'):
+ if any(fnmatch(parent, pat) for pat in excludes):
+ # prevent descending into subdirs
+ dirs[:] = []
+ continue
+ for f in files:
+ static_data.append(pjoin(parent, f))
+
+ # for verification purposes, explicitly add main.min.js
+ # so that installation will fail if they are missing
+ for app in ['auth', 'edit', 'notebook', 'terminal', 'tree']:
+ static_data.append(pjoin('static', app, 'js', 'main.min.js'))
+
+ components = pjoin("static", "components")
+ # select the components we actually need to install
+ # (there are lots of resources we bundle for sdist-reasons that we don't actually use)
+ static_data.extend([
+ pjoin(components, "backbone", "backbone-min.js"),
+ pjoin(components, "bootstrap", "js", "bootstrap.min.js"),
+ pjoin(components, "bootstrap-tour", "build", "css", "bootstrap-tour.min.css"),
+ pjoin(components, "bootstrap-tour", "build", "js", "bootstrap-tour.min.js"),
+ pjoin(components, "es6-promise", "*.js"),
+ pjoin(components, "font-awesome", "fonts", "*.*"),
+ pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
+ pjoin(components, "jquery", "jquery.min.js"),
+ pjoin(components, "jquery-typeahead", "dist", "jquery.typeahead.min.js"),
+ pjoin(components, "jquery-typeahead", "dist", "jquery.typeahead.min.css"),
+ pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
+ pjoin(components, "jquery-ui", "themes", "smoothness", "jquery-ui.min.css"),
+ pjoin(components, "jquery-ui", "themes", "smoothness", "images", "*"),
+ pjoin(components, "marked", "lib", "marked.js"),
+ pjoin(components, "requirejs", "require.js"),
+ pjoin(components, "underscore", "underscore-min.js"),
+ pjoin(components, "moment", "moment.js"),
+ pjoin(components, "moment", "min", "moment.min.js"),
+ pjoin(components, "term.js", "src", "term.js"),
+ pjoin(components, "text-encoding", "lib", "encoding.js"),
+ ])
+
+ # Ship all of Codemirror's CSS and JS
+ for parent, dirs, files in os.walk(pjoin(components, 'codemirror')):
+ for f in files:
+ if f.endswith(('.js', '.css')):
+ static_data.append(pjoin(parent, f))
+
+ # Trim mathjax
+ mj = lambda *path: pjoin(components, 'MathJax', *path)
+ static_data.extend([
+ mj('MathJax.js'),
+ mj('config', 'TeX-AMS_HTML-full.js'),
+ mj('config', 'Safe.js'),
+ ])
+
+ trees = []
+ mj_out = mj('jax', 'output')
+
+ if os.path.exists(mj_out):
+ for output in os.listdir(mj_out):
+ path = pjoin(mj_out, output)
+ static_data.append(pjoin(path, '*.js'))
+ autoload = pjoin(path, 'autoload')
+ if os.path.isdir(autoload):
+ trees.append(autoload)
+
+ for tree in trees + [
+ mj('localization'), # limit to en?
+ mj('fonts', 'HTML-CSS', 'STIX-Web', 'woff'),
+ mj('extensions'),
+ mj('jax', 'input', 'TeX'),
+ mj('jax', 'output', 'HTML-CSS', 'fonts', 'STIX-Web'),
+ mj('jax', 'output', 'SVG', 'fonts', 'STIX-Web'),
+ ]:
+ for parent, dirs, files in os.walk(tree):
+ for f in files:
+ static_data.append(pjoin(parent, f))
+
+ os.chdir(os.path.join('tests',))
+ js_tests = glob('*.js') + glob('*/*.js')
+
+ os.chdir(cwd)
+
+ package_data = {
+ 'notebook' : ['templates/*'] + static_data,
+ 'notebook.tests' : js_tests,
+ }
+
+ return package_data
+
+
+def check_package_data(package_data):
+ """verify that package_data globs make sense"""
+ print("checking package data")
+ for pkg, data in package_data.items():
+ pkg_root = pjoin(*pkg.split('.'))
+ for d in data:
+ path = pjoin(pkg_root, d)
+ if '*' in path:
+ assert len(glob(path)) > 0, "No files match pattern %s" % path
+ else:
+ assert os.path.exists(path), "Missing package data: %s" % path
+
+
+def check_package_data_first(command):
+ """decorator for checking package_data before running a given command
+
+ Probably only needs to wrap build_py
+ """
+ class DecoratedCommand(command):
+ def run(self):
+ check_package_data(self.package_data)
+ command.run(self)
+ return DecoratedCommand
+
+def update_package_data(distribution):
+ """update package_data to catch changes during setup"""
+ build_py = distribution.get_command_obj('build_py')
+ distribution.package_data = find_package_data()
+ # re-init build_py options which load package_data
+ build_py.finalize_options()
+
+#---------------------------------------------------------------------------
+# Notebook related
+#---------------------------------------------------------------------------
+
+try:
+ from shutil import which
+except ImportError:
+ ## which() function copied from Python 3.4.3; PSF license
+ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
+ """Given a command, mode, and a PATH string, return the path which
+ conforms to the given mode on the PATH, or None if there is no such
+ file.
+
+ `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
+ of os.environ.get("PATH"), or can be overridden with a custom search
+ path.
+
+ """
+ # Check that a given file can be accessed with the correct mode.
+ # Additionally check that `file` is not a directory, as on Windows
+ # directories pass the os.access check.
+ def _access_check(fn, mode):
+ return (os.path.exists(fn) and os.access(fn, mode)
+ and not os.path.isdir(fn))
+
+ # If we're given a path with a directory part, look it up directly rather
+ # than referring to PATH directories. This includes checking relative to the
+ # current directory, e.g. ./script
+ if os.path.dirname(cmd):
+ if _access_check(cmd, mode):
+ return cmd
+ return None
+
+ if path is None:
+ path = os.environ.get("PATH", os.defpath)
+ if not path:
+ return None
+ path = path.split(os.pathsep)
+
+ if sys.platform == "win32":
+ # The current directory takes precedence on Windows.
+ if not os.curdir in path:
+ path.insert(0, os.curdir)
+
+ # PATHEXT is necessary to check on Windows.
+ pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
+ # See if the given file matches any of the expected path extensions.
+ # This will allow us to short circuit when given "python.exe".
+ # If it does match, only test that one, otherwise we have to try
+ # others.
+ if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
+ files = [cmd]
+ else:
+ files = [cmd + ext for ext in pathext]
+ else:
+ # On other platforms you don't have things like PATHEXT to tell you
+ # what file suffixes are executable, so just pass on cmd as-is.
+ files = [cmd]
+
+ seen = set()
+ for dir in path:
+ normdir = os.path.normcase(dir)
+ if not normdir in seen:
+ seen.add(normdir)
+ for thefile in files:
+ name = os.path.join(dir, thefile)
+ if _access_check(name, mode):
+ return name
+ return None
+
+
+static = pjoin(repo_root, 'notebook', 'static')
+
+npm_path = os.pathsep.join([
+ pjoin(repo_root, 'node_modules', '.bin'),
+ os.environ.get("PATH", os.defpath),
+])
+
+def mtime(path):
+ """shorthand for mtime"""
+ return os.stat(path).st_mtime
+
+
+def run(cmd, *args, **kwargs):
+ """Echo a command before running it"""
+ log.info('> ' + list2cmdline(cmd))
+ kwargs['shell'] = (sys.platform == 'win32')
+ return check_call(cmd, *args, **kwargs)
+
+
+class Bower(Command):
+ description = "fetch static client-side components with bower"
+
+ user_options = [
+ ('force', 'f', "force fetching of bower dependencies"),
+ ]
+
+ def initialize_options(self):
+ self.force = False
+
+ def finalize_options(self):
+ self.force = bool(self.force)
+
+ bower_dir = pjoin(static, 'components')
+ node_modules = pjoin(repo_root, 'node_modules')
+
+ def should_run(self):
+ if self.force:
+ return True
+ if not os.path.exists(self.bower_dir):
+ return True
+ return mtime(self.bower_dir) < mtime(pjoin(repo_root, 'bower.json'))
+
+ def should_run_npm(self):
+ if not which('npm'):
+ print("npm unavailable", file=sys.stderr)
+ return False
+ if not os.path.exists(self.node_modules):
+ return True
+ return mtime(self.node_modules) < mtime(pjoin(repo_root, 'package.json'))
+
+ def run(self):
+ if not self.should_run():
+ print("bower dependencies up to date")
+ return
+
+ if self.should_run_npm():
+ print("installing build dependencies with npm")
+ run(['npm', 'install'], cwd=repo_root)
+ os.utime(self.node_modules, None)
+
+ env = os.environ.copy()
+ env['PATH'] = npm_path
+
+ try:
+ run(
+ ['bower', 'install', '--allow-root', '--config.interactive=false'],
+ cwd=repo_root,
+ env=env
+ )
+ except OSError as e:
+ print("Failed to run bower: %s" % e, file=sys.stderr)
+ print("You can install js dependencies with `npm install`", file=sys.stderr)
+ raise
+ os.utime(self.bower_dir, None)
+ # update package data in case this created new files
+ update_package_data(self.distribution)
+
+
+class CompileCSS(Command):
+ """Recompile Notebook CSS
+
+ Regenerate the compiled CSS from LESS sources.
+
+ Requires various dev dependencies, such as require and lessc.
+ """
+ description = "Recompile Notebook CSS"
+ user_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ sources = []
+ targets = []
+ for name in ('ipython', 'style'):
+ sources.append(pjoin(static, 'style', '%s.less' % name))
+ targets.append(pjoin(static, 'style', '%s.min.css' % name))
+
+ def run(self):
+ self.run_command('jsdeps')
+ env = os.environ.copy()
+ env['PATH'] = npm_path
+
+ for src, dst in zip(self.sources, self.targets):
+ try:
+ run(['lessc',
+ '--source-map',
+ '--include-path=%s' % pipes.quote(static),
+ src,
+ dst,
+ ], cwd=repo_root, env=env)
+ except OSError as e:
+ print("Failed to build css: %s" % e, file=sys.stderr)
+ print("You can install js dependencies with `npm install`", file=sys.stderr)
+ raise
+ # update package data in case this created new files
+ update_package_data(self.distribution)
+
+
+class CompileJS(Command):
+ """Rebuild Notebook Javascript main.min.js files
+
+ Calls require via build-main.js
+ """
+ description = "Rebuild Notebook Javascript main.min.js files"
+ user_options = [
+ ('force', 'f', "force rebuilding js targets"),
+ ]
+
+ def initialize_options(self):
+ self.force = False
+
+ def finalize_options(self):
+ self.force = bool(self.force)
+
+ apps = ['notebook', 'tree', 'edit', 'terminal', 'auth']
+ targets = [ pjoin(static, app, 'js', 'main.min.js') for app in apps ]
+
+ def sources(self, name):
+ """Generator yielding .js sources that an application depends on"""
+ yield pjoin(static, name, 'js', 'main.js')
+
+ for sec in [name, 'base', 'auth']:
+ for f in glob(pjoin(static, sec, 'js', '*.js')):
+ if not f.endswith('.min.js'):
+ yield f
+ yield pjoin(static, 'services', 'config.js')
+ if name == 'notebook':
+ for f in glob(pjoin(static, 'services', '*', '*.js')):
+ yield f
+ for parent, dirs, files in os.walk(pjoin(static, 'components')):
+ if os.path.basename(parent) == 'MathJax':
+ # don't look in MathJax, since it takes forever to walk it
+ dirs[:] = []
+ continue
+ for f in files:
+ yield pjoin(parent, f)
+
+ def should_run(self, name, target):
+ if self.force or not os.path.exists(target):
+ return True
+ target_mtime = mtime(target)
+ for source in self.sources(name):
+ if mtime(source) > target_mtime:
+ print(source, target)
+ return True
+ return False
+
+ def build_main(self, name):
+ """Build main.min.js"""
+ target = pjoin(static, name, 'js', 'main.min.js')
+
+ if not self.should_run(name, target):
+ log.info("%s up to date" % target)
+ return
+ log.info("Rebuilding %s" % target)
+ run(['node', 'tools/build-main.js', name])
+
+ def run(self):
+ self.run_command('jsdeps')
+ env = os.environ.copy()
+ env['PATH'] = npm_path
+ pool = ThreadPool()
+ pool.map(self.build_main, self.apps)
+ # update package data in case this created new files
+ update_package_data(self.distribution)
+
+
+class JavascriptVersion(Command):
+ """write the javascript version to notebook javascript"""
+ description = "Write Jupyter version to javascript"
+ user_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ nsfile = pjoin(repo_root, "notebook", "static", "base", "js", "namespace.js")
+ with open(nsfile) as f:
+ lines = f.readlines()
+ with open(nsfile, 'w') as f:
+ found = False
+ for line in lines:
+ if line.strip().startswith("Jupyter.version"):
+ line = ' Jupyter.version = "{0}";\n'.format(version)
+ found = True
+ f.write(line)
+ if not found:
+ raise RuntimeError("Didn't find Jupyter.version line in %s" % nsfile)
+
+
+def css_js_prerelease(command, strict=False):
+ """decorator for building minified js/css prior to another command"""
+ class DecoratedCommand(command):
+ def run(self):
+ self.distribution.run_command('jsversion')
+ jsdeps = self.distribution.get_command_obj('jsdeps')
+ js = self.distribution.get_command_obj('js')
+ css = self.distribution.get_command_obj('css')
+ jsdeps.force = js.force = strict
+
+ targets = [ jsdeps.bower_dir ]
+ targets.extend(js.targets)
+ targets.extend(css.targets)
+ missing = [ t for t in targets if not os.path.exists(t) ]
+
+ if not is_repo and not missing:
+ # If we're an sdist, we aren't a repo and everything should be present.
+ # Don't rebuild js/css in that case.
+ command.run(self)
+ return
+
+ try:
+ self.distribution.run_command('css')
+ self.distribution.run_command('js')
+ except Exception as e:
+ # refresh missing
+ missing = [ t for t in targets if not os.path.exists(t) ]
+ if strict or missing:
+ # die if strict or any targets didn't build
+ prefix = os.path.commonprefix([repo_root + os.sep] + missing)
+ missing = [ m[len(prefix):] for m in missing ]
+ log.warn("rebuilding js and css failed. The following required files are missing: %s" % missing)
+ raise e
+ else:
+ log.warn("rebuilding js and css failed (not a problem)")
+ log.warn(str(e))
+
+ # check again for missing targets, just in case:
+ missing = [ t for t in targets if not os.path.exists(t) ]
+ if missing:
+ # command succeeded, but targets still missing (?!)
+ prefix = os.path.commonprefix([repo_root + os.sep] + missing)
+ missing = [ m[len(prefix):] for m in missing ]
+ raise ValueError("The following required files are missing: %s" % missing)
+
+ command.run(self)
+ return DecoratedCommand
diff --git a/tools/build-main.js b/tools/build-main.js
new file mode 100644
index 0000000..bd0316b
--- /dev/null
+++ b/tools/build-main.js
@@ -0,0 +1,68 @@
+// build main.min.js
+// spawned by gulp to allow parallelism
+
+var rjs = require('requirejs').optimize;
+
+var name = process.argv[2];
+
+var rjs_config = {
+ name: name + '/js/main',
+ out: './notebook/static/' + name + '/js/main.min.js',
+ baseUrl: 'notebook/static',
+ preserveLicenseComments: false, // license comments conflict with sourcemap generation
+ generateSourceMaps: true,
+ optimize: "none",
+ paths: {
+ underscore : 'components/underscore/underscore-min',
+ backbone : 'components/backbone/backbone-min',
+ jquery: 'components/jquery/jquery.min',
+ bootstrap: 'components/bootstrap/js/bootstrap.min',
+ bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
+ "jquery-ui": 'components/jquery-ui/ui/minified/jquery-ui.min',
+ moment: 'components/moment/moment',
+ codemirror: 'components/codemirror',
+ termjs: 'components/term.js/src/term',
+ typeahead: 'components/jquery-typeahead/dist/jquery.typeahead',
+ contents: 'empty:',
+ custom: 'empty:',
+ },
+ map: { // for backward compatibility
+ "*": {
+ "jqueryui": "jquery-ui",
+ }
+ },
+ shim: {
+ typeahead: {
+ deps: ["jquery"],
+ exports: "typeahead"
+ },
+ underscore: {
+ exports: '_'
+ },
+ backbone: {
+ deps: ["underscore", "jquery"],
+ exports: "Backbone"
+ },
+ bootstrap: {
+ deps: ["jquery"],
+ exports: "bootstrap"
+ },
+ bootstraptour: {
+ deps: ["bootstrap"],
+ exports: "Tour"
+ },
+ "jquery-ui": {
+ deps: ["jquery"],
+ exports: "$"
+ }
+ },
+
+ exclude: [
+ "custom/custom",
+ ]
+};
+
+rjs(rjs_config, console.log, function (err) {
+ console.log("Failed to build", name, err);
+ process.exit(1);
+});
diff --git a/tools/secure_notebook.py b/tools/secure_notebook.py
new file mode 100644
index 0000000..4590e43
--- /dev/null
+++ b/tools/secure_notebook.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+"""
+script to automatically setup notebook over SSL.
+
+Generate cert and keyfiles (rsa 1024) in ~/.ssh/, ask for a password, and add
+the corresponding entries in the notbook json configuration file.
+
+"""
+
+import six
+
+from notebook.auth import passwd
+from traitlets.config.loader import JSONFileConfigLoader, ConfigFileNotFound
+from jupyter_core.paths import jupyter_config_dir
+from traitlets.config import Config
+
+from contextlib import contextmanager
+
+from OpenSSL import crypto
+from os.path import exists, join
+
+import io
+import os
+import json
+import traceback
+
+
+def create_self_signed_cert(cert_dir, keyfile, certfile):
+ """
+ Create a self-signed `keyfile` and `certfile` in `cert_dir`
+
+ Abort if one of the keyfile of certfile exist.
+ """
+
+ if exists(join(cert_dir, certfile)) or exists(join(cert_dir, keyfile)):
+ raise FileExistsError('{} or {} already exist in {}. Aborting.'.format(keyfile, certfile, cert_dir))
+ else:
+ # create a key pair
+ k = crypto.PKey()
+ k.generate_key(crypto.TYPE_RSA, 1024)
+
+ # create a self-signed cert
+ cert = crypto.X509()
+ cert.get_subject().C = "US"
+ cert.get_subject().ST = "Jupyter notebook self-signed certificate"
+ cert.get_subject().L = "Jupyter notebook self-signed certificate"
+ cert.get_subject().O = "Jupyter notebook self-signed certificate"
+ cert.get_subject().OU = "my organization"
+ cert.get_subject().CN = "Jupyter notebook self-signed certificate"
+ cert.set_serial_number(1000)
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(365*24*60*60)
+ cert.set_issuer(cert.get_subject())
+ cert.set_pubkey(k)
+ cert.sign(k, 'sha256')
+
+ with io.open(join(cert_dir, certfile), "wt") as f:
+ f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf8'))
+ os.chmod(join(cert_dir, certfile), 0o600)
+
+ with io.open(join(cert_dir, keyfile), "wt") as f:
+ f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('utf8'))
+ os.chmod(join(cert_dir, keyfile), 0o600)
+
+
+
+@contextmanager
+def persist_config(mode=0o600):
+ """Context manager that can be use to modify a config object
+
+ On exit of the context manager, the config will be written back to disk,
+ by defauld with 600 permissions.
+ """
+
+ loader = JSONFileConfigLoader('jupyter_notebook_config.json', jupyter_config_dir())
+ try:
+ config = loader.load_config()
+ except ConfigFileNotFound:
+ config = Config()
+
+ yield config
+
+ filepath = os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json')
+ with io.open(filepath, 'w') as f:
+ f.write(six.u(json.dumps(config, indent=2)))
+ try:
+ os.chmod(filepath, mode)
+ except Exception:
+ traceback.print_exc()
+
+ print("Something went wrong changing file permissions")
+
+
+def set_password():
+ """Ask user for password, store it in notebook json configuration file"""
+
+ print("First choose a password.")
+ hashedpw = passwd()
+ print("We will store your password encrypted in the notebook configuration file: ")
+ print(hashedpw)
+
+ with persist_config() as config:
+ config.NotebookApp.password = hashedpw
+
+ print('... done\n')
+
+
+def set_certifs():
+ """
+ Generate certificate to run notebook over ssl and set up the notebook config.
+ """
+ print("Let's generate self-signed certificates to secure your connexion.")
+ print("where should the certificate live?")
+
+ location = input('path [~/.ssh]: ')
+ if not location.strip():
+ location = os.path.expanduser('~/.ssh')
+ keyfile = input('keyfile name [jupyter_server.key]: ')
+ if not keyfile.strip():
+ keyfile = 'jupyter_server.key'
+ certfile = input('certfile name [jupyter_server.crt]: ')
+ if not certfile.strip():
+ certfile = 'jupyter_server.crt'
+
+ create_self_signed_cert(location, keyfile, certfile)
+
+ fullkey = os.path.join(location, keyfile)
+ fullcrt = os.path.join(location, certfile)
+ with persist_config() as config:
+ config.NotebookApp.certfile = fullcrt
+ config.NotebookApp.keyfile = fullkey
+
+ print('done.\n')
+
+
+if __name__ == '__main__':
+ print("This will guide you through the steps towards securing your notebook server.")
+ set_password()
+ set_certifs()
diff --git a/tools/tests/ANSI Test.ipynb b/tools/tests/ANSI Test.ipynb
new file mode 100644
index 0000000..98c91ff
--- /dev/null
+++ b/tools/tests/ANSI Test.ipynb
@@ -0,0 +1,560 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This notebook tests the processing of ANSI and VT100 color escapes"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from __future__ import print_function"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "ESC = '\\x1b['\n",
+ "RESET = ESC + \"00m\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Plain ANSI 16-color"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[01;40;30mtext \u001b[01m\u001b[01;40;31mtext \u001b[01m\u001b[01;40;32mtext \u001b[01m\u001b[01;40;33mtext \u001b[01m\u001b[01;40;34mtext \u001b[01m\u001b[01;40;35mtext \u001b[01m\u001b[01;40;36mtext \u001b[01m\u001b[01;40;37mtext \u001b[01m\n",
+ "\u001b[01;41;30mtext \u001b[01m\u001b[01;41;31mtext \u001b[01m\u001b[01;41;32mtext \u001b[01m\u001b[01;41;33mtext \u001b[01m\u001b[01;41;34mtext \u001b[01m\u001b[01;41;35mtext \u001b[01m\u001b[01;41;36mtext \u001b[01m\u001b[01;41;37mtext \u001b[01m\n",
+ "\u001b[01;42;30mtext \u001b[01m\u001b[01;42;31mtext \u001b[01m\u001b[01;42;32mtext \u001b[01m\u001b[01;42;33mtext \u001b[01m\u001b[01;42;34mtext \u001b[01m\u001b[01;42;35mtext \u001b[01m\u001b[01;42;36mtext \u001b[01m\u001b[01;42;37mtext \u001b[01m\n",
+ "\u001b[01;43;30mtext \u001b[01m\u001b[01;43;31mtext \u001b[01m\u001b[01;43;32mtext \u001b[01m\u001b[01;43;33mtext \u001b[01m\u001b[01;43;34mtext \u001b[01m\u001b[01;43;35mtext \u001b[01m\u001b[01;43;36mtext \u001b[01m\u001b[01;43;37mtext \u001b[01m\n",
+ "\u001b[01;44;30mtext \u001b[01m\u001b[01;44;31mtext \u001b[01m\u001b[01;44;32mtext \u001b[01m\u001b[01;44;33mtext \u001b[01m\u001b[01;44;34mtext \u001b[01m\u001b[01;44;35mtext \u001b[01m\u001b[01;44;36mtext \u001b[01m\u001b[01;44;37mtext \u001b[01m\n",
+ "\u001b[01;45;30mtext \u001b[01m\u001b[01;45;31mtext \u001b[01m\u001b[01;45;32mtext \u001b[01m\u001b[01;45;33mtext \u001b[01m\u001b[01;45;34mtext \u001b[01m\u001b[01;45;35mtext \u001b[01m\u001b[01;45;36mtext \u001b[01m\u001b[01;45;37mtext \u001b[01m\n",
+ "\u001b[01;46;30mtext \u001b[01m\u001b[01;46;31mtext \u001b[01m\u001b[01;46;32mtext \u001b[01m\u001b[01;46;33mtext \u001b[01m\u001b[01;46;34mtext \u001b[01m\u001b[01;46;35mtext \u001b[01m\u001b[01;46;36mtext \u001b[01m\u001b[01;46;37mtext \u001b[01m\n",
+ "\u001b[01;47;30mtext \u001b[01m\u001b[01;47;31mtext \u001b[01m\u001b[01;47;32mtext \u001b[01m\u001b[01;47;33mtext \u001b[01m\u001b[01;47;34mtext \u001b[01m\u001b[01;47;35mtext \u001b[01m\u001b[01;47;36mtext \u001b[01m\u001b[01;47;37mtext \u001b[01m\n"
+ ]
+ }
+ ],
+ "source": [
+ "for bg in range(40,48):\n",
+ " for fg in range(30,38):\n",
+ " print (\"{ESC}01;{bg};{fg}mtext {RESET}\".format(**locals()), end='')\n",
+ " print ()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 256-color"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[00;38;5;0m000 \u001b[01m\u001b[00;38;5;1m001 \u001b[01m\u001b[00;38;5;2m002 \u001b[01m\u001b[00;38;5;3m003 \u001b[01m\u001b[00;38;5;4m004 \u001b[01m\u001b[00;38;5;5m005 \u001b[01m\u001b[00;38;5;6m006 \u001b[01m\u001b[00;38;5;7m007 \u001b[01m\n",
+ "\u001b[00;38;5;8m008 \u001b[01m\u001b[00;38;5;9m009 \u001b[01m\u001b[00;38;5;10m010 \u001b[01m\u001b[00;38;5;11m011 \u001b[01m\u001b[00;38;5;12m012 \u001b[01m\u001b[00;38;5;13m013 \u001b[01m\u001b[00;38;5;14m014 \u001b[01m\u001b[00;38;5;15m015 \u001b[01m\n",
+ "\n",
+ "\u001b[00;38;5;16m016 \u001b[01m\u001b[00;38;5;17m017 \u001b[01m\u001b[00;38;5;18m018 \u001b[01m\u001b[00;38;5;19m019 \u001b[01m\u001b[00;38;5;20m020 \u001b[01m\u001b[00;38;5;21m021 \u001b[01m\n",
+ "\u001b[00;38;5;22m022 \u001b[01m\u001b[00;38;5;23m023 \u001b[01m\u001b[00;38;5;24m024 \u001b[01m\u001b[00;38;5;25m025 \u001b[01m\u001b[00;38;5;26m026 \u001b[01m\u001b[00;38;5;27m027 \u001b[01m\n",
+ "\u001b[00;38;5;28m028 \u001b[01m\u001b[00;38;5;29m029 \u001b[01m\u001b[00;38;5;30m030 \u001b[01m\u001b[00;38;5;31m031 \u001b[01m\u001b[00;38;5;32m032 \u001b[01m\u001b[00;38;5;33m033 \u001b[01m\n",
+ "\u001b[00;38;5;34m034 \u001b[01m\u001b[00;38;5;35m035 \u001b[01m\u001b[00;38;5;36m036 \u001b[01m\u001b[00;38;5;37m037 \u001b[01m\u001b[00;38;5;38m038 \u001b[01m\u001b[00;38;5;39m039 \u001b[01m\n",
+ "\u001b[00;38;5;40m040 \u001b[01m\u001b[00;38;5;41m041 \u001b[01m\u001b[00;38;5;42m042 \u001b[01m\u001b[00;38;5;43m043 \u001b[01m\u001b[00;38;5;44m044 \u001b[01m\u001b[00;38;5;45m045 \u001b[01m\n",
+ "\u001b[00;38;5;46m046 \u001b[01m\u001b[00;38;5;47m047 \u001b[01m\u001b[00;38;5;48m048 \u001b[01m\u001b[00;38;5;49m049 \u001b[01m\u001b[00;38;5;50m050 \u001b[01m\u001b[00;38;5;51m051 \u001b[01m\n",
+ "\n",
+ "\u001b[00;38;5;52m052 \u001b[01m\u001b[00;38;5;53m053 \u001b[01m\u001b[00;38;5;54m054 \u001b[01m\u001b[00;38;5;55m055 \u001b[01m\u001b[00;38;5;56m056 \u001b[01m\u001b[00;38;5;57m057 \u001b[01m\n",
+ "\u001b[00;38;5;58m058 \u001b[01m\u001b[00;38;5;59m059 \u001b[01m\u001b[00;38;5;60m060 \u001b[01m\u001b[00;38;5;61m061 \u001b[01m\u001b[00;38;5;62m062 \u001b[01m\u001b[00;38;5;63m063 \u001b[01m\n",
+ "\u001b[00;38;5;64m064 \u001b[01m\u001b[00;38;5;65m065 \u001b[01m\u001b[00;38;5;66m066 \u001b[01m\u001b[00;38;5;67m067 \u001b[01m\u001b[00;38;5;68m068 \u001b[01m\u001b[00;38;5;69m069 \u001b[01m\n",
+ "\u001b[00;38;5;70m070 \u001b[01m\u001b[00;38;5;71m071 \u001b[01m\u001b[00;38;5;72m072 \u001b[01m\u001b[00;38;5;73m073 \u001b[01m\u001b[00;38;5;74m074 \u001b[01m\u001b[00;38;5;75m075 \u001b[01m\n",
+ "\u001b[00;38;5;76m076 \u001b[01m\u001b[00;38;5;77m077 \u001b[01m\u001b[00;38;5;78m078 \u001b[01m\u001b[00;38;5;79m079 \u001b[01m\u001b[00;38;5;80m080 \u001b[01m\u001b[00;38;5;81m081 \u001b[01m\n",
+ "\u001b[00;38;5;82m082 \u001b[01m\u001b[00;38;5;83m083 \u001b[01m\u001b[00;38;5;84m084 \u001b[01m\u001b[00;38;5;85m085 \u001b[01m\u001b[00;38;5;86m086 \u001b[01m\u001b[00;38;5;87m087 \u001b[01m\n",
+ "\n",
+ "\u001b[00;38;5;88m088 \u001b[01m\u001b[00;38;5;89m089 \u001b[01m\u001b[00;38;5;90m090 \u001b[01m\u001b[00;38;5;91m091 \u001b[01m\u001b[00;38;5;92m092 \u001b[01m\u001b[00;38;5;93m093 \u001b[01m\n",
+ "\u001b[00;38;5;94m094 \u001b[01m\u001b[00;38;5;95m095 \u001b[01m\u001b[00;38;5;96m096 \u001b[01m\u001b[00;38;5;97m097 \u001b[01m\u001b[00;38;5;98m098 \u001b[01m\u001b[00;38;5;99m099 \u001b[01m\n",
+ "\u001b[00;38;5;100m100 \u001b[01m\u001b[00;38;5;101m101 \u001b[01m\u001b[00;38;5;102m102 \u001b[01m\u001b[00;38;5;103m103 \u001b[01m\u001b[00;38;5;104m104 \u001b[01m\u001b[00;38;5;105m105 \u001b[01m\n",
+ "\u001b[00;38;5;106m106 \u001b[01m\u001b[00;38;5;107m107 \u001b[01m\u001b[00;38;5;108m108 \u001b[01m\u001b[00;38;5;109m109 \u001b[01m\u001b[00;38;5;110m110 \u001b[01m\u001b[00;38;5;111m111 \u001b[01m\n",
+ "\u001b[00;38;5;112m112 \u001b[01m\u001b[00;38;5;113m113 \u001b[01m\u001b[00;38;5;114m114 \u001b[01m\u001b[00;38;5;115m115 \u001b[01m\u001b[00;38;5;116m116 \u001b[01m\u001b[00;38;5;117m117 \u001b[01m\n",
+ "\u001b[00;38;5;118m118 \u001b[01m\u001b[00;38;5;119m119 \u001b[01m\u001b[00;38;5;120m120 \u001b[01m\u001b[00;38;5;121m121 \u001b[01m\u001b[00;38;5;122m122 \u001b[01m\u001b[00;38;5;123m123 \u001b[01m\n",
+ "\n",
+ "\u001b[00;38;5;124m124 \u001b[01m\u001b[00;38;5;125m125 \u001b[01m\u001b[00;38;5;126m126 \u001b[01m\u001b[00;38;5;127m127 \u001b[01m\u001b[00;38;5;128m128 \u001b[01m\u001b[00;38;5;129m129 \u001b[01m\n",
+ "\u001b[00;38;5;130m130 \u001b[01m\u001b[00;38;5;131m131 \u001b[01m\u001b[00;38;5;132m132 \u001b[01m\u001b[00;38;5;133m133 \u001b[01m\u001b[00;38;5;134m134 \u001b[01m\u001b[00;38;5;135m135 \u001b[01m\n",
+ "\u001b[00;38;5;136m136 \u001b[01m\u001b[00;38;5;137m137 \u001b[01m\u001b[00;38;5;138m138 \u001b[01m\u001b[00;38;5;139m139 \u001b[01m\u001b[00;38;5;140m140 \u001b[01m\u001b[00;38;5;141m141 \u001b[01m\n",
+ "\u001b[00;38;5;142m142 \u001b[01m\u001b[00;38;5;143m143 \u001b[01m\u001b[00;38;5;144m144 \u001b[01m\u001b[00;38;5;145m145 \u001b[01m\u001b[00;38;5;146m146 \u001b[01m\u001b[00;38;5;147m147 \u001b[01m\n",
+ "\u001b[00;38;5;148m148 \u001b[01m\u001b[00;38;5;149m149 \u001b[01m\u001b[00;38;5;150m150 \u001b[01m\u001b[00;38;5;151m151 \u001b[01m\u001b[00;38;5;152m152 \u001b[01m\u001b[00;38;5;153m153 \u001b[01m\n",
+ "\u001b[00;38;5;154m154 \u001b[01m\u001b[00;38;5;155m155 \u001b[01m\u001b[00;38;5;156m156 \u001b[01m\u001b[00;38;5;157m157 \u001b[01m\u001b[00;38;5;158m158 \u001b[01m\u001b[00;38;5;159m159 \u001b[01m\n",
+ "\n",
+ "\u001b[00;38;5;160m160 \u001b[01m\u001b[00;38;5;161m161 \u001b[01m\u001b[00;38;5;162m162 \u001b[01m\u001b[00;38;5;163m163 \u001b[01m\u001b[00;38;5;164m164 \u001b[01m\u001b[00;38;5;165m165 \u001b[01m\n",
+ "\u001b[00;38;5;166m166 \u001b[01m\u001b[00;38;5;167m167 \u001b[01m\u001b[00;38;5;168m168 \u001b[01m\u001b[00;38;5;169m169 \u001b[01m\u001b[00;38;5;170m170 \u001b[01m\u001b[00;38;5;171m171 \u001b[01m\n",
+ "\u001b[00;38;5;172m172 \u001b[01m\u001b[00;38;5;173m173 \u001b[01m\u001b[00;38;5;174m174 \u001b[01m\u001b[00;38;5;175m175 \u001b[01m\u001b[00;38;5;176m176 \u001b[01m\u001b[00;38;5;177m177 \u001b[01m\n",
+ "\u001b[00;38;5;178m178 \u001b[01m\u001b[00;38;5;179m179 \u001b[01m\u001b[00;38;5;180m180 \u001b[01m\u001b[00;38;5;181m181 \u001b[01m\u001b[00;38;5;182m182 \u001b[01m\u001b[00;38;5;183m183 \u001b[01m\n",
+ "\u001b[00;38;5;184m184 \u001b[01m\u001b[00;38;5;185m185 \u001b[01m\u001b[00;38;5;186m186 \u001b[01m\u001b[00;38;5;187m187 \u001b[01m\u001b[00;38;5;188m188 \u001b[01m\u001b[00;38;5;189m189 \u001b[01m\n",
+ "\u001b[00;38;5;190m190 \u001b[01m\u001b[00;38;5;191m191 \u001b[01m\u001b[00;38;5;192m192 \u001b[01m\u001b[00;38;5;193m193 \u001b[01m\u001b[00;38;5;194m194 \u001b[01m\u001b[00;38;5;195m195 \u001b[01m\n",
+ "\n",
+ "\u001b[00;38;5;196m196 \u001b[01m\u001b[00;38;5;197m197 \u001b[01m\u001b[00;38;5;198m198 \u001b[01m\u001b[00;38;5;199m199 \u001b[01m\u001b[00;38;5;200m200 \u001b[01m\u001b[00;38;5;201m201 \u001b[01m\n",
+ "\u001b[00;38;5;202m202 \u001b[01m\u001b[00;38;5;203m203 \u001b[01m\u001b[00;38;5;204m204 \u001b[01m\u001b[00;38;5;205m205 \u001b[01m\u001b[00;38;5;206m206 \u001b[01m\u001b[00;38;5;207m207 \u001b[01m\n",
+ "\u001b[00;38;5;208m208 \u001b[01m\u001b[00;38;5;209m209 \u001b[01m\u001b[00;38;5;210m210 \u001b[01m\u001b[00;38;5;211m211 \u001b[01m\u001b[00;38;5;212m212 \u001b[01m\u001b[00;38;5;213m213 \u001b[01m\n",
+ "\u001b[00;38;5;214m214 \u001b[01m\u001b[00;38;5;215m215 \u001b[01m\u001b[00;38;5;216m216 \u001b[01m\u001b[00;38;5;217m217 \u001b[01m\u001b[00;38;5;218m218 \u001b[01m\u001b[00;38;5;219m219 \u001b[01m\n",
+ "\u001b[00;38;5;220m220 \u001b[01m\u001b[00;38;5;221m221 \u001b[01m\u001b[00;38;5;222m222 \u001b[01m\u001b[00;38;5;223m223 \u001b[01m\u001b[00;38;5;224m224 \u001b[01m\u001b[00;38;5;225m225 \u001b[01m\n",
+ "\u001b[00;38;5;226m226 \u001b[01m\u001b[00;38;5;227m227 \u001b[01m\u001b[00;38;5;228m228 \u001b[01m\u001b[00;38;5;229m229 \u001b[01m\u001b[00;38;5;230m230 \u001b[01m\u001b[00;38;5;231m231 \u001b[01m\n",
+ "\n",
+ "\n",
+ "\u001b[00;38;5;232m232 \u001b[01m\u001b[00;38;5;233m233 \u001b[01m\u001b[00;38;5;234m234 \u001b[01m\u001b[00;38;5;235m235 \u001b[01m\u001b[00;38;5;236m236 \u001b[01m\u001b[00;38;5;237m237 \u001b[01m\u001b[00;38;5;238m238 \u001b[01m\u001b[00;38;5;239m239 \u001b[01m\u001b[00;38;5;240m240 \u001b[01m\u001b[00;38;5;241m241 \u001b[01m\u001b[00;38;5;242m242 \u001b[01m\u001b[00;38;5;243m243 \u001b[01m\n",
+ "\u001b[00;38;5;244m244 \u001b[01m\u001b[00;38;5;245m245 \u001b[01m\u001b[00;38;5;246m246 \u001b[01m\u001b[00;38;5;247m247 \u001b[01m\u001b[00;38;5;248m248 \u001b[01m\u001b[00;38;5;249m249 \u001b[01m\u001b[00;38;5;250m250 \u001b[01m\u001b[00;38;5;251m251 \u001b[01m\u001b[00;38;5;252m252 \u001b[01m\u001b[00;38;5;253m253 \u001b[01m\u001b[00;38;5;254m254 \u001b[01m\u001b[00;38;5;255m255 \u001b[01m\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "t = \"{ESC}00;38;5;{i}m{i:03} {RESET}\"\n",
+ "for i in range(16):\n",
+ " print (t.format(**locals()), end='')\n",
+ " if i % 8 == 7:\n",
+ " print ()\n",
+ "print ()\n",
+ "\n",
+ "for i in range(16,232):\n",
+ " print (t.format(**locals()), end='')\n",
+ " if (i-16) % 6 == 5:\n",
+ " print ()\n",
+ " if (i-16) % 36 == 35:\n",
+ " print ()\n",
+ "\n",
+ "print ()\n",
+ "\n",
+ "for i in range(232,256):\n",
+ " print (t.format(**locals()), end='')\n",
+ " if (i-232) % 12 == 11:\n",
+ " print ()\n",
+ "print ()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 256-color background"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[00;48;5;0m000 \u001b[01m\u001b[00;48;5;1m001 \u001b[01m\u001b[00;48;5;2m002 \u001b[01m\u001b[00;48;5;3m003 \u001b[01m\u001b[00;48;5;4m004 \u001b[01m\u001b[00;48;5;5m005 \u001b[01m\u001b[00;48;5;6m006 \u001b[01m\u001b[00;48;5;7m007 \u001b[01m\n",
+ "\u001b[00;48;5;8m008 \u001b[01m\u001b[00;48;5;9m009 \u001b[01m\u001b[00;48;5;10m010 \u001b[01m\u001b[00;48;5;11m011 \u001b[01m\u001b[00;48;5;12m012 \u001b[01m\u001b[00;48;5;13m013 \u001b[01m\u001b[00;48;5;14m014 \u001b[01m\u001b[00;48;5;15m015 \u001b[01m\n",
+ "\n",
+ "\u001b[00;48;5;16m016 \u001b[01m\u001b[00;48;5;17m017 \u001b[01m\u001b[00;48;5;18m018 \u001b[01m\u001b[00;48;5;19m019 \u001b[01m\u001b[00;48;5;20m020 \u001b[01m\u001b[00;48;5;21m021 \u001b[01m\n",
+ "\u001b[00;48;5;22m022 \u001b[01m\u001b[00;48;5;23m023 \u001b[01m\u001b[00;48;5;24m024 \u001b[01m\u001b[00;48;5;25m025 \u001b[01m\u001b[00;48;5;26m026 \u001b[01m\u001b[00;48;5;27m027 \u001b[01m\n",
+ "\u001b[00;48;5;28m028 \u001b[01m\u001b[00;48;5;29m029 \u001b[01m\u001b[00;48;5;30m030 \u001b[01m\u001b[00;48;5;31m031 \u001b[01m\u001b[00;48;5;32m032 \u001b[01m\u001b[00;48;5;33m033 \u001b[01m\n",
+ "\u001b[00;48;5;34m034 \u001b[01m\u001b[00;48;5;35m035 \u001b[01m\u001b[00;48;5;36m036 \u001b[01m\u001b[00;48;5;37m037 \u001b[01m\u001b[00;48;5;38m038 \u001b[01m\u001b[00;48;5;39m039 \u001b[01m\n",
+ "\u001b[00;48;5;40m040 \u001b[01m\u001b[00;48;5;41m041 \u001b[01m\u001b[00;48;5;42m042 \u001b[01m\u001b[00;48;5;43m043 \u001b[01m\u001b[00;48;5;44m044 \u001b[01m\u001b[00;48;5;45m045 \u001b[01m\n",
+ "\u001b[00;48;5;46m046 \u001b[01m\u001b[00;48;5;47m047 \u001b[01m\u001b[00;48;5;48m048 \u001b[01m\u001b[00;48;5;49m049 \u001b[01m\u001b[00;48;5;50m050 \u001b[01m\u001b[00;48;5;51m051 \u001b[01m\n",
+ "\n",
+ "\u001b[00;48;5;52m052 \u001b[01m\u001b[00;48;5;53m053 \u001b[01m\u001b[00;48;5;54m054 \u001b[01m\u001b[00;48;5;55m055 \u001b[01m\u001b[00;48;5;56m056 \u001b[01m\u001b[00;48;5;57m057 \u001b[01m\n",
+ "\u001b[00;48;5;58m058 \u001b[01m\u001b[00;48;5;59m059 \u001b[01m\u001b[00;48;5;60m060 \u001b[01m\u001b[00;48;5;61m061 \u001b[01m\u001b[00;48;5;62m062 \u001b[01m\u001b[00;48;5;63m063 \u001b[01m\n",
+ "\u001b[00;48;5;64m064 \u001b[01m\u001b[00;48;5;65m065 \u001b[01m\u001b[00;48;5;66m066 \u001b[01m\u001b[00;48;5;67m067 \u001b[01m\u001b[00;48;5;68m068 \u001b[01m\u001b[00;48;5;69m069 \u001b[01m\n",
+ "\u001b[00;48;5;70m070 \u001b[01m\u001b[00;48;5;71m071 \u001b[01m\u001b[00;48;5;72m072 \u001b[01m\u001b[00;48;5;73m073 \u001b[01m\u001b[00;48;5;74m074 \u001b[01m\u001b[00;48;5;75m075 \u001b[01m\n",
+ "\u001b[00;48;5;76m076 \u001b[01m\u001b[00;48;5;77m077 \u001b[01m\u001b[00;48;5;78m078 \u001b[01m\u001b[00;48;5;79m079 \u001b[01m\u001b[00;48;5;80m080 \u001b[01m\u001b[00;48;5;81m081 \u001b[01m\n",
+ "\u001b[00;48;5;82m082 \u001b[01m\u001b[00;48;5;83m083 \u001b[01m\u001b[00;48;5;84m084 \u001b[01m\u001b[00;48;5;85m085 \u001b[01m\u001b[00;48;5;86m086 \u001b[01m\u001b[00;48;5;87m087 \u001b[01m\n",
+ "\n",
+ "\u001b[00;48;5;88m088 \u001b[01m\u001b[00;48;5;89m089 \u001b[01m\u001b[00;48;5;90m090 \u001b[01m\u001b[00;48;5;91m091 \u001b[01m\u001b[00;48;5;92m092 \u001b[01m\u001b[00;48;5;93m093 \u001b[01m\n",
+ "\u001b[00;48;5;94m094 \u001b[01m\u001b[00;48;5;95m095 \u001b[01m\u001b[00;48;5;96m096 \u001b[01m\u001b[00;48;5;97m097 \u001b[01m\u001b[00;48;5;98m098 \u001b[01m\u001b[00;48;5;99m099 \u001b[01m\n",
+ "\u001b[00;48;5;100m100 \u001b[01m\u001b[00;48;5;101m101 \u001b[01m\u001b[00;48;5;102m102 \u001b[01m\u001b[00;48;5;103m103 \u001b[01m\u001b[00;48;5;104m104 \u001b[01m\u001b[00;48;5;105m105 \u001b[01m\n",
+ "\u001b[00;48;5;106m106 \u001b[01m\u001b[00;48;5;107m107 \u001b[01m\u001b[00;48;5;108m108 \u001b[01m\u001b[00;48;5;109m109 \u001b[01m\u001b[00;48;5;110m110 \u001b[01m\u001b[00;48;5;111m111 \u001b[01m\n",
+ "\u001b[00;48;5;112m112 \u001b[01m\u001b[00;48;5;113m113 \u001b[01m\u001b[00;48;5;114m114 \u001b[01m\u001b[00;48;5;115m115 \u001b[01m\u001b[00;48;5;116m116 \u001b[01m\u001b[00;48;5;117m117 \u001b[01m\n",
+ "\u001b[00;48;5;118m118 \u001b[01m\u001b[00;48;5;119m119 \u001b[01m\u001b[00;48;5;120m120 \u001b[01m\u001b[00;48;5;121m121 \u001b[01m\u001b[00;48;5;122m122 \u001b[01m\u001b[00;48;5;123m123 \u001b[01m\n",
+ "\n",
+ "\u001b[00;48;5;124m124 \u001b[01m\u001b[00;48;5;125m125 \u001b[01m\u001b[00;48;5;126m126 \u001b[01m\u001b[00;48;5;127m127 \u001b[01m\u001b[00;48;5;128m128 \u001b[01m\u001b[00;48;5;129m129 \u001b[01m\n",
+ "\u001b[00;48;5;130m130 \u001b[01m\u001b[00;48;5;131m131 \u001b[01m\u001b[00;48;5;132m132 \u001b[01m\u001b[00;48;5;133m133 \u001b[01m\u001b[00;48;5;134m134 \u001b[01m\u001b[00;48;5;135m135 \u001b[01m\n",
+ "\u001b[00;48;5;136m136 \u001b[01m\u001b[00;48;5;137m137 \u001b[01m\u001b[00;48;5;138m138 \u001b[01m\u001b[00;48;5;139m139 \u001b[01m\u001b[00;48;5;140m140 \u001b[01m\u001b[00;48;5;141m141 \u001b[01m\n",
+ "\u001b[00;48;5;142m142 \u001b[01m\u001b[00;48;5;143m143 \u001b[01m\u001b[00;48;5;144m144 \u001b[01m\u001b[00;48;5;145m145 \u001b[01m\u001b[00;48;5;146m146 \u001b[01m\u001b[00;48;5;147m147 \u001b[01m\n",
+ "\u001b[00;48;5;148m148 \u001b[01m\u001b[00;48;5;149m149 \u001b[01m\u001b[00;48;5;150m150 \u001b[01m\u001b[00;48;5;151m151 \u001b[01m\u001b[00;48;5;152m152 \u001b[01m\u001b[00;48;5;153m153 \u001b[01m\n",
+ "\u001b[00;48;5;154m154 \u001b[01m\u001b[00;48;5;155m155 \u001b[01m\u001b[00;48;5;156m156 \u001b[01m\u001b[00;48;5;157m157 \u001b[01m\u001b[00;48;5;158m158 \u001b[01m\u001b[00;48;5;159m159 \u001b[01m\n",
+ "\n",
+ "\u001b[00;48;5;160m160 \u001b[01m\u001b[00;48;5;161m161 \u001b[01m\u001b[00;48;5;162m162 \u001b[01m\u001b[00;48;5;163m163 \u001b[01m\u001b[00;48;5;164m164 \u001b[01m\u001b[00;48;5;165m165 \u001b[01m\n",
+ "\u001b[00;48;5;166m166 \u001b[01m\u001b[00;48;5;167m167 \u001b[01m\u001b[00;48;5;168m168 \u001b[01m\u001b[00;48;5;169m169 \u001b[01m\u001b[00;48;5;170m170 \u001b[01m\u001b[00;48;5;171m171 \u001b[01m\n",
+ "\u001b[00;48;5;172m172 \u001b[01m\u001b[00;48;5;173m173 \u001b[01m\u001b[00;48;5;174m174 \u001b[01m\u001b[00;48;5;175m175 \u001b[01m\u001b[00;48;5;176m176 \u001b[01m\u001b[00;48;5;177m177 \u001b[01m\n",
+ "\u001b[00;48;5;178m178 \u001b[01m\u001b[00;48;5;179m179 \u001b[01m\u001b[00;48;5;180m180 \u001b[01m\u001b[00;48;5;181m181 \u001b[01m\u001b[00;48;5;182m182 \u001b[01m\u001b[00;48;5;183m183 \u001b[01m\n",
+ "\u001b[00;48;5;184m184 \u001b[01m\u001b[00;48;5;185m185 \u001b[01m\u001b[00;48;5;186m186 \u001b[01m\u001b[00;48;5;187m187 \u001b[01m\u001b[00;48;5;188m188 \u001b[01m\u001b[00;48;5;189m189 \u001b[01m\n",
+ "\u001b[00;48;5;190m190 \u001b[01m\u001b[00;48;5;191m191 \u001b[01m\u001b[00;48;5;192m192 \u001b[01m\u001b[00;48;5;193m193 \u001b[01m\u001b[00;48;5;194m194 \u001b[01m\u001b[00;48;5;195m195 \u001b[01m\n",
+ "\n",
+ "\u001b[00;48;5;196m196 \u001b[01m\u001b[00;48;5;197m197 \u001b[01m\u001b[00;48;5;198m198 \u001b[01m\u001b[00;48;5;199m199 \u001b[01m\u001b[00;48;5;200m200 \u001b[01m\u001b[00;48;5;201m201 \u001b[01m\n",
+ "\u001b[00;48;5;202m202 \u001b[01m\u001b[00;48;5;203m203 \u001b[01m\u001b[00;48;5;204m204 \u001b[01m\u001b[00;48;5;205m205 \u001b[01m\u001b[00;48;5;206m206 \u001b[01m\u001b[00;48;5;207m207 \u001b[01m\n",
+ "\u001b[00;48;5;208m208 \u001b[01m\u001b[00;48;5;209m209 \u001b[01m\u001b[00;48;5;210m210 \u001b[01m\u001b[00;48;5;211m211 \u001b[01m\u001b[00;48;5;212m212 \u001b[01m\u001b[00;48;5;213m213 \u001b[01m\n",
+ "\u001b[00;48;5;214m214 \u001b[01m\u001b[00;48;5;215m215 \u001b[01m\u001b[00;48;5;216m216 \u001b[01m\u001b[00;48;5;217m217 \u001b[01m\u001b[00;48;5;218m218 \u001b[01m\u001b[00;48;5;219m219 \u001b[01m\n",
+ "\u001b[00;48;5;220m220 \u001b[01m\u001b[00;48;5;221m221 \u001b[01m\u001b[00;48;5;222m222 \u001b[01m\u001b[00;48;5;223m223 \u001b[01m\u001b[00;48;5;224m224 \u001b[01m\u001b[00;48;5;225m225 \u001b[01m\n",
+ "\u001b[00;48;5;226m226 \u001b[01m\u001b[00;48;5;227m227 \u001b[01m\u001b[00;48;5;228m228 \u001b[01m\u001b[00;48;5;229m229 \u001b[01m\u001b[00;48;5;230m230 \u001b[01m\u001b[00;48;5;231m231 \u001b[01m\n",
+ "\n",
+ "\n",
+ "\u001b[00;48;5;232m232 \u001b[01m\u001b[00;48;5;233m233 \u001b[01m\u001b[00;48;5;234m234 \u001b[01m\u001b[00;48;5;235m235 \u001b[01m\u001b[00;48;5;236m236 \u001b[01m\u001b[00;48;5;237m237 \u001b[01m\u001b[00;48;5;238m238 \u001b[01m\u001b[00;48;5;239m239 \u001b[01m\u001b[00;48;5;240m240 \u001b[01m\u001b[00;48;5;241m241 \u001b[01m\u001b[00;48;5;242m242 \u001b[01m\u001b[00;48;5;243m243 \u001b[01m\n",
+ "\u001b[00;48;5;244m244 \u001b[01m\u001b[00;48;5;245m245 \u001b[01m\u001b[00;48;5;246m246 \u001b[01m\u001b[00;48;5;247m247 \u001b[01m\u001b[00;48;5;248m248 \u001b[01m\u001b[00;48;5;249m249 \u001b[01m\u001b[00;48;5;250m250 \u001b[01m\u001b[00;48;5;251m251 \u001b[01m\u001b[00;48;5;252m252 \u001b[01m\u001b[00;48;5;253m253 \u001b[01m\u001b[00;48;5;254m254 \u001b[01m\u001b[00;48;5;255m255 \u001b[01m\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "t = \"{ESC}00;48;5;{i}m{i:03} {RESET}\"\n",
+ "for i in range(16):\n",
+ " print (t.format(**locals()), end='')\n",
+ " if i % 8 == 7:\n",
+ " print ()\n",
+ "\n",
+ "print ()\n",
+ "\n",
+ "for i in range(16,232):\n",
+ " print (t.format(**locals()), end='')\n",
+ " if (i-16) % 6 == 5:\n",
+ " print ()\n",
+ " if (i-16) % 36 == 35:\n",
+ " print ()\n",
+ "\n",
+ "print ()\n",
+ "\n",
+ "for i in range(232,256):\n",
+ " print (t.format(**locals()), end='')\n",
+ " if (i-232) % 12 == 11:\n",
+ " print ()\n",
+ "print ()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 256-color background and foreground"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 42,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[00;48;5;2;38;5;165m165 on 002 \u001b[00m\u001b[00;48;5;2;38;5;102m102 on 002 \u001b[00m\u001b[00;48;5;2;38;5;252m252 on 002 \u001b[00m\u001b[00;48;5;2;38;5;9m009 on 002 \u001b[00m\n",
+ "\u001b[00;48;5;57;38;5;165m165 on 057 \u001b[00m\u001b[00;48;5;57;38;5;102m102 on 057 \u001b[00m\u001b[00;48;5;57;38;5;252m252 on 057 \u001b[00m\u001b[00;48;5;57;38;5;9m009 on 057 \u001b[00m\n",
+ "\u001b[00;48;5;160;38;5;165m165 on 160 \u001b[00m\u001b[00;48;5;160;38;5;102m102 on 160 \u001b[00m\u001b[00;48;5;160;38;5;252m252 on 160 \u001b[00m\u001b[00;48;5;160;38;5;9m009 on 160 \u001b[00m\n",
+ "\u001b[00;48;5;246;38;5;165m165 on 246 \u001b[00m\u001b[00;48;5;246;38;5;102m102 on 246 \u001b[00m\u001b[00;48;5;246;38;5;252m252 on 246 \u001b[00m\u001b[00;48;5;246;38;5;9m009 on 246 \u001b[00m\n"
+ ]
+ }
+ ],
+ "source": [
+ "t = \"{ESC}00;48;5;{bg};38;5;{fg}m{fg:03} on {bg:03} {RESET}\"\n",
+ "\n",
+ "for bg in [2, 57, 160, 246]:\n",
+ " for fg in [165, 102, 252, 9]:\n",
+ " print (t.format(**locals()), end='')\n",
+ " print()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 24-bit RGB"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[00;38;2;0;0;0m000|000|000 \u001b[00m\u001b[00;38;2;0;0;30m000|000|030 \u001b[00m\u001b[00;38;2;0;0;60m000|000|060 \u001b[00m\u001b[00;38;2;0;0;90m000|000|090 \u001b[00m\u001b[00;38;2;0;0;120m000|000|120 \u001b[00m\u001b[00;38;2;0;0;150m000|000|150 \u001b[00m\u001b[00;38;2;0;0;180m000|000|180 \u001b[00m\u001b[00;38;2;0;0;210m000|000|210 \u001b[00m\u001b[00;38;2;0;0;240m000|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;0;30;0m000|030|000 \u001b[00m\u001b[00;38;2;0;30;30m000|030|030 \u001b[00m\u001b[00;38;2;0;30;60m000|030|060 \u001b[00m\u001b[00;38;2;0;30;90m000|030|090 \u001b[00m\u001b[00;38;2;0;30;120m000|030|120 \u001b[00m\u001b[00;38;2;0;30;150m000|030|150 \u001b[00m\u001b[00;38;2;0;30;180m000|030|180 \u001b[00m\u001b[00;38;2;0;30;210m000|030|210 \u001b[00m\u001b[00;38;2;0;30;240m000|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;0;60;0m000|060|000 \u001b[00m\u001b[00;38;2;0;60;30m000|060|030 \u001b[00m\u001b[00;38;2;0;60;60m000|060|060 \u001b[00m\u001b[00;38;2;0;60;90m000|060|090 \u001b[00m\u001b[00;38;2;0;60;120m000|060|120 \u001b[00m\u001b[00;38;2;0;60;150m000|060|150 \u001b[00m\u001b[00;38;2;0;60;180m000|060|180 \u001b[00m\u001b[00;38;2;0;60;210m000|060|210 \u001b[00m\u001b[00;38;2;0;60;240m000|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;0;90;0m000|090|000 \u001b[00m\u001b[00;38;2;0;90;30m000|090|030 \u001b[00m\u001b[00;38;2;0;90;60m000|090|060 \u001b[00m\u001b[00;38;2;0;90;90m000|090|090 \u001b[00m\u001b[00;38;2;0;90;120m000|090|120 \u001b[00m\u001b[00;38;2;0;90;150m000|090|150 \u001b[00m\u001b[00;38;2;0;90;180m000|090|180 \u001b[00m\u001b[00;38;2;0;90;210m000|090|210 \u001b[00m\u001b[00;38;2;0;90;240m000|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;0;120;0m000|120|000 \u001b[00m\u001b[00;38;2;0;120;30m000|120|030 \u001b[00m\u001b[00;38;2;0;120;60m000|120|060 \u001b[00m\u001b[00;38;2;0;120;90m000|120|090 \u001b[00m\u001b[00;38;2;0;120;120m000|120|120 \u001b[00m\u001b[00;38;2;0;120;150m000|120|150 \u001b[00m\u001b[00;38;2;0;120;180m000|120|180 \u001b[00m\u001b[00;38;2;0;120;210m000|120|210 \u001b[00m\u001b[00;38;2;0;120;240m000|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;0;150;0m000|150|000 \u001b[00m\u001b[00;38;2;0;150;30m000|150|030 \u001b[00m\u001b[00;38;2;0;150;60m000|150|060 \u001b[00m\u001b[00;38;2;0;150;90m000|150|090 \u001b[00m\u001b[00;38;2;0;150;120m000|150|120 \u001b[00m\u001b[00;38;2;0;150;150m000|150|150 \u001b[00m\u001b[00;38;2;0;150;180m000|150|180 \u001b[00m\u001b[00;38;2;0;150;210m000|150|210 \u001b[00m\u001b[00;38;2;0;150;240m000|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;0;180;0m000|180|000 \u001b[00m\u001b[00;38;2;0;180;30m000|180|030 \u001b[00m\u001b[00;38;2;0;180;60m000|180|060 \u001b[00m\u001b[00;38;2;0;180;90m000|180|090 \u001b[00m\u001b[00;38;2;0;180;120m000|180|120 \u001b[00m\u001b[00;38;2;0;180;150m000|180|150 \u001b[00m\u001b[00;38;2;0;180;180m000|180|180 \u001b[00m\u001b[00;38;2;0;180;210m000|180|210 \u001b[00m\u001b[00;38;2;0;180;240m000|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;0;210;0m000|210|000 \u001b[00m\u001b[00;38;2;0;210;30m000|210|030 \u001b[00m\u001b[00;38;2;0;210;60m000|210|060 \u001b[00m\u001b[00;38;2;0;210;90m000|210|090 \u001b[00m\u001b[00;38;2;0;210;120m000|210|120 \u001b[00m\u001b[00;38;2;0;210;150m000|210|150 \u001b[00m\u001b[00;38;2;0;210;180m000|210|180 \u001b[00m\u001b[00;38;2;0;210;210m000|210|210 \u001b[00m\u001b[00;38;2;0;210;240m000|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;0;240;0m000|240|000 \u001b[00m\u001b[00;38;2;0;240;30m000|240|030 \u001b[00m\u001b[00;38;2;0;240;60m000|240|060 \u001b[00m\u001b[00;38;2;0;240;90m000|240|090 \u001b[00m\u001b[00;38;2;0;240;120m000|240|120 \u001b[00m\u001b[00;38;2;0;240;150m000|240|150 \u001b[00m\u001b[00;38;2;0;240;180m000|240|180 \u001b[00m\u001b[00;38;2;0;240;210m000|240|210 \u001b[00m\u001b[00;38;2;0;240;240m000|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;38;2;30;0;0m030|000|000 \u001b[00m\u001b[00;38;2;30;0;30m030|000|030 \u001b[00m\u001b[00;38;2;30;0;60m030|000|060 \u001b[00m\u001b[00;38;2;30;0;90m030|000|090 \u001b[00m\u001b[00;38;2;30;0;120m030|000|120 \u001b[00m\u001b[00;38;2;30;0;150m030|000|150 \u001b[00m\u001b[00;38;2;30;0;180m030|000|180 \u001b[00m\u001b[00;38;2;30;0;210m030|000|210 \u001b[00m\u001b[00;38;2;30;0;240m030|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;30;30;0m030|030|000 \u001b[00m\u001b[00;38;2;30;30;30m030|030|030 \u001b[00m\u001b[00;38;2;30;30;60m030|030|060 \u001b[00m\u001b[00;38;2;30;30;90m030|030|090 \u001b[00m\u001b[00;38;2;30;30;120m030|030|120 \u001b[00m\u001b[00;38;2;30;30;150m030|030|150 \u001b[00m\u001b[00;38;2;30;30;180m030|030|180 \u001b[00m\u001b[00;38;2;30;30;210m030|030|210 \u001b[00m\u001b[00;38;2;30;30;240m030|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;30;60;0m030|060|000 \u001b[00m\u001b[00;38;2;30;60;30m030|060|030 \u001b[00m\u001b[00;38;2;30;60;60m030|060|060 \u001b[00m\u001b[00;38;2;30;60;90m030|060|090 \u001b[00m\u001b[00;38;2;30;60;120m030|060|120 \u001b[00m\u001b[00;38;2;30;60;150m030|060|150 \u001b[00m\u001b[00;38;2;30;60;180m030|060|180 \u001b[00m\u001b[00;38;2;30;60;210m030|060|210 \u001b[00m\u001b[00;38;2;30;60;240m030|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;30;90;0m030|090|000 \u001b[00m\u001b[00;38;2;30;90;30m030|090|030 \u001b[00m\u001b[00;38;2;30;90;60m030|090|060 \u001b[00m\u001b[00;38;2;30;90;90m030|090|090 \u001b[00m\u001b[00;38;2;30;90;120m030|090|120 \u001b[00m\u001b[00;38;2;30;90;150m030|090|150 \u001b[00m\u001b[00;38;2;30;90;180m030|090|180 \u001b[00m\u001b[00;38;2;30;90;210m030|090|210 \u001b[00m\u001b[00;38;2;30;90;240m030|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;30;120;0m030|120|000 \u001b[00m\u001b[00;38;2;30;120;30m030|120|030 \u001b[00m\u001b[00;38;2;30;120;60m030|120|060 \u001b[00m\u001b[00;38;2;30;120;90m030|120|090 \u001b[00m\u001b[00;38;2;30;120;120m030|120|120 \u001b[00m\u001b[00;38;2;30;120;150m030|120|150 \u001b[00m\u001b[00;38;2;30;120;180m030|120|180 \u001b[00m\u001b[00;38;2;30;120;210m030|120|210 \u001b[00m\u001b[00;38;2;30;120;240m030|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;30;150;0m030|150|000 \u001b[00m\u001b[00;38;2;30;150;30m030|150|030 \u001b[00m\u001b[00;38;2;30;150;60m030|150|060 \u001b[00m\u001b[00;38;2;30;150;90m030|150|090 \u001b[00m\u001b[00;38;2;30;150;120m030|150|120 \u001b[00m\u001b[00;38;2;30;150;150m030|150|150 \u001b[00m\u001b[00;38;2;30;150;180m030|150|180 \u001b[00m\u001b[00;38;2;30;150;210m030|150|210 \u001b[00m\u001b[00;38;2;30;150;240m030|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;30;180;0m030|180|000 \u001b[00m\u001b[00;38;2;30;180;30m030|180|030 \u001b[00m\u001b[00;38;2;30;180;60m030|180|060 \u001b[00m\u001b[00;38;2;30;180;90m030|180|090 \u001b[00m\u001b[00;38;2;30;180;120m030|180|120 \u001b[00m\u001b[00;38;2;30;180;150m030|180|150 \u001b[00m\u001b[00;38;2;30;180;180m030|180|180 \u001b[00m\u001b[00;38;2;30;180;210m030|180|210 \u001b[00m\u001b[00;38;2;30;180;240m030|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;30;210;0m030|210|000 \u001b[00m\u001b[00;38;2;30;210;30m030|210|030 \u001b[00m\u001b[00;38;2;30;210;60m030|210|060 \u001b[00m\u001b[00;38;2;30;210;90m030|210|090 \u001b[00m\u001b[00;38;2;30;210;120m030|210|120 \u001b[00m\u001b[00;38;2;30;210;150m030|210|150 \u001b[00m\u001b[00;38;2;30;210;180m030|210|180 \u001b[00m\u001b[00;38;2;30;210;210m030|210|210 \u001b[00m\u001b[00;38;2;30;210;240m030|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;30;240;0m030|240|000 \u001b[00m\u001b[00;38;2;30;240;30m030|240|030 \u001b[00m\u001b[00;38;2;30;240;60m030|240|060 \u001b[00m\u001b[00;38;2;30;240;90m030|240|090 \u001b[00m\u001b[00;38;2;30;240;120m030|240|120 \u001b[00m\u001b[00;38;2;30;240;150m030|240|150 \u001b[00m\u001b[00;38;2;30;240;180m030|240|180 \u001b[00m\u001b[00;38;2;30;240;210m030|240|210 \u001b[00m\u001b[00;38;2;30;240;240m030|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;38;2;60;0;0m060|000|000 \u001b[00m\u001b[00;38;2;60;0;30m060|000|030 \u001b[00m\u001b[00;38;2;60;0;60m060|000|060 \u001b[00m\u001b[00;38;2;60;0;90m060|000|090 \u001b[00m\u001b[00;38;2;60;0;120m060|000|120 \u001b[00m\u001b[00;38;2;60;0;150m060|000|150 \u001b[00m\u001b[00;38;2;60;0;180m060|000|180 \u001b[00m\u001b[00;38;2;60;0;210m060|000|210 \u001b[00m\u001b[00;38;2;60;0;240m060|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;60;30;0m060|030|000 \u001b[00m\u001b[00;38;2;60;30;30m060|030|030 \u001b[00m\u001b[00;38;2;60;30;60m060|030|060 \u001b[00m\u001b[00;38;2;60;30;90m060|030|090 \u001b[00m\u001b[00;38;2;60;30;120m060|030|120 \u001b[00m\u001b[00;38;2;60;30;150m060|030|150 \u001b[00m\u001b[00;38;2;60;30;180m060|030|180 \u001b[00m\u001b[00;38;2;60;30;210m060|030|210 \u001b[00m\u001b[00;38;2;60;30;240m060|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;60;60;0m060|060|000 \u001b[00m\u001b[00;38;2;60;60;30m060|060|030 \u001b[00m\u001b[00;38;2;60;60;60m060|060|060 \u001b[00m\u001b[00;38;2;60;60;90m060|060|090 \u001b[00m\u001b[00;38;2;60;60;120m060|060|120 \u001b[00m\u001b[00;38;2;60;60;150m060|060|150 \u001b[00m\u001b[00;38;2;60;60;180m060|060|180 \u001b[00m\u001b[00;38;2;60;60;210m060|060|210 \u001b[00m\u001b[00;38;2;60;60;240m060|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;60;90;0m060|090|000 \u001b[00m\u001b[00;38;2;60;90;30m060|090|030 \u001b[00m\u001b[00;38;2;60;90;60m060|090|060 \u001b[00m\u001b[00;38;2;60;90;90m060|090|090 \u001b[00m\u001b[00;38;2;60;90;120m060|090|120 \u001b[00m\u001b[00;38;2;60;90;150m060|090|150 \u001b[00m\u001b[00;38;2;60;90;180m060|090|180 \u001b[00m\u001b[00;38;2;60;90;210m060|090|210 \u001b[00m\u001b[00;38;2;60;90;240m060|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;60;120;0m060|120|000 \u001b[00m\u001b[00;38;2;60;120;30m060|120|030 \u001b[00m\u001b[00;38;2;60;120;60m060|120|060 \u001b[00m\u001b[00;38;2;60;120;90m060|120|090 \u001b[00m\u001b[00;38;2;60;120;120m060|120|120 \u001b[00m\u001b[00;38;2;60;120;150m060|120|150 \u001b[00m\u001b[00;38;2;60;120;180m060|120|180 \u001b[00m\u001b[00;38;2;60;120;210m060|120|210 \u001b[00m\u001b[00;38;2;60;120;240m060|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;60;150;0m060|150|000 \u001b[00m\u001b[00;38;2;60;150;30m060|150|030 \u001b[00m\u001b[00;38;2;60;150;60m060|150|060 \u001b[00m\u001b[00;38;2;60;150;90m060|150|090 \u001b[00m\u001b[00;38;2;60;150;120m060|150|120 \u001b[00m\u001b[00;38;2;60;150;150m060|150|150 \u001b[00m\u001b[00;38;2;60;150;180m060|150|180 \u001b[00m\u001b[00;38;2;60;150;210m060|150|210 \u001b[00m\u001b[00;38;2;60;150;240m060|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;60;180;0m060|180|000 \u001b[00m\u001b[00;38;2;60;180;30m060|180|030 \u001b[00m\u001b[00;38;2;60;180;60m060|180|060 \u001b[00m\u001b[00;38;2;60;180;90m060|180|090 \u001b[00m\u001b[00;38;2;60;180;120m060|180|120 \u001b[00m\u001b[00;38;2;60;180;150m060|180|150 \u001b[00m\u001b[00;38;2;60;180;180m060|180|180 \u001b[00m\u001b[00;38;2;60;180;210m060|180|210 \u001b[00m\u001b[00;38;2;60;180;240m060|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;60;210;0m060|210|000 \u001b[00m\u001b[00;38;2;60;210;30m060|210|030 \u001b[00m\u001b[00;38;2;60;210;60m060|210|060 \u001b[00m\u001b[00;38;2;60;210;90m060|210|090 \u001b[00m\u001b[00;38;2;60;210;120m060|210|120 \u001b[00m\u001b[00;38;2;60;210;150m060|210|150 \u001b[00m\u001b[00;38;2;60;210;180m060|210|180 \u001b[00m\u001b[00;38;2;60;210;210m060|210|210 \u001b[00m\u001b[00;38;2;60;210;240m060|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;60;240;0m060|240|000 \u001b[00m\u001b[00;38;2;60;240;30m060|240|030 \u001b[00m\u001b[00;38;2;60;240;60m060|240|060 \u001b[00m\u001b[00;38;2;60;240;90m060|240|090 \u001b[00m\u001b[00;38;2;60;240;120m060|240|120 \u001b[00m\u001b[00;38;2;60;240;150m060|240|150 \u001b[00m\u001b[00;38;2;60;240;180m060|240|180 \u001b[00m\u001b[00;38;2;60;240;210m060|240|210 \u001b[00m\u001b[00;38;2;60;240;240m060|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;38;2;90;0;0m090|000|000 \u001b[00m\u001b[00;38;2;90;0;30m090|000|030 \u001b[00m\u001b[00;38;2;90;0;60m090|000|060 \u001b[00m\u001b[00;38;2;90;0;90m090|000|090 \u001b[00m\u001b[00;38;2;90;0;120m090|000|120 \u001b[00m\u001b[00;38;2;90;0;150m090|000|150 \u001b[00m\u001b[00;38;2;90;0;180m090|000|180 \u001b[00m\u001b[00;38;2;90;0;210m090|000|210 \u001b[00m\u001b[00;38;2;90;0;240m090|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;90;30;0m090|030|000 \u001b[00m\u001b[00;38;2;90;30;30m090|030|030 \u001b[00m\u001b[00;38;2;90;30;60m090|030|060 \u001b[00m\u001b[00;38;2;90;30;90m090|030|090 \u001b[00m\u001b[00;38;2;90;30;120m090|030|120 \u001b[00m\u001b[00;38;2;90;30;150m090|030|150 \u001b[00m\u001b[00;38;2;90;30;180m090|030|180 \u001b[00m\u001b[00;38;2;90;30;210m090|030|210 \u001b[00m\u001b[00;38;2;90;30;240m090|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;90;60;0m090|060|000 \u001b[00m\u001b[00;38;2;90;60;30m090|060|030 \u001b[00m\u001b[00;38;2;90;60;60m090|060|060 \u001b[00m\u001b[00;38;2;90;60;90m090|060|090 \u001b[00m\u001b[00;38;2;90;60;120m090|060|120 \u001b[00m\u001b[00;38;2;90;60;150m090|060|150 \u001b[00m\u001b[00;38;2;90;60;180m090|060|180 \u001b[00m\u001b[00;38;2;90;60;210m090|060|210 \u001b[00m\u001b[00;38;2;90;60;240m090|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;90;90;0m090|090|000 \u001b[00m\u001b[00;38;2;90;90;30m090|090|030 \u001b[00m\u001b[00;38;2;90;90;60m090|090|060 \u001b[00m\u001b[00;38;2;90;90;90m090|090|090 \u001b[00m\u001b[00;38;2;90;90;120m090|090|120 \u001b[00m\u001b[00;38;2;90;90;150m090|090|150 \u001b[00m\u001b[00;38;2;90;90;180m090|090|180 \u001b[00m\u001b[00;38;2;90;90;210m090|090|210 \u001b[00m\u001b[00;38;2;90;90;240m090|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;90;120;0m090|120|000 \u001b[00m\u001b[00;38;2;90;120;30m090|120|030 \u001b[00m\u001b[00;38;2;90;120;60m090|120|060 \u001b[00m\u001b[00;38;2;90;120;90m090|120|090 \u001b[00m\u001b[00;38;2;90;120;120m090|120|120 \u001b[00m\u001b[00;38;2;90;120;150m090|120|150 \u001b[00m\u001b[00;38;2;90;120;180m090|120|180 \u001b[00m\u001b[00;38;2;90;120;210m090|120|210 \u001b[00m\u001b[00;38;2;90;120;240m090|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;90;150;0m090|150|000 \u001b[00m\u001b[00;38;2;90;150;30m090|150|030 \u001b[00m\u001b[00;38;2;90;150;60m090|150|060 \u001b[00m\u001b[00;38;2;90;150;90m090|150|090 \u001b[00m\u001b[00;38;2;90;150;120m090|150|120 \u001b[00m\u001b[00;38;2;90;150;150m090|150|150 \u001b[00m\u001b[00;38;2;90;150;180m090|150|180 \u001b[00m\u001b[00;38;2;90;150;210m090|150|210 \u001b[00m\u001b[00;38;2;90;150;240m090|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;90;180;0m090|180|000 \u001b[00m\u001b[00;38;2;90;180;30m090|180|030 \u001b[00m\u001b[00;38;2;90;180;60m090|180|060 \u001b[00m\u001b[00;38;2;90;180;90m090|180|090 \u001b[00m\u001b[00;38;2;90;180;120m090|180|120 \u001b[00m\u001b[00;38;2;90;180;150m090|180|150 \u001b[00m\u001b[00;38;2;90;180;180m090|180|180 \u001b[00m\u001b[00;38;2;90;180;210m090|180|210 \u001b[00m\u001b[00;38;2;90;180;240m090|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;90;210;0m090|210|000 \u001b[00m\u001b[00;38;2;90;210;30m090|210|030 \u001b[00m\u001b[00;38;2;90;210;60m090|210|060 \u001b[00m\u001b[00;38;2;90;210;90m090|210|090 \u001b[00m\u001b[00;38;2;90;210;120m090|210|120 \u001b[00m\u001b[00;38;2;90;210;150m090|210|150 \u001b[00m\u001b[00;38;2;90;210;180m090|210|180 \u001b[00m\u001b[00;38;2;90;210;210m090|210|210 \u001b[00m\u001b[00;38;2;90;210;240m090|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;90;240;0m090|240|000 \u001b[00m\u001b[00;38;2;90;240;30m090|240|030 \u001b[00m\u001b[00;38;2;90;240;60m090|240|060 \u001b[00m\u001b[00;38;2;90;240;90m090|240|090 \u001b[00m\u001b[00;38;2;90;240;120m090|240|120 \u001b[00m\u001b[00;38;2;90;240;150m090|240|150 \u001b[00m\u001b[00;38;2;90;240;180m090|240|180 \u001b[00m\u001b[00;38;2;90;240;210m090|240|210 \u001b[00m\u001b[00;38;2;90;240;240m090|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;38;2;120;0;0m120|000|000 \u001b[00m\u001b[00;38;2;120;0;30m120|000|030 \u001b[00m\u001b[00;38;2;120;0;60m120|000|060 \u001b[00m\u001b[00;38;2;120;0;90m120|000|090 \u001b[00m\u001b[00;38;2;120;0;120m120|000|120 \u001b[00m\u001b[00;38;2;120;0;150m120|000|150 \u001b[00m\u001b[00;38;2;120;0;180m120|000|180 \u001b[00m\u001b[00;38;2;120;0;210m120|000|210 \u001b[00m\u001b[00;38;2;120;0;240m120|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;120;30;0m120|030|000 \u001b[00m\u001b[00;38;2;120;30;30m120|030|030 \u001b[00m\u001b[00;38;2;120;30;60m120|030|060 \u001b[00m\u001b[00;38;2;120;30;90m120|030|090 \u001b[00m\u001b[00;38;2;120;30;120m120|030|120 \u001b[00m\u001b[00;38;2;120;30;150m120|030|150 \u001b[00m\u001b[00;38;2;120;30;180m120|030|180 \u001b[00m\u001b[00;38;2;120;30;210m120|030|210 \u001b[00m\u001b[00;38;2;120;30;240m120|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;120;60;0m120|060|000 \u001b[00m\u001b[00;38;2;120;60;30m120|060|030 \u001b[00m\u001b[00;38;2;120;60;60m120|060|060 \u001b[00m\u001b[00;38;2;120;60;90m120|060|090 \u001b[00m\u001b[00;38;2;120;60;120m120|060|120 \u001b[00m\u001b[00;38;2;120;60;150m120|060|150 \u001b[00m\u001b[00;38;2;120;60;180m120|060|180 \u001b[00m\u001b[00;38;2;120;60;210m120|060|210 \u001b[00m\u001b[00;38;2;120;60;240m120|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;120;90;0m120|090|000 \u001b[00m\u001b[00;38;2;120;90;30m120|090|030 \u001b[00m\u001b[00;38;2;120;90;60m120|090|060 \u001b[00m\u001b[00;38;2;120;90;90m120|090|090 \u001b[00m\u001b[00;38;2;120;90;120m120|090|120 \u001b[00m\u001b[00;38;2;120;90;150m120|090|150 \u001b[00m\u001b[00;38;2;120;90;180m120|090|180 \u001b[00m\u001b[00;38;2;120;90;210m120|090|210 \u001b[00m\u001b[00;38;2;120;90;240m120|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;120;120;0m120|120|000 \u001b[00m\u001b[00;38;2;120;120;30m120|120|030 \u001b[00m\u001b[00;38;2;120;120;60m120|120|060 \u001b[00m\u001b[00;38;2;120;120;90m120|120|090 \u001b[00m\u001b[00;38;2;120;120;120m120|120|120 \u001b[00m\u001b[00;38;2;120;120;150m120|120|150 \u001b[00m\u001b[00;38;2;120;120;180m120|120|180 \u001b[00m\u001b[00;38;2;120;120;210m120|120|210 \u001b[00m\u001b[00;38;2;120;120;240m120|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;120;150;0m120|150|000 \u001b[00m\u001b[00;38;2;120;150;30m120|150|030 \u001b[00m\u001b[00;38;2;120;150;60m120|150|060 \u001b[00m\u001b[00;38;2;120;150;90m120|150|090 \u001b[00m\u001b[00;38;2;120;150;120m120|150|120 \u001b[00m\u001b[00;38;2;120;150;150m120|150|150 \u001b[00m\u001b[00;38;2;120;150;180m120|150|180 \u001b[00m\u001b[00;38;2;120;150;210m120|150|210 \u001b[00m\u001b[00;38;2;120;150;240m120|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;120;180;0m120|180|000 \u001b[00m\u001b[00;38;2;120;180;30m120|180|030 \u001b[00m\u001b[00;38;2;120;180;60m120|180|060 \u001b[00m\u001b[00;38;2;120;180;90m120|180|090 \u001b[00m\u001b[00;38;2;120;180;120m120|180|120 \u001b[00m\u001b[00;38;2;120;180;150m120|180|150 \u001b[00m\u001b[00;38;2;120;180;180m120|180|180 \u001b[00m\u001b[00;38;2;120;180;210m120|180|210 \u001b[00m\u001b[00;38;2;120;180;240m120|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;120;210;0m120|210|000 \u001b[00m\u001b[00;38;2;120;210;30m120|210|030 \u001b[00m\u001b[00;38;2;120;210;60m120|210|060 \u001b[00m\u001b[00;38;2;120;210;90m120|210|090 \u001b[00m\u001b[00;38;2;120;210;120m120|210|120 \u001b[00m\u001b[00;38;2;120;210;150m120|210|150 \u001b[00m\u001b[00;38;2;120;210;180m120|210|180 \u001b[00m\u001b[00;38;2;120;210;210m120|210|210 \u001b[00m\u001b[00;38;2;120;210;240m120|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;120;240;0m120|240|000 \u001b[00m\u001b[00;38;2;120;240;30m120|240|030 \u001b[00m\u001b[00;38;2;120;240;60m120|240|060 \u001b[00m\u001b[00;38;2;120;240;90m120|240|090 \u001b[00m\u001b[00;38;2;120;240;120m120|240|120 \u001b[00m\u001b[00;38;2;120;240;150m120|240|150 \u001b[00m\u001b[00;38;2;120;240;180m120|240|180 \u001b[00m\u001b[00;38;2;120;240;210m120|240|210 \u001b[00m\u001b[00;38;2;120;240;240m120|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;38;2;150;0;0m150|000|000 \u001b[00m\u001b[00;38;2;150;0;30m150|000|030 \u001b[00m\u001b[00;38;2;150;0;60m150|000|060 \u001b[00m\u001b[00;38;2;150;0;90m150|000|090 \u001b[00m\u001b[00;38;2;150;0;120m150|000|120 \u001b[00m\u001b[00;38;2;150;0;150m150|000|150 \u001b[00m\u001b[00;38;2;150;0;180m150|000|180 \u001b[00m\u001b[00;38;2;150;0;210m150|000|210 \u001b[00m\u001b[00;38;2;150;0;240m150|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;150;30;0m150|030|000 \u001b[00m\u001b[00;38;2;150;30;30m150|030|030 \u001b[00m\u001b[00;38;2;150;30;60m150|030|060 \u001b[00m\u001b[00;38;2;150;30;90m150|030|090 \u001b[00m\u001b[00;38;2;150;30;120m150|030|120 \u001b[00m\u001b[00;38;2;150;30;150m150|030|150 \u001b[00m\u001b[00;38;2;150;30;180m150|030|180 \u001b[00m\u001b[00;38;2;150;30;210m150|030|210 \u001b[00m\u001b[00;38;2;150;30;240m150|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;150;60;0m150|060|000 \u001b[00m\u001b[00;38;2;150;60;30m150|060|030 \u001b[00m\u001b[00;38;2;150;60;60m150|060|060 \u001b[00m\u001b[00;38;2;150;60;90m150|060|090 \u001b[00m\u001b[00;38;2;150;60;120m150|060|120 \u001b[00m\u001b[00;38;2;150;60;150m150|060|150 \u001b[00m\u001b[00;38;2;150;60;180m150|060|180 \u001b[00m\u001b[00;38;2;150;60;210m150|060|210 \u001b[00m\u001b[00;38;2;150;60;240m150|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;150;90;0m150|090|000 \u001b[00m\u001b[00;38;2;150;90;30m150|090|030 \u001b[00m\u001b[00;38;2;150;90;60m150|090|060 \u001b[00m\u001b[00;38;2;150;90;90m150|090|090 \u001b[00m\u001b[00;38;2;150;90;120m150|090|120 \u001b[00m\u001b[00;38;2;150;90;150m150|090|150 \u001b[00m\u001b[00;38;2;150;90;180m150|090|180 \u001b[00m\u001b[00;38;2;150;90;210m150|090|210 \u001b[00m\u001b[00;38;2;150;90;240m150|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;150;120;0m150|120|000 \u001b[00m\u001b[00;38;2;150;120;30m150|120|030 \u001b[00m\u001b[00;38;2;150;120;60m150|120|060 \u001b[00m\u001b[00;38;2;150;120;90m150|120|090 \u001b[00m\u001b[00;38;2;150;120;120m150|120|120 \u001b[00m\u001b[00;38;2;150;120;150m150|120|150 \u001b[00m\u001b[00;38;2;150;120;180m150|120|180 \u001b[00m\u001b[00;38;2;150;120;210m150|120|210 \u001b[00m\u001b[00;38;2;150;120;240m150|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;150;150;0m150|150|000 \u001b[00m\u001b[00;38;2;150;150;30m150|150|030 \u001b[00m\u001b[00;38;2;150;150;60m150|150|060 \u001b[00m\u001b[00;38;2;150;150;90m150|150|090 \u001b[00m\u001b[00;38;2;150;150;120m150|150|120 \u001b[00m\u001b[00;38;2;150;150;150m150|150|150 \u001b[00m\u001b[00;38;2;150;150;180m150|150|180 \u001b[00m\u001b[00;38;2;150;150;210m150|150|210 \u001b[00m\u001b[00;38;2;150;150;240m150|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;150;180;0m150|180|000 \u001b[00m\u001b[00;38;2;150;180;30m150|180|030 \u001b[00m\u001b[00;38;2;150;180;60m150|180|060 \u001b[00m\u001b[00;38;2;150;180;90m150|180|090 \u001b[00m\u001b[00;38;2;150;180;120m150|180|120 \u001b[00m\u001b[00;38;2;150;180;150m150|180|150 \u001b[00m\u001b[00;38;2;150;180;180m150|180|180 \u001b[00m\u001b[00;38;2;150;180;210m150|180|210 \u001b[00m\u001b[00;38;2;150;180;240m150|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;150;210;0m150|210|000 \u001b[00m\u001b[00;38;2;150;210;30m150|210|030 \u001b[00m\u001b[00;38;2;150;210;60m150|210|060 \u001b[00m\u001b[00;38;2;150;210;90m150|210|090 \u001b[00m\u001b[00;38;2;150;210;120m150|210|120 \u001b[00m\u001b[00;38;2;150;210;150m150|210|150 \u001b[00m\u001b[00;38;2;150;210;180m150|210|180 \u001b[00m\u001b[00;38;2;150;210;210m150|210|210 \u001b[00m\u001b[00;38;2;150;210;240m150|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;150;240;0m150|240|000 \u001b[00m\u001b[00;38;2;150;240;30m150|240|030 \u001b[00m\u001b[00;38;2;150;240;60m150|240|060 \u001b[00m\u001b[00;38;2;150;240;90m150|240|090 \u001b[00m\u001b[00;38;2;150;240;120m150|240|120 \u001b[00m\u001b[00;38;2;150;240;150m150|240|150 \u001b[00m\u001b[00;38;2;150;240;180m150|240|180 \u001b[00m\u001b[00;38;2;150;240;210m150|240|210 \u001b[00m\u001b[00;38;2;150;240;240m150|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;38;2;180;0;0m180|000|000 \u001b[00m\u001b[00;38;2;180;0;30m180|000|030 \u001b[00m\u001b[00;38;2;180;0;60m180|000|060 \u001b[00m\u001b[00;38;2;180;0;90m180|000|090 \u001b[00m\u001b[00;38;2;180;0;120m180|000|120 \u001b[00m\u001b[00;38;2;180;0;150m180|000|150 \u001b[00m\u001b[00;38;2;180;0;180m180|000|180 \u001b[00m\u001b[00;38;2;180;0;210m180|000|210 \u001b[00m\u001b[00;38;2;180;0;240m180|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;180;30;0m180|030|000 \u001b[00m\u001b[00;38;2;180;30;30m180|030|030 \u001b[00m\u001b[00;38;2;180;30;60m180|030|060 \u001b[00m\u001b[00;38;2;180;30;90m180|030|090 \u001b[00m\u001b[00;38;2;180;30;120m180|030|120 \u001b[00m\u001b[00;38;2;180;30;150m180|030|150 \u001b[00m\u001b[00;38;2;180;30;180m180|030|180 \u001b[00m\u001b[00;38;2;180;30;210m180|030|210 \u001b[00m\u001b[00;38;2;180;30;240m180|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;180;60;0m180|060|000 \u001b[00m\u001b[00;38;2;180;60;30m180|060|030 \u001b[00m\u001b[00;38;2;180;60;60m180|060|060 \u001b[00m\u001b[00;38;2;180;60;90m180|060|090 \u001b[00m\u001b[00;38;2;180;60;120m180|060|120 \u001b[00m\u001b[00;38;2;180;60;150m180|060|150 \u001b[00m\u001b[00;38;2;180;60;180m180|060|180 \u001b[00m\u001b[00;38;2;180;60;210m180|060|210 \u001b[00m\u001b[00;38;2;180;60;240m180|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;180;90;0m180|090|000 \u001b[00m\u001b[00;38;2;180;90;30m180|090|030 \u001b[00m\u001b[00;38;2;180;90;60m180|090|060 \u001b[00m\u001b[00;38;2;180;90;90m180|090|090 \u001b[00m\u001b[00;38;2;180;90;120m180|090|120 \u001b[00m\u001b[00;38;2;180;90;150m180|090|150 \u001b[00m\u001b[00;38;2;180;90;180m180|090|180 \u001b[00m\u001b[00;38;2;180;90;210m180|090|210 \u001b[00m\u001b[00;38;2;180;90;240m180|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;180;120;0m180|120|000 \u001b[00m\u001b[00;38;2;180;120;30m180|120|030 \u001b[00m\u001b[00;38;2;180;120;60m180|120|060 \u001b[00m\u001b[00;38;2;180;120;90m180|120|090 \u001b[00m\u001b[00;38;2;180;120;120m180|120|120 \u001b[00m\u001b[00;38;2;180;120;150m180|120|150 \u001b[00m\u001b[00;38;2;180;120;180m180|120|180 \u001b[00m\u001b[00;38;2;180;120;210m180|120|210 \u001b[00m\u001b[00;38;2;180;120;240m180|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;180;150;0m180|150|000 \u001b[00m\u001b[00;38;2;180;150;30m180|150|030 \u001b[00m\u001b[00;38;2;180;150;60m180|150|060 \u001b[00m\u001b[00;38;2;180;150;90m180|150|090 \u001b[00m\u001b[00;38;2;180;150;120m180|150|120 \u001b[00m\u001b[00;38;2;180;150;150m180|150|150 \u001b[00m\u001b[00;38;2;180;150;180m180|150|180 \u001b[00m\u001b[00;38;2;180;150;210m180|150|210 \u001b[00m\u001b[00;38;2;180;150;240m180|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;180;180;0m180|180|000 \u001b[00m\u001b[00;38;2;180;180;30m180|180|030 \u001b[00m\u001b[00;38;2;180;180;60m180|180|060 \u001b[00m\u001b[00;38;2;180;180;90m180|180|090 \u001b[00m\u001b[00;38;2;180;180;120m180|180|120 \u001b[00m\u001b[00;38;2;180;180;150m180|180|150 \u001b[00m\u001b[00;38;2;180;180;180m180|180|180 \u001b[00m\u001b[00;38;2;180;180;210m180|180|210 \u001b[00m\u001b[00;38;2;180;180;240m180|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;180;210;0m180|210|000 \u001b[00m\u001b[00;38;2;180;210;30m180|210|030 \u001b[00m\u001b[00;38;2;180;210;60m180|210|060 \u001b[00m\u001b[00;38;2;180;210;90m180|210|090 \u001b[00m\u001b[00;38;2;180;210;120m180|210|120 \u001b[00m\u001b[00;38;2;180;210;150m180|210|150 \u001b[00m\u001b[00;38;2;180;210;180m180|210|180 \u001b[00m\u001b[00;38;2;180;210;210m180|210|210 \u001b[00m\u001b[00;38;2;180;210;240m180|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;180;240;0m180|240|000 \u001b[00m\u001b[00;38;2;180;240;30m180|240|030 \u001b[00m\u001b[00;38;2;180;240;60m180|240|060 \u001b[00m\u001b[00;38;2;180;240;90m180|240|090 \u001b[00m\u001b[00;38;2;180;240;120m180|240|120 \u001b[00m\u001b[00;38;2;180;240;150m180|240|150 \u001b[00m\u001b[00;38;2;180;240;180m180|240|180 \u001b[00m\u001b[00;38;2;180;240;210m180|240|210 \u001b[00m\u001b[00;38;2;180;240;240m180|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;38;2;210;0;0m210|000|000 \u001b[00m\u001b[00;38;2;210;0;30m210|000|030 \u001b[00m\u001b[00;38;2;210;0;60m210|000|060 \u001b[00m\u001b[00;38;2;210;0;90m210|000|090 \u001b[00m\u001b[00;38;2;210;0;120m210|000|120 \u001b[00m\u001b[00;38;2;210;0;150m210|000|150 \u001b[00m\u001b[00;38;2;210;0;180m210|000|180 \u001b[00m\u001b[00;38;2;210;0;210m210|000|210 \u001b[00m\u001b[00;38;2;210;0;240m210|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;210;30;0m210|030|000 \u001b[00m\u001b[00;38;2;210;30;30m210|030|030 \u001b[00m\u001b[00;38;2;210;30;60m210|030|060 \u001b[00m\u001b[00;38;2;210;30;90m210|030|090 \u001b[00m\u001b[00;38;2;210;30;120m210|030|120 \u001b[00m\u001b[00;38;2;210;30;150m210|030|150 \u001b[00m\u001b[00;38;2;210;30;180m210|030|180 \u001b[00m\u001b[00;38;2;210;30;210m210|030|210 \u001b[00m\u001b[00;38;2;210;30;240m210|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;210;60;0m210|060|000 \u001b[00m\u001b[00;38;2;210;60;30m210|060|030 \u001b[00m\u001b[00;38;2;210;60;60m210|060|060 \u001b[00m\u001b[00;38;2;210;60;90m210|060|090 \u001b[00m\u001b[00;38;2;210;60;120m210|060|120 \u001b[00m\u001b[00;38;2;210;60;150m210|060|150 \u001b[00m\u001b[00;38;2;210;60;180m210|060|180 \u001b[00m\u001b[00;38;2;210;60;210m210|060|210 \u001b[00m\u001b[00;38;2;210;60;240m210|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;210;90;0m210|090|000 \u001b[00m\u001b[00;38;2;210;90;30m210|090|030 \u001b[00m\u001b[00;38;2;210;90;60m210|090|060 \u001b[00m\u001b[00;38;2;210;90;90m210|090|090 \u001b[00m\u001b[00;38;2;210;90;120m210|090|120 \u001b[00m\u001b[00;38;2;210;90;150m210|090|150 \u001b[00m\u001b[00;38;2;210;90;180m210|090|180 \u001b[00m\u001b[00;38;2;210;90;210m210|090|210 \u001b[00m\u001b[00;38;2;210;90;240m210|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;210;120;0m210|120|000 \u001b[00m\u001b[00;38;2;210;120;30m210|120|030 \u001b[00m\u001b[00;38;2;210;120;60m210|120|060 \u001b[00m\u001b[00;38;2;210;120;90m210|120|090 \u001b[00m\u001b[00;38;2;210;120;120m210|120|120 \u001b[00m\u001b[00;38;2;210;120;150m210|120|150 \u001b[00m\u001b[00;38;2;210;120;180m210|120|180 \u001b[00m\u001b[00;38;2;210;120;210m210|120|210 \u001b[00m\u001b[00;38;2;210;120;240m210|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;210;150;0m210|150|000 \u001b[00m\u001b[00;38;2;210;150;30m210|150|030 \u001b[00m\u001b[00;38;2;210;150;60m210|150|060 \u001b[00m\u001b[00;38;2;210;150;90m210|150|090 \u001b[00m\u001b[00;38;2;210;150;120m210|150|120 \u001b[00m\u001b[00;38;2;210;150;150m210|150|150 \u001b[00m\u001b[00;38;2;210;150;180m210|150|180 \u001b[00m\u001b[00;38;2;210;150;210m210|150|210 \u001b[00m\u001b[00;38;2;210;150;240m210|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;210;180;0m210|180|000 \u001b[00m\u001b[00;38;2;210;180;30m210|180|030 \u001b[00m\u001b[00;38;2;210;180;60m210|180|060 \u001b[00m\u001b[00;38;2;210;180;90m210|180|090 \u001b[00m\u001b[00;38;2;210;180;120m210|180|120 \u001b[00m\u001b[00;38;2;210;180;150m210|180|150 \u001b[00m\u001b[00;38;2;210;180;180m210|180|180 \u001b[00m\u001b[00;38;2;210;180;210m210|180|210 \u001b[00m\u001b[00;38;2;210;180;240m210|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;210;210;0m210|210|000 \u001b[00m\u001b[00;38;2;210;210;30m210|210|030 \u001b[00m\u001b[00;38;2;210;210;60m210|210|060 \u001b[00m\u001b[00;38;2;210;210;90m210|210|090 \u001b[00m\u001b[00;38;2;210;210;120m210|210|120 \u001b[00m\u001b[00;38;2;210;210;150m210|210|150 \u001b[00m\u001b[00;38;2;210;210;180m210|210|180 \u001b[00m\u001b[00;38;2;210;210;210m210|210|210 \u001b[00m\u001b[00;38;2;210;210;240m210|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;210;240;0m210|240|000 \u001b[00m\u001b[00;38;2;210;240;30m210|240|030 \u001b[00m\u001b[00;38;2;210;240;60m210|240|060 \u001b[00m\u001b[00;38;2;210;240;90m210|240|090 \u001b[00m\u001b[00;38;2;210;240;120m210|240|120 \u001b[00m\u001b[00;38;2;210;240;150m210|240|150 \u001b[00m\u001b[00;38;2;210;240;180m210|240|180 \u001b[00m\u001b[00;38;2;210;240;210m210|240|210 \u001b[00m\u001b[00;38;2;210;240;240m210|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;38;2;240;0;0m240|000|000 \u001b[00m\u001b[00;38;2;240;0;30m240|000|030 \u001b[00m\u001b[00;38;2;240;0;60m240|000|060 \u001b[00m\u001b[00;38;2;240;0;90m240|000|090 \u001b[00m\u001b[00;38;2;240;0;120m240|000|120 \u001b[00m\u001b[00;38;2;240;0;150m240|000|150 \u001b[00m\u001b[00;38;2;240;0;180m240|000|180 \u001b[00m\u001b[00;38;2;240;0;210m240|000|210 \u001b[00m\u001b[00;38;2;240;0;240m240|000|240 \u001b[00m\n",
+ "\u001b[00;38;2;240;30;0m240|030|000 \u001b[00m\u001b[00;38;2;240;30;30m240|030|030 \u001b[00m\u001b[00;38;2;240;30;60m240|030|060 \u001b[00m\u001b[00;38;2;240;30;90m240|030|090 \u001b[00m\u001b[00;38;2;240;30;120m240|030|120 \u001b[00m\u001b[00;38;2;240;30;150m240|030|150 \u001b[00m\u001b[00;38;2;240;30;180m240|030|180 \u001b[00m\u001b[00;38;2;240;30;210m240|030|210 \u001b[00m\u001b[00;38;2;240;30;240m240|030|240 \u001b[00m\n",
+ "\u001b[00;38;2;240;60;0m240|060|000 \u001b[00m\u001b[00;38;2;240;60;30m240|060|030 \u001b[00m\u001b[00;38;2;240;60;60m240|060|060 \u001b[00m\u001b[00;38;2;240;60;90m240|060|090 \u001b[00m\u001b[00;38;2;240;60;120m240|060|120 \u001b[00m\u001b[00;38;2;240;60;150m240|060|150 \u001b[00m\u001b[00;38;2;240;60;180m240|060|180 \u001b[00m\u001b[00;38;2;240;60;210m240|060|210 \u001b[00m\u001b[00;38;2;240;60;240m240|060|240 \u001b[00m\n",
+ "\u001b[00;38;2;240;90;0m240|090|000 \u001b[00m\u001b[00;38;2;240;90;30m240|090|030 \u001b[00m\u001b[00;38;2;240;90;60m240|090|060 \u001b[00m\u001b[00;38;2;240;90;90m240|090|090 \u001b[00m\u001b[00;38;2;240;90;120m240|090|120 \u001b[00m\u001b[00;38;2;240;90;150m240|090|150 \u001b[00m\u001b[00;38;2;240;90;180m240|090|180 \u001b[00m\u001b[00;38;2;240;90;210m240|090|210 \u001b[00m\u001b[00;38;2;240;90;240m240|090|240 \u001b[00m\n",
+ "\u001b[00;38;2;240;120;0m240|120|000 \u001b[00m\u001b[00;38;2;240;120;30m240|120|030 \u001b[00m\u001b[00;38;2;240;120;60m240|120|060 \u001b[00m\u001b[00;38;2;240;120;90m240|120|090 \u001b[00m\u001b[00;38;2;240;120;120m240|120|120 \u001b[00m\u001b[00;38;2;240;120;150m240|120|150 \u001b[00m\u001b[00;38;2;240;120;180m240|120|180 \u001b[00m\u001b[00;38;2;240;120;210m240|120|210 \u001b[00m\u001b[00;38;2;240;120;240m240|120|240 \u001b[00m\n",
+ "\u001b[00;38;2;240;150;0m240|150|000 \u001b[00m\u001b[00;38;2;240;150;30m240|150|030 \u001b[00m\u001b[00;38;2;240;150;60m240|150|060 \u001b[00m\u001b[00;38;2;240;150;90m240|150|090 \u001b[00m\u001b[00;38;2;240;150;120m240|150|120 \u001b[00m\u001b[00;38;2;240;150;150m240|150|150 \u001b[00m\u001b[00;38;2;240;150;180m240|150|180 \u001b[00m\u001b[00;38;2;240;150;210m240|150|210 \u001b[00m\u001b[00;38;2;240;150;240m240|150|240 \u001b[00m\n",
+ "\u001b[00;38;2;240;180;0m240|180|000 \u001b[00m\u001b[00;38;2;240;180;30m240|180|030 \u001b[00m\u001b[00;38;2;240;180;60m240|180|060 \u001b[00m\u001b[00;38;2;240;180;90m240|180|090 \u001b[00m\u001b[00;38;2;240;180;120m240|180|120 \u001b[00m\u001b[00;38;2;240;180;150m240|180|150 \u001b[00m\u001b[00;38;2;240;180;180m240|180|180 \u001b[00m\u001b[00;38;2;240;180;210m240|180|210 \u001b[00m\u001b[00;38;2;240;180;240m240|180|240 \u001b[00m\n",
+ "\u001b[00;38;2;240;210;0m240|210|000 \u001b[00m\u001b[00;38;2;240;210;30m240|210|030 \u001b[00m\u001b[00;38;2;240;210;60m240|210|060 \u001b[00m\u001b[00;38;2;240;210;90m240|210|090 \u001b[00m\u001b[00;38;2;240;210;120m240|210|120 \u001b[00m\u001b[00;38;2;240;210;150m240|210|150 \u001b[00m\u001b[00;38;2;240;210;180m240|210|180 \u001b[00m\u001b[00;38;2;240;210;210m240|210|210 \u001b[00m\u001b[00;38;2;240;210;240m240|210|240 \u001b[00m\n",
+ "\u001b[00;38;2;240;240;0m240|240|000 \u001b[00m\u001b[00;38;2;240;240;30m240|240|030 \u001b[00m\u001b[00;38;2;240;240;60m240|240|060 \u001b[00m\u001b[00;38;2;240;240;90m240|240|090 \u001b[00m\u001b[00;38;2;240;240;120m240|240|120 \u001b[00m\u001b[00;38;2;240;240;150m240|240|150 \u001b[00m\u001b[00;38;2;240;240;180m240|240|180 \u001b[00m\u001b[00;38;2;240;240;210m240|240|210 \u001b[00m\u001b[00;38;2;240;240;240m240|240|240 \u001b[00m\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "steps = range(0,256,30)\n",
+ "\n",
+ "t = \"{ESC}00;38;2;{r};{g};{b}m{r:03}|{g:03}|{b:03} {RESET}\"\n",
+ "for r in steps:\n",
+ " for g in steps:\n",
+ " for b in steps:\n",
+ " print (t.format(**locals()), end='')\n",
+ " print()\n",
+ " print()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 24-bit RGB background"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 47,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[00;48;2;0;0;0m000|000|000 \u001b[00m\u001b[00;48;2;0;0;30m000|000|030 \u001b[00m\u001b[00;48;2;0;0;60m000|000|060 \u001b[00m\u001b[00;48;2;0;0;90m000|000|090 \u001b[00m\u001b[00;48;2;0;0;120m000|000|120 \u001b[00m\u001b[00;48;2;0;0;150m000|000|150 \u001b[00m\u001b[00;48;2;0;0;180m000|000|180 \u001b[00m\u001b[00;48;2;0;0;210m000|000|210 \u001b[00m\u001b[00;48;2;0;0;240m000|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;0;30;0m000|030|000 \u001b[00m\u001b[00;48;2;0;30;30m000|030|030 \u001b[00m\u001b[00;48;2;0;30;60m000|030|060 \u001b[00m\u001b[00;48;2;0;30;90m000|030|090 \u001b[00m\u001b[00;48;2;0;30;120m000|030|120 \u001b[00m\u001b[00;48;2;0;30;150m000|030|150 \u001b[00m\u001b[00;48;2;0;30;180m000|030|180 \u001b[00m\u001b[00;48;2;0;30;210m000|030|210 \u001b[00m\u001b[00;48;2;0;30;240m000|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;0;60;0m000|060|000 \u001b[00m\u001b[00;48;2;0;60;30m000|060|030 \u001b[00m\u001b[00;48;2;0;60;60m000|060|060 \u001b[00m\u001b[00;48;2;0;60;90m000|060|090 \u001b[00m\u001b[00;48;2;0;60;120m000|060|120 \u001b[00m\u001b[00;48;2;0;60;150m000|060|150 \u001b[00m\u001b[00;48;2;0;60;180m000|060|180 \u001b[00m\u001b[00;48;2;0;60;210m000|060|210 \u001b[00m\u001b[00;48;2;0;60;240m000|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;0;90;0m000|090|000 \u001b[00m\u001b[00;48;2;0;90;30m000|090|030 \u001b[00m\u001b[00;48;2;0;90;60m000|090|060 \u001b[00m\u001b[00;48;2;0;90;90m000|090|090 \u001b[00m\u001b[00;48;2;0;90;120m000|090|120 \u001b[00m\u001b[00;48;2;0;90;150m000|090|150 \u001b[00m\u001b[00;48;2;0;90;180m000|090|180 \u001b[00m\u001b[00;48;2;0;90;210m000|090|210 \u001b[00m\u001b[00;48;2;0;90;240m000|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;0;120;0m000|120|000 \u001b[00m\u001b[00;48;2;0;120;30m000|120|030 \u001b[00m\u001b[00;48;2;0;120;60m000|120|060 \u001b[00m\u001b[00;48;2;0;120;90m000|120|090 \u001b[00m\u001b[00;48;2;0;120;120m000|120|120 \u001b[00m\u001b[00;48;2;0;120;150m000|120|150 \u001b[00m\u001b[00;48;2;0;120;180m000|120|180 \u001b[00m\u001b[00;48;2;0;120;210m000|120|210 \u001b[00m\u001b[00;48;2;0;120;240m000|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;0;150;0m000|150|000 \u001b[00m\u001b[00;48;2;0;150;30m000|150|030 \u001b[00m\u001b[00;48;2;0;150;60m000|150|060 \u001b[00m\u001b[00;48;2;0;150;90m000|150|090 \u001b[00m\u001b[00;48;2;0;150;120m000|150|120 \u001b[00m\u001b[00;48;2;0;150;150m000|150|150 \u001b[00m\u001b[00;48;2;0;150;180m000|150|180 \u001b[00m\u001b[00;48;2;0;150;210m000|150|210 \u001b[00m\u001b[00;48;2;0;150;240m000|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;0;180;0m000|180|000 \u001b[00m\u001b[00;48;2;0;180;30m000|180|030 \u001b[00m\u001b[00;48;2;0;180;60m000|180|060 \u001b[00m\u001b[00;48;2;0;180;90m000|180|090 \u001b[00m\u001b[00;48;2;0;180;120m000|180|120 \u001b[00m\u001b[00;48;2;0;180;150m000|180|150 \u001b[00m\u001b[00;48;2;0;180;180m000|180|180 \u001b[00m\u001b[00;48;2;0;180;210m000|180|210 \u001b[00m\u001b[00;48;2;0;180;240m000|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;0;210;0m000|210|000 \u001b[00m\u001b[00;48;2;0;210;30m000|210|030 \u001b[00m\u001b[00;48;2;0;210;60m000|210|060 \u001b[00m\u001b[00;48;2;0;210;90m000|210|090 \u001b[00m\u001b[00;48;2;0;210;120m000|210|120 \u001b[00m\u001b[00;48;2;0;210;150m000|210|150 \u001b[00m\u001b[00;48;2;0;210;180m000|210|180 \u001b[00m\u001b[00;48;2;0;210;210m000|210|210 \u001b[00m\u001b[00;48;2;0;210;240m000|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;0;240;0m000|240|000 \u001b[00m\u001b[00;48;2;0;240;30m000|240|030 \u001b[00m\u001b[00;48;2;0;240;60m000|240|060 \u001b[00m\u001b[00;48;2;0;240;90m000|240|090 \u001b[00m\u001b[00;48;2;0;240;120m000|240|120 \u001b[00m\u001b[00;48;2;0;240;150m000|240|150 \u001b[00m\u001b[00;48;2;0;240;180m000|240|180 \u001b[00m\u001b[00;48;2;0;240;210m000|240|210 \u001b[00m\u001b[00;48;2;0;240;240m000|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;48;2;30;0;0m030|000|000 \u001b[00m\u001b[00;48;2;30;0;30m030|000|030 \u001b[00m\u001b[00;48;2;30;0;60m030|000|060 \u001b[00m\u001b[00;48;2;30;0;90m030|000|090 \u001b[00m\u001b[00;48;2;30;0;120m030|000|120 \u001b[00m\u001b[00;48;2;30;0;150m030|000|150 \u001b[00m\u001b[00;48;2;30;0;180m030|000|180 \u001b[00m\u001b[00;48;2;30;0;210m030|000|210 \u001b[00m\u001b[00;48;2;30;0;240m030|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;30;30;0m030|030|000 \u001b[00m\u001b[00;48;2;30;30;30m030|030|030 \u001b[00m\u001b[00;48;2;30;30;60m030|030|060 \u001b[00m\u001b[00;48;2;30;30;90m030|030|090 \u001b[00m\u001b[00;48;2;30;30;120m030|030|120 \u001b[00m\u001b[00;48;2;30;30;150m030|030|150 \u001b[00m\u001b[00;48;2;30;30;180m030|030|180 \u001b[00m\u001b[00;48;2;30;30;210m030|030|210 \u001b[00m\u001b[00;48;2;30;30;240m030|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;30;60;0m030|060|000 \u001b[00m\u001b[00;48;2;30;60;30m030|060|030 \u001b[00m\u001b[00;48;2;30;60;60m030|060|060 \u001b[00m\u001b[00;48;2;30;60;90m030|060|090 \u001b[00m\u001b[00;48;2;30;60;120m030|060|120 \u001b[00m\u001b[00;48;2;30;60;150m030|060|150 \u001b[00m\u001b[00;48;2;30;60;180m030|060|180 \u001b[00m\u001b[00;48;2;30;60;210m030|060|210 \u001b[00m\u001b[00;48;2;30;60;240m030|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;30;90;0m030|090|000 \u001b[00m\u001b[00;48;2;30;90;30m030|090|030 \u001b[00m\u001b[00;48;2;30;90;60m030|090|060 \u001b[00m\u001b[00;48;2;30;90;90m030|090|090 \u001b[00m\u001b[00;48;2;30;90;120m030|090|120 \u001b[00m\u001b[00;48;2;30;90;150m030|090|150 \u001b[00m\u001b[00;48;2;30;90;180m030|090|180 \u001b[00m\u001b[00;48;2;30;90;210m030|090|210 \u001b[00m\u001b[00;48;2;30;90;240m030|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;30;120;0m030|120|000 \u001b[00m\u001b[00;48;2;30;120;30m030|120|030 \u001b[00m\u001b[00;48;2;30;120;60m030|120|060 \u001b[00m\u001b[00;48;2;30;120;90m030|120|090 \u001b[00m\u001b[00;48;2;30;120;120m030|120|120 \u001b[00m\u001b[00;48;2;30;120;150m030|120|150 \u001b[00m\u001b[00;48;2;30;120;180m030|120|180 \u001b[00m\u001b[00;48;2;30;120;210m030|120|210 \u001b[00m\u001b[00;48;2;30;120;240m030|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;30;150;0m030|150|000 \u001b[00m\u001b[00;48;2;30;150;30m030|150|030 \u001b[00m\u001b[00;48;2;30;150;60m030|150|060 \u001b[00m\u001b[00;48;2;30;150;90m030|150|090 \u001b[00m\u001b[00;48;2;30;150;120m030|150|120 \u001b[00m\u001b[00;48;2;30;150;150m030|150|150 \u001b[00m\u001b[00;48;2;30;150;180m030|150|180 \u001b[00m\u001b[00;48;2;30;150;210m030|150|210 \u001b[00m\u001b[00;48;2;30;150;240m030|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;30;180;0m030|180|000 \u001b[00m\u001b[00;48;2;30;180;30m030|180|030 \u001b[00m\u001b[00;48;2;30;180;60m030|180|060 \u001b[00m\u001b[00;48;2;30;180;90m030|180|090 \u001b[00m\u001b[00;48;2;30;180;120m030|180|120 \u001b[00m\u001b[00;48;2;30;180;150m030|180|150 \u001b[00m\u001b[00;48;2;30;180;180m030|180|180 \u001b[00m\u001b[00;48;2;30;180;210m030|180|210 \u001b[00m\u001b[00;48;2;30;180;240m030|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;30;210;0m030|210|000 \u001b[00m\u001b[00;48;2;30;210;30m030|210|030 \u001b[00m\u001b[00;48;2;30;210;60m030|210|060 \u001b[00m\u001b[00;48;2;30;210;90m030|210|090 \u001b[00m\u001b[00;48;2;30;210;120m030|210|120 \u001b[00m\u001b[00;48;2;30;210;150m030|210|150 \u001b[00m\u001b[00;48;2;30;210;180m030|210|180 \u001b[00m\u001b[00;48;2;30;210;210m030|210|210 \u001b[00m\u001b[00;48;2;30;210;240m030|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;30;240;0m030|240|000 \u001b[00m\u001b[00;48;2;30;240;30m030|240|030 \u001b[00m\u001b[00;48;2;30;240;60m030|240|060 \u001b[00m\u001b[00;48;2;30;240;90m030|240|090 \u001b[00m\u001b[00;48;2;30;240;120m030|240|120 \u001b[00m\u001b[00;48;2;30;240;150m030|240|150 \u001b[00m\u001b[00;48;2;30;240;180m030|240|180 \u001b[00m\u001b[00;48;2;30;240;210m030|240|210 \u001b[00m\u001b[00;48;2;30;240;240m030|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;48;2;60;0;0m060|000|000 \u001b[00m\u001b[00;48;2;60;0;30m060|000|030 \u001b[00m\u001b[00;48;2;60;0;60m060|000|060 \u001b[00m\u001b[00;48;2;60;0;90m060|000|090 \u001b[00m\u001b[00;48;2;60;0;120m060|000|120 \u001b[00m\u001b[00;48;2;60;0;150m060|000|150 \u001b[00m\u001b[00;48;2;60;0;180m060|000|180 \u001b[00m\u001b[00;48;2;60;0;210m060|000|210 \u001b[00m\u001b[00;48;2;60;0;240m060|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;60;30;0m060|030|000 \u001b[00m\u001b[00;48;2;60;30;30m060|030|030 \u001b[00m\u001b[00;48;2;60;30;60m060|030|060 \u001b[00m\u001b[00;48;2;60;30;90m060|030|090 \u001b[00m\u001b[00;48;2;60;30;120m060|030|120 \u001b[00m\u001b[00;48;2;60;30;150m060|030|150 \u001b[00m\u001b[00;48;2;60;30;180m060|030|180 \u001b[00m\u001b[00;48;2;60;30;210m060|030|210 \u001b[00m\u001b[00;48;2;60;30;240m060|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;60;60;0m060|060|000 \u001b[00m\u001b[00;48;2;60;60;30m060|060|030 \u001b[00m\u001b[00;48;2;60;60;60m060|060|060 \u001b[00m\u001b[00;48;2;60;60;90m060|060|090 \u001b[00m\u001b[00;48;2;60;60;120m060|060|120 \u001b[00m\u001b[00;48;2;60;60;150m060|060|150 \u001b[00m\u001b[00;48;2;60;60;180m060|060|180 \u001b[00m\u001b[00;48;2;60;60;210m060|060|210 \u001b[00m\u001b[00;48;2;60;60;240m060|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;60;90;0m060|090|000 \u001b[00m\u001b[00;48;2;60;90;30m060|090|030 \u001b[00m\u001b[00;48;2;60;90;60m060|090|060 \u001b[00m\u001b[00;48;2;60;90;90m060|090|090 \u001b[00m\u001b[00;48;2;60;90;120m060|090|120 \u001b[00m\u001b[00;48;2;60;90;150m060|090|150 \u001b[00m\u001b[00;48;2;60;90;180m060|090|180 \u001b[00m\u001b[00;48;2;60;90;210m060|090|210 \u001b[00m\u001b[00;48;2;60;90;240m060|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;60;120;0m060|120|000 \u001b[00m\u001b[00;48;2;60;120;30m060|120|030 \u001b[00m\u001b[00;48;2;60;120;60m060|120|060 \u001b[00m\u001b[00;48;2;60;120;90m060|120|090 \u001b[00m\u001b[00;48;2;60;120;120m060|120|120 \u001b[00m\u001b[00;48;2;60;120;150m060|120|150 \u001b[00m\u001b[00;48;2;60;120;180m060|120|180 \u001b[00m\u001b[00;48;2;60;120;210m060|120|210 \u001b[00m\u001b[00;48;2;60;120;240m060|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;60;150;0m060|150|000 \u001b[00m\u001b[00;48;2;60;150;30m060|150|030 \u001b[00m\u001b[00;48;2;60;150;60m060|150|060 \u001b[00m\u001b[00;48;2;60;150;90m060|150|090 \u001b[00m\u001b[00;48;2;60;150;120m060|150|120 \u001b[00m\u001b[00;48;2;60;150;150m060|150|150 \u001b[00m\u001b[00;48;2;60;150;180m060|150|180 \u001b[00m\u001b[00;48;2;60;150;210m060|150|210 \u001b[00m\u001b[00;48;2;60;150;240m060|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;60;180;0m060|180|000 \u001b[00m\u001b[00;48;2;60;180;30m060|180|030 \u001b[00m\u001b[00;48;2;60;180;60m060|180|060 \u001b[00m\u001b[00;48;2;60;180;90m060|180|090 \u001b[00m\u001b[00;48;2;60;180;120m060|180|120 \u001b[00m\u001b[00;48;2;60;180;150m060|180|150 \u001b[00m\u001b[00;48;2;60;180;180m060|180|180 \u001b[00m\u001b[00;48;2;60;180;210m060|180|210 \u001b[00m\u001b[00;48;2;60;180;240m060|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;60;210;0m060|210|000 \u001b[00m\u001b[00;48;2;60;210;30m060|210|030 \u001b[00m\u001b[00;48;2;60;210;60m060|210|060 \u001b[00m\u001b[00;48;2;60;210;90m060|210|090 \u001b[00m\u001b[00;48;2;60;210;120m060|210|120 \u001b[00m\u001b[00;48;2;60;210;150m060|210|150 \u001b[00m\u001b[00;48;2;60;210;180m060|210|180 \u001b[00m\u001b[00;48;2;60;210;210m060|210|210 \u001b[00m\u001b[00;48;2;60;210;240m060|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;60;240;0m060|240|000 \u001b[00m\u001b[00;48;2;60;240;30m060|240|030 \u001b[00m\u001b[00;48;2;60;240;60m060|240|060 \u001b[00m\u001b[00;48;2;60;240;90m060|240|090 \u001b[00m\u001b[00;48;2;60;240;120m060|240|120 \u001b[00m\u001b[00;48;2;60;240;150m060|240|150 \u001b[00m\u001b[00;48;2;60;240;180m060|240|180 \u001b[00m\u001b[00;48;2;60;240;210m060|240|210 \u001b[00m\u001b[00;48;2;60;240;240m060|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;48;2;90;0;0m090|000|000 \u001b[00m\u001b[00;48;2;90;0;30m090|000|030 \u001b[00m\u001b[00;48;2;90;0;60m090|000|060 \u001b[00m\u001b[00;48;2;90;0;90m090|000|090 \u001b[00m\u001b[00;48;2;90;0;120m090|000|120 \u001b[00m\u001b[00;48;2;90;0;150m090|000|150 \u001b[00m\u001b[00;48;2;90;0;180m090|000|180 \u001b[00m\u001b[00;48;2;90;0;210m090|000|210 \u001b[00m\u001b[00;48;2;90;0;240m090|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;90;30;0m090|030|000 \u001b[00m\u001b[00;48;2;90;30;30m090|030|030 \u001b[00m\u001b[00;48;2;90;30;60m090|030|060 \u001b[00m\u001b[00;48;2;90;30;90m090|030|090 \u001b[00m\u001b[00;48;2;90;30;120m090|030|120 \u001b[00m\u001b[00;48;2;90;30;150m090|030|150 \u001b[00m\u001b[00;48;2;90;30;180m090|030|180 \u001b[00m\u001b[00;48;2;90;30;210m090|030|210 \u001b[00m\u001b[00;48;2;90;30;240m090|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;90;60;0m090|060|000 \u001b[00m\u001b[00;48;2;90;60;30m090|060|030 \u001b[00m\u001b[00;48;2;90;60;60m090|060|060 \u001b[00m\u001b[00;48;2;90;60;90m090|060|090 \u001b[00m\u001b[00;48;2;90;60;120m090|060|120 \u001b[00m\u001b[00;48;2;90;60;150m090|060|150 \u001b[00m\u001b[00;48;2;90;60;180m090|060|180 \u001b[00m\u001b[00;48;2;90;60;210m090|060|210 \u001b[00m\u001b[00;48;2;90;60;240m090|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;90;90;0m090|090|000 \u001b[00m\u001b[00;48;2;90;90;30m090|090|030 \u001b[00m\u001b[00;48;2;90;90;60m090|090|060 \u001b[00m\u001b[00;48;2;90;90;90m090|090|090 \u001b[00m\u001b[00;48;2;90;90;120m090|090|120 \u001b[00m\u001b[00;48;2;90;90;150m090|090|150 \u001b[00m\u001b[00;48;2;90;90;180m090|090|180 \u001b[00m\u001b[00;48;2;90;90;210m090|090|210 \u001b[00m\u001b[00;48;2;90;90;240m090|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;90;120;0m090|120|000 \u001b[00m\u001b[00;48;2;90;120;30m090|120|030 \u001b[00m\u001b[00;48;2;90;120;60m090|120|060 \u001b[00m\u001b[00;48;2;90;120;90m090|120|090 \u001b[00m\u001b[00;48;2;90;120;120m090|120|120 \u001b[00m\u001b[00;48;2;90;120;150m090|120|150 \u001b[00m\u001b[00;48;2;90;120;180m090|120|180 \u001b[00m\u001b[00;48;2;90;120;210m090|120|210 \u001b[00m\u001b[00;48;2;90;120;240m090|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;90;150;0m090|150|000 \u001b[00m\u001b[00;48;2;90;150;30m090|150|030 \u001b[00m\u001b[00;48;2;90;150;60m090|150|060 \u001b[00m\u001b[00;48;2;90;150;90m090|150|090 \u001b[00m\u001b[00;48;2;90;150;120m090|150|120 \u001b[00m\u001b[00;48;2;90;150;150m090|150|150 \u001b[00m\u001b[00;48;2;90;150;180m090|150|180 \u001b[00m\u001b[00;48;2;90;150;210m090|150|210 \u001b[00m\u001b[00;48;2;90;150;240m090|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;90;180;0m090|180|000 \u001b[00m\u001b[00;48;2;90;180;30m090|180|030 \u001b[00m\u001b[00;48;2;90;180;60m090|180|060 \u001b[00m\u001b[00;48;2;90;180;90m090|180|090 \u001b[00m\u001b[00;48;2;90;180;120m090|180|120 \u001b[00m\u001b[00;48;2;90;180;150m090|180|150 \u001b[00m\u001b[00;48;2;90;180;180m090|180|180 \u001b[00m\u001b[00;48;2;90;180;210m090|180|210 \u001b[00m\u001b[00;48;2;90;180;240m090|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;90;210;0m090|210|000 \u001b[00m\u001b[00;48;2;90;210;30m090|210|030 \u001b[00m\u001b[00;48;2;90;210;60m090|210|060 \u001b[00m\u001b[00;48;2;90;210;90m090|210|090 \u001b[00m\u001b[00;48;2;90;210;120m090|210|120 \u001b[00m\u001b[00;48;2;90;210;150m090|210|150 \u001b[00m\u001b[00;48;2;90;210;180m090|210|180 \u001b[00m\u001b[00;48;2;90;210;210m090|210|210 \u001b[00m\u001b[00;48;2;90;210;240m090|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;90;240;0m090|240|000 \u001b[00m\u001b[00;48;2;90;240;30m090|240|030 \u001b[00m\u001b[00;48;2;90;240;60m090|240|060 \u001b[00m\u001b[00;48;2;90;240;90m090|240|090 \u001b[00m\u001b[00;48;2;90;240;120m090|240|120 \u001b[00m\u001b[00;48;2;90;240;150m090|240|150 \u001b[00m\u001b[00;48;2;90;240;180m090|240|180 \u001b[00m\u001b[00;48;2;90;240;210m090|240|210 \u001b[00m\u001b[00;48;2;90;240;240m090|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;48;2;120;0;0m120|000|000 \u001b[00m\u001b[00;48;2;120;0;30m120|000|030 \u001b[00m\u001b[00;48;2;120;0;60m120|000|060 \u001b[00m\u001b[00;48;2;120;0;90m120|000|090 \u001b[00m\u001b[00;48;2;120;0;120m120|000|120 \u001b[00m\u001b[00;48;2;120;0;150m120|000|150 \u001b[00m\u001b[00;48;2;120;0;180m120|000|180 \u001b[00m\u001b[00;48;2;120;0;210m120|000|210 \u001b[00m\u001b[00;48;2;120;0;240m120|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;120;30;0m120|030|000 \u001b[00m\u001b[00;48;2;120;30;30m120|030|030 \u001b[00m\u001b[00;48;2;120;30;60m120|030|060 \u001b[00m\u001b[00;48;2;120;30;90m120|030|090 \u001b[00m\u001b[00;48;2;120;30;120m120|030|120 \u001b[00m\u001b[00;48;2;120;30;150m120|030|150 \u001b[00m\u001b[00;48;2;120;30;180m120|030|180 \u001b[00m\u001b[00;48;2;120;30;210m120|030|210 \u001b[00m\u001b[00;48;2;120;30;240m120|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;120;60;0m120|060|000 \u001b[00m\u001b[00;48;2;120;60;30m120|060|030 \u001b[00m\u001b[00;48;2;120;60;60m120|060|060 \u001b[00m\u001b[00;48;2;120;60;90m120|060|090 \u001b[00m\u001b[00;48;2;120;60;120m120|060|120 \u001b[00m\u001b[00;48;2;120;60;150m120|060|150 \u001b[00m\u001b[00;48;2;120;60;180m120|060|180 \u001b[00m\u001b[00;48;2;120;60;210m120|060|210 \u001b[00m\u001b[00;48;2;120;60;240m120|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;120;90;0m120|090|000 \u001b[00m\u001b[00;48;2;120;90;30m120|090|030 \u001b[00m\u001b[00;48;2;120;90;60m120|090|060 \u001b[00m\u001b[00;48;2;120;90;90m120|090|090 \u001b[00m\u001b[00;48;2;120;90;120m120|090|120 \u001b[00m\u001b[00;48;2;120;90;150m120|090|150 \u001b[00m\u001b[00;48;2;120;90;180m120|090|180 \u001b[00m\u001b[00;48;2;120;90;210m120|090|210 \u001b[00m\u001b[00;48;2;120;90;240m120|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;120;120;0m120|120|000 \u001b[00m\u001b[00;48;2;120;120;30m120|120|030 \u001b[00m\u001b[00;48;2;120;120;60m120|120|060 \u001b[00m\u001b[00;48;2;120;120;90m120|120|090 \u001b[00m\u001b[00;48;2;120;120;120m120|120|120 \u001b[00m\u001b[00;48;2;120;120;150m120|120|150 \u001b[00m\u001b[00;48;2;120;120;180m120|120|180 \u001b[00m\u001b[00;48;2;120;120;210m120|120|210 \u001b[00m\u001b[00;48;2;120;120;240m120|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;120;150;0m120|150|000 \u001b[00m\u001b[00;48;2;120;150;30m120|150|030 \u001b[00m\u001b[00;48;2;120;150;60m120|150|060 \u001b[00m\u001b[00;48;2;120;150;90m120|150|090 \u001b[00m\u001b[00;48;2;120;150;120m120|150|120 \u001b[00m\u001b[00;48;2;120;150;150m120|150|150 \u001b[00m\u001b[00;48;2;120;150;180m120|150|180 \u001b[00m\u001b[00;48;2;120;150;210m120|150|210 \u001b[00m\u001b[00;48;2;120;150;240m120|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;120;180;0m120|180|000 \u001b[00m\u001b[00;48;2;120;180;30m120|180|030 \u001b[00m\u001b[00;48;2;120;180;60m120|180|060 \u001b[00m\u001b[00;48;2;120;180;90m120|180|090 \u001b[00m\u001b[00;48;2;120;180;120m120|180|120 \u001b[00m\u001b[00;48;2;120;180;150m120|180|150 \u001b[00m\u001b[00;48;2;120;180;180m120|180|180 \u001b[00m\u001b[00;48;2;120;180;210m120|180|210 \u001b[00m\u001b[00;48;2;120;180;240m120|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;120;210;0m120|210|000 \u001b[00m\u001b[00;48;2;120;210;30m120|210|030 \u001b[00m\u001b[00;48;2;120;210;60m120|210|060 \u001b[00m\u001b[00;48;2;120;210;90m120|210|090 \u001b[00m\u001b[00;48;2;120;210;120m120|210|120 \u001b[00m\u001b[00;48;2;120;210;150m120|210|150 \u001b[00m\u001b[00;48;2;120;210;180m120|210|180 \u001b[00m\u001b[00;48;2;120;210;210m120|210|210 \u001b[00m\u001b[00;48;2;120;210;240m120|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;120;240;0m120|240|000 \u001b[00m\u001b[00;48;2;120;240;30m120|240|030 \u001b[00m\u001b[00;48;2;120;240;60m120|240|060 \u001b[00m\u001b[00;48;2;120;240;90m120|240|090 \u001b[00m\u001b[00;48;2;120;240;120m120|240|120 \u001b[00m\u001b[00;48;2;120;240;150m120|240|150 \u001b[00m\u001b[00;48;2;120;240;180m120|240|180 \u001b[00m\u001b[00;48;2;120;240;210m120|240|210 \u001b[00m\u001b[00;48;2;120;240;240m120|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;48;2;150;0;0m150|000|000 \u001b[00m\u001b[00;48;2;150;0;30m150|000|030 \u001b[00m\u001b[00;48;2;150;0;60m150|000|060 \u001b[00m\u001b[00;48;2;150;0;90m150|000|090 \u001b[00m\u001b[00;48;2;150;0;120m150|000|120 \u001b[00m\u001b[00;48;2;150;0;150m150|000|150 \u001b[00m\u001b[00;48;2;150;0;180m150|000|180 \u001b[00m\u001b[00;48;2;150;0;210m150|000|210 \u001b[00m\u001b[00;48;2;150;0;240m150|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;150;30;0m150|030|000 \u001b[00m\u001b[00;48;2;150;30;30m150|030|030 \u001b[00m\u001b[00;48;2;150;30;60m150|030|060 \u001b[00m\u001b[00;48;2;150;30;90m150|030|090 \u001b[00m\u001b[00;48;2;150;30;120m150|030|120 \u001b[00m\u001b[00;48;2;150;30;150m150|030|150 \u001b[00m\u001b[00;48;2;150;30;180m150|030|180 \u001b[00m\u001b[00;48;2;150;30;210m150|030|210 \u001b[00m\u001b[00;48;2;150;30;240m150|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;150;60;0m150|060|000 \u001b[00m\u001b[00;48;2;150;60;30m150|060|030 \u001b[00m\u001b[00;48;2;150;60;60m150|060|060 \u001b[00m\u001b[00;48;2;150;60;90m150|060|090 \u001b[00m\u001b[00;48;2;150;60;120m150|060|120 \u001b[00m\u001b[00;48;2;150;60;150m150|060|150 \u001b[00m\u001b[00;48;2;150;60;180m150|060|180 \u001b[00m\u001b[00;48;2;150;60;210m150|060|210 \u001b[00m\u001b[00;48;2;150;60;240m150|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;150;90;0m150|090|000 \u001b[00m\u001b[00;48;2;150;90;30m150|090|030 \u001b[00m\u001b[00;48;2;150;90;60m150|090|060 \u001b[00m\u001b[00;48;2;150;90;90m150|090|090 \u001b[00m\u001b[00;48;2;150;90;120m150|090|120 \u001b[00m\u001b[00;48;2;150;90;150m150|090|150 \u001b[00m\u001b[00;48;2;150;90;180m150|090|180 \u001b[00m\u001b[00;48;2;150;90;210m150|090|210 \u001b[00m\u001b[00;48;2;150;90;240m150|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;150;120;0m150|120|000 \u001b[00m\u001b[00;48;2;150;120;30m150|120|030 \u001b[00m\u001b[00;48;2;150;120;60m150|120|060 \u001b[00m\u001b[00;48;2;150;120;90m150|120|090 \u001b[00m\u001b[00;48;2;150;120;120m150|120|120 \u001b[00m\u001b[00;48;2;150;120;150m150|120|150 \u001b[00m\u001b[00;48;2;150;120;180m150|120|180 \u001b[00m\u001b[00;48;2;150;120;210m150|120|210 \u001b[00m\u001b[00;48;2;150;120;240m150|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;150;150;0m150|150|000 \u001b[00m\u001b[00;48;2;150;150;30m150|150|030 \u001b[00m\u001b[00;48;2;150;150;60m150|150|060 \u001b[00m\u001b[00;48;2;150;150;90m150|150|090 \u001b[00m\u001b[00;48;2;150;150;120m150|150|120 \u001b[00m\u001b[00;48;2;150;150;150m150|150|150 \u001b[00m\u001b[00;48;2;150;150;180m150|150|180 \u001b[00m\u001b[00;48;2;150;150;210m150|150|210 \u001b[00m\u001b[00;48;2;150;150;240m150|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;150;180;0m150|180|000 \u001b[00m\u001b[00;48;2;150;180;30m150|180|030 \u001b[00m\u001b[00;48;2;150;180;60m150|180|060 \u001b[00m\u001b[00;48;2;150;180;90m150|180|090 \u001b[00m\u001b[00;48;2;150;180;120m150|180|120 \u001b[00m\u001b[00;48;2;150;180;150m150|180|150 \u001b[00m\u001b[00;48;2;150;180;180m150|180|180 \u001b[00m\u001b[00;48;2;150;180;210m150|180|210 \u001b[00m\u001b[00;48;2;150;180;240m150|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;150;210;0m150|210|000 \u001b[00m\u001b[00;48;2;150;210;30m150|210|030 \u001b[00m\u001b[00;48;2;150;210;60m150|210|060 \u001b[00m\u001b[00;48;2;150;210;90m150|210|090 \u001b[00m\u001b[00;48;2;150;210;120m150|210|120 \u001b[00m\u001b[00;48;2;150;210;150m150|210|150 \u001b[00m\u001b[00;48;2;150;210;180m150|210|180 \u001b[00m\u001b[00;48;2;150;210;210m150|210|210 \u001b[00m\u001b[00;48;2;150;210;240m150|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;150;240;0m150|240|000 \u001b[00m\u001b[00;48;2;150;240;30m150|240|030 \u001b[00m\u001b[00;48;2;150;240;60m150|240|060 \u001b[00m\u001b[00;48;2;150;240;90m150|240|090 \u001b[00m\u001b[00;48;2;150;240;120m150|240|120 \u001b[00m\u001b[00;48;2;150;240;150m150|240|150 \u001b[00m\u001b[00;48;2;150;240;180m150|240|180 \u001b[00m\u001b[00;48;2;150;240;210m150|240|210 \u001b[00m\u001b[00;48;2;150;240;240m150|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;48;2;180;0;0m180|000|000 \u001b[00m\u001b[00;48;2;180;0;30m180|000|030 \u001b[00m\u001b[00;48;2;180;0;60m180|000|060 \u001b[00m\u001b[00;48;2;180;0;90m180|000|090 \u001b[00m\u001b[00;48;2;180;0;120m180|000|120 \u001b[00m\u001b[00;48;2;180;0;150m180|000|150 \u001b[00m\u001b[00;48;2;180;0;180m180|000|180 \u001b[00m\u001b[00;48;2;180;0;210m180|000|210 \u001b[00m\u001b[00;48;2;180;0;240m180|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;180;30;0m180|030|000 \u001b[00m\u001b[00;48;2;180;30;30m180|030|030 \u001b[00m\u001b[00;48;2;180;30;60m180|030|060 \u001b[00m\u001b[00;48;2;180;30;90m180|030|090 \u001b[00m\u001b[00;48;2;180;30;120m180|030|120 \u001b[00m\u001b[00;48;2;180;30;150m180|030|150 \u001b[00m\u001b[00;48;2;180;30;180m180|030|180 \u001b[00m\u001b[00;48;2;180;30;210m180|030|210 \u001b[00m\u001b[00;48;2;180;30;240m180|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;180;60;0m180|060|000 \u001b[00m\u001b[00;48;2;180;60;30m180|060|030 \u001b[00m\u001b[00;48;2;180;60;60m180|060|060 \u001b[00m\u001b[00;48;2;180;60;90m180|060|090 \u001b[00m\u001b[00;48;2;180;60;120m180|060|120 \u001b[00m\u001b[00;48;2;180;60;150m180|060|150 \u001b[00m\u001b[00;48;2;180;60;180m180|060|180 \u001b[00m\u001b[00;48;2;180;60;210m180|060|210 \u001b[00m\u001b[00;48;2;180;60;240m180|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;180;90;0m180|090|000 \u001b[00m\u001b[00;48;2;180;90;30m180|090|030 \u001b[00m\u001b[00;48;2;180;90;60m180|090|060 \u001b[00m\u001b[00;48;2;180;90;90m180|090|090 \u001b[00m\u001b[00;48;2;180;90;120m180|090|120 \u001b[00m\u001b[00;48;2;180;90;150m180|090|150 \u001b[00m\u001b[00;48;2;180;90;180m180|090|180 \u001b[00m\u001b[00;48;2;180;90;210m180|090|210 \u001b[00m\u001b[00;48;2;180;90;240m180|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;180;120;0m180|120|000 \u001b[00m\u001b[00;48;2;180;120;30m180|120|030 \u001b[00m\u001b[00;48;2;180;120;60m180|120|060 \u001b[00m\u001b[00;48;2;180;120;90m180|120|090 \u001b[00m\u001b[00;48;2;180;120;120m180|120|120 \u001b[00m\u001b[00;48;2;180;120;150m180|120|150 \u001b[00m\u001b[00;48;2;180;120;180m180|120|180 \u001b[00m\u001b[00;48;2;180;120;210m180|120|210 \u001b[00m\u001b[00;48;2;180;120;240m180|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;180;150;0m180|150|000 \u001b[00m\u001b[00;48;2;180;150;30m180|150|030 \u001b[00m\u001b[00;48;2;180;150;60m180|150|060 \u001b[00m\u001b[00;48;2;180;150;90m180|150|090 \u001b[00m\u001b[00;48;2;180;150;120m180|150|120 \u001b[00m\u001b[00;48;2;180;150;150m180|150|150 \u001b[00m\u001b[00;48;2;180;150;180m180|150|180 \u001b[00m\u001b[00;48;2;180;150;210m180|150|210 \u001b[00m\u001b[00;48;2;180;150;240m180|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;180;180;0m180|180|000 \u001b[00m\u001b[00;48;2;180;180;30m180|180|030 \u001b[00m\u001b[00;48;2;180;180;60m180|180|060 \u001b[00m\u001b[00;48;2;180;180;90m180|180|090 \u001b[00m\u001b[00;48;2;180;180;120m180|180|120 \u001b[00m\u001b[00;48;2;180;180;150m180|180|150 \u001b[00m\u001b[00;48;2;180;180;180m180|180|180 \u001b[00m\u001b[00;48;2;180;180;210m180|180|210 \u001b[00m\u001b[00;48;2;180;180;240m180|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;180;210;0m180|210|000 \u001b[00m\u001b[00;48;2;180;210;30m180|210|030 \u001b[00m\u001b[00;48;2;180;210;60m180|210|060 \u001b[00m\u001b[00;48;2;180;210;90m180|210|090 \u001b[00m\u001b[00;48;2;180;210;120m180|210|120 \u001b[00m\u001b[00;48;2;180;210;150m180|210|150 \u001b[00m\u001b[00;48;2;180;210;180m180|210|180 \u001b[00m\u001b[00;48;2;180;210;210m180|210|210 \u001b[00m\u001b[00;48;2;180;210;240m180|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;180;240;0m180|240|000 \u001b[00m\u001b[00;48;2;180;240;30m180|240|030 \u001b[00m\u001b[00;48;2;180;240;60m180|240|060 \u001b[00m\u001b[00;48;2;180;240;90m180|240|090 \u001b[00m\u001b[00;48;2;180;240;120m180|240|120 \u001b[00m\u001b[00;48;2;180;240;150m180|240|150 \u001b[00m\u001b[00;48;2;180;240;180m180|240|180 \u001b[00m\u001b[00;48;2;180;240;210m180|240|210 \u001b[00m\u001b[00;48;2;180;240;240m180|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;48;2;210;0;0m210|000|000 \u001b[00m\u001b[00;48;2;210;0;30m210|000|030 \u001b[00m\u001b[00;48;2;210;0;60m210|000|060 \u001b[00m\u001b[00;48;2;210;0;90m210|000|090 \u001b[00m\u001b[00;48;2;210;0;120m210|000|120 \u001b[00m\u001b[00;48;2;210;0;150m210|000|150 \u001b[00m\u001b[00;48;2;210;0;180m210|000|180 \u001b[00m\u001b[00;48;2;210;0;210m210|000|210 \u001b[00m\u001b[00;48;2;210;0;240m210|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;210;30;0m210|030|000 \u001b[00m\u001b[00;48;2;210;30;30m210|030|030 \u001b[00m\u001b[00;48;2;210;30;60m210|030|060 \u001b[00m\u001b[00;48;2;210;30;90m210|030|090 \u001b[00m\u001b[00;48;2;210;30;120m210|030|120 \u001b[00m\u001b[00;48;2;210;30;150m210|030|150 \u001b[00m\u001b[00;48;2;210;30;180m210|030|180 \u001b[00m\u001b[00;48;2;210;30;210m210|030|210 \u001b[00m\u001b[00;48;2;210;30;240m210|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;210;60;0m210|060|000 \u001b[00m\u001b[00;48;2;210;60;30m210|060|030 \u001b[00m\u001b[00;48;2;210;60;60m210|060|060 \u001b[00m\u001b[00;48;2;210;60;90m210|060|090 \u001b[00m\u001b[00;48;2;210;60;120m210|060|120 \u001b[00m\u001b[00;48;2;210;60;150m210|060|150 \u001b[00m\u001b[00;48;2;210;60;180m210|060|180 \u001b[00m\u001b[00;48;2;210;60;210m210|060|210 \u001b[00m\u001b[00;48;2;210;60;240m210|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;210;90;0m210|090|000 \u001b[00m\u001b[00;48;2;210;90;30m210|090|030 \u001b[00m\u001b[00;48;2;210;90;60m210|090|060 \u001b[00m\u001b[00;48;2;210;90;90m210|090|090 \u001b[00m\u001b[00;48;2;210;90;120m210|090|120 \u001b[00m\u001b[00;48;2;210;90;150m210|090|150 \u001b[00m\u001b[00;48;2;210;90;180m210|090|180 \u001b[00m\u001b[00;48;2;210;90;210m210|090|210 \u001b[00m\u001b[00;48;2;210;90;240m210|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;210;120;0m210|120|000 \u001b[00m\u001b[00;48;2;210;120;30m210|120|030 \u001b[00m\u001b[00;48;2;210;120;60m210|120|060 \u001b[00m\u001b[00;48;2;210;120;90m210|120|090 \u001b[00m\u001b[00;48;2;210;120;120m210|120|120 \u001b[00m\u001b[00;48;2;210;120;150m210|120|150 \u001b[00m\u001b[00;48;2;210;120;180m210|120|180 \u001b[00m\u001b[00;48;2;210;120;210m210|120|210 \u001b[00m\u001b[00;48;2;210;120;240m210|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;210;150;0m210|150|000 \u001b[00m\u001b[00;48;2;210;150;30m210|150|030 \u001b[00m\u001b[00;48;2;210;150;60m210|150|060 \u001b[00m\u001b[00;48;2;210;150;90m210|150|090 \u001b[00m\u001b[00;48;2;210;150;120m210|150|120 \u001b[00m\u001b[00;48;2;210;150;150m210|150|150 \u001b[00m\u001b[00;48;2;210;150;180m210|150|180 \u001b[00m\u001b[00;48;2;210;150;210m210|150|210 \u001b[00m\u001b[00;48;2;210;150;240m210|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;210;180;0m210|180|000 \u001b[00m\u001b[00;48;2;210;180;30m210|180|030 \u001b[00m\u001b[00;48;2;210;180;60m210|180|060 \u001b[00m\u001b[00;48;2;210;180;90m210|180|090 \u001b[00m\u001b[00;48;2;210;180;120m210|180|120 \u001b[00m\u001b[00;48;2;210;180;150m210|180|150 \u001b[00m\u001b[00;48;2;210;180;180m210|180|180 \u001b[00m\u001b[00;48;2;210;180;210m210|180|210 \u001b[00m\u001b[00;48;2;210;180;240m210|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;210;210;0m210|210|000 \u001b[00m\u001b[00;48;2;210;210;30m210|210|030 \u001b[00m\u001b[00;48;2;210;210;60m210|210|060 \u001b[00m\u001b[00;48;2;210;210;90m210|210|090 \u001b[00m\u001b[00;48;2;210;210;120m210|210|120 \u001b[00m\u001b[00;48;2;210;210;150m210|210|150 \u001b[00m\u001b[00;48;2;210;210;180m210|210|180 \u001b[00m\u001b[00;48;2;210;210;210m210|210|210 \u001b[00m\u001b[00;48;2;210;210;240m210|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;210;240;0m210|240|000 \u001b[00m\u001b[00;48;2;210;240;30m210|240|030 \u001b[00m\u001b[00;48;2;210;240;60m210|240|060 \u001b[00m\u001b[00;48;2;210;240;90m210|240|090 \u001b[00m\u001b[00;48;2;210;240;120m210|240|120 \u001b[00m\u001b[00;48;2;210;240;150m210|240|150 \u001b[00m\u001b[00;48;2;210;240;180m210|240|180 \u001b[00m\u001b[00;48;2;210;240;210m210|240|210 \u001b[00m\u001b[00;48;2;210;240;240m210|240|240 \u001b[00m\n",
+ "\n",
+ "\u001b[00;48;2;240;0;0m240|000|000 \u001b[00m\u001b[00;48;2;240;0;30m240|000|030 \u001b[00m\u001b[00;48;2;240;0;60m240|000|060 \u001b[00m\u001b[00;48;2;240;0;90m240|000|090 \u001b[00m\u001b[00;48;2;240;0;120m240|000|120 \u001b[00m\u001b[00;48;2;240;0;150m240|000|150 \u001b[00m\u001b[00;48;2;240;0;180m240|000|180 \u001b[00m\u001b[00;48;2;240;0;210m240|000|210 \u001b[00m\u001b[00;48;2;240;0;240m240|000|240 \u001b[00m\n",
+ "\u001b[00;48;2;240;30;0m240|030|000 \u001b[00m\u001b[00;48;2;240;30;30m240|030|030 \u001b[00m\u001b[00;48;2;240;30;60m240|030|060 \u001b[00m\u001b[00;48;2;240;30;90m240|030|090 \u001b[00m\u001b[00;48;2;240;30;120m240|030|120 \u001b[00m\u001b[00;48;2;240;30;150m240|030|150 \u001b[00m\u001b[00;48;2;240;30;180m240|030|180 \u001b[00m\u001b[00;48;2;240;30;210m240|030|210 \u001b[00m\u001b[00;48;2;240;30;240m240|030|240 \u001b[00m\n",
+ "\u001b[00;48;2;240;60;0m240|060|000 \u001b[00m\u001b[00;48;2;240;60;30m240|060|030 \u001b[00m\u001b[00;48;2;240;60;60m240|060|060 \u001b[00m\u001b[00;48;2;240;60;90m240|060|090 \u001b[00m\u001b[00;48;2;240;60;120m240|060|120 \u001b[00m\u001b[00;48;2;240;60;150m240|060|150 \u001b[00m\u001b[00;48;2;240;60;180m240|060|180 \u001b[00m\u001b[00;48;2;240;60;210m240|060|210 \u001b[00m\u001b[00;48;2;240;60;240m240|060|240 \u001b[00m\n",
+ "\u001b[00;48;2;240;90;0m240|090|000 \u001b[00m\u001b[00;48;2;240;90;30m240|090|030 \u001b[00m\u001b[00;48;2;240;90;60m240|090|060 \u001b[00m\u001b[00;48;2;240;90;90m240|090|090 \u001b[00m\u001b[00;48;2;240;90;120m240|090|120 \u001b[00m\u001b[00;48;2;240;90;150m240|090|150 \u001b[00m\u001b[00;48;2;240;90;180m240|090|180 \u001b[00m\u001b[00;48;2;240;90;210m240|090|210 \u001b[00m\u001b[00;48;2;240;90;240m240|090|240 \u001b[00m\n",
+ "\u001b[00;48;2;240;120;0m240|120|000 \u001b[00m\u001b[00;48;2;240;120;30m240|120|030 \u001b[00m\u001b[00;48;2;240;120;60m240|120|060 \u001b[00m\u001b[00;48;2;240;120;90m240|120|090 \u001b[00m\u001b[00;48;2;240;120;120m240|120|120 \u001b[00m\u001b[00;48;2;240;120;150m240|120|150 \u001b[00m\u001b[00;48;2;240;120;180m240|120|180 \u001b[00m\u001b[00;48;2;240;120;210m240|120|210 \u001b[00m\u001b[00;48;2;240;120;240m240|120|240 \u001b[00m\n",
+ "\u001b[00;48;2;240;150;0m240|150|000 \u001b[00m\u001b[00;48;2;240;150;30m240|150|030 \u001b[00m\u001b[00;48;2;240;150;60m240|150|060 \u001b[00m\u001b[00;48;2;240;150;90m240|150|090 \u001b[00m\u001b[00;48;2;240;150;120m240|150|120 \u001b[00m\u001b[00;48;2;240;150;150m240|150|150 \u001b[00m\u001b[00;48;2;240;150;180m240|150|180 \u001b[00m\u001b[00;48;2;240;150;210m240|150|210 \u001b[00m\u001b[00;48;2;240;150;240m240|150|240 \u001b[00m\n",
+ "\u001b[00;48;2;240;180;0m240|180|000 \u001b[00m\u001b[00;48;2;240;180;30m240|180|030 \u001b[00m\u001b[00;48;2;240;180;60m240|180|060 \u001b[00m\u001b[00;48;2;240;180;90m240|180|090 \u001b[00m\u001b[00;48;2;240;180;120m240|180|120 \u001b[00m\u001b[00;48;2;240;180;150m240|180|150 \u001b[00m\u001b[00;48;2;240;180;180m240|180|180 \u001b[00m\u001b[00;48;2;240;180;210m240|180|210 \u001b[00m\u001b[00;48;2;240;180;240m240|180|240 \u001b[00m\n",
+ "\u001b[00;48;2;240;210;0m240|210|000 \u001b[00m\u001b[00;48;2;240;210;30m240|210|030 \u001b[00m\u001b[00;48;2;240;210;60m240|210|060 \u001b[00m\u001b[00;48;2;240;210;90m240|210|090 \u001b[00m\u001b[00;48;2;240;210;120m240|210|120 \u001b[00m\u001b[00;48;2;240;210;150m240|210|150 \u001b[00m\u001b[00;48;2;240;210;180m240|210|180 \u001b[00m\u001b[00;48;2;240;210;210m240|210|210 \u001b[00m\u001b[00;48;2;240;210;240m240|210|240 \u001b[00m\n",
+ "\u001b[00;48;2;240;240;0m240|240|000 \u001b[00m\u001b[00;48;2;240;240;30m240|240|030 \u001b[00m\u001b[00;48;2;240;240;60m240|240|060 \u001b[00m\u001b[00;48;2;240;240;90m240|240|090 \u001b[00m\u001b[00;48;2;240;240;120m240|240|120 \u001b[00m\u001b[00;48;2;240;240;150m240|240|150 \u001b[00m\u001b[00;48;2;240;240;180m240|240|180 \u001b[00m\u001b[00;48;2;240;240;210m240|240|210 \u001b[00m\u001b[00;48;2;240;240;240m240|240|240 \u001b[00m\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "steps = range(0,256,30)\n",
+ "\n",
+ "t = \"{ESC}00;48;2;{r};{g};{b}m{r:03}|{g:03}|{b:03} {RESET}\"\n",
+ "for r in steps:\n",
+ " for g in steps:\n",
+ " for b in steps:\n",
+ " print (t.format(**locals()), end='')\n",
+ " print()\n",
+ " print()\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/tools/tests/CSS Reference.ipynb b/tools/tests/CSS Reference.ipynb
new file mode 100644
index 0000000..b1df3f9
--- /dev/null
+++ b/tools/tests/CSS Reference.ipynb
@@ -0,0 +1,8193 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "<center><h1> CSS Playground </h1></center>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A notebook that contain most of the things that could be displayed, to test CSS, feel free to add things to it, and send modification"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Title first level"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Title second Level"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Title third level"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### h4"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "##### h5"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "###### h6"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# h1\n",
+ "## h2\n",
+ "### h3\n",
+ "#### h4\n",
+ "##### h6\n",
+ "\n",
+ "This is just a sample paragraph\n",
+ "\n",
+ "> With a blockquote\n",
+ "\n",
+ " def some_code():\n",
+ " return 'by indenting'\n",
+ "\n",
+ "```\n",
+ "def some_other_code():\n",
+ " return 'bewtween_backticks'\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can look at different level of nested unorderd list \n",
+ "\n",
+ "- level 1\n",
+ " - level 2\n",
+ " - level 2\n",
+ " - level 2\n",
+ " - level 3\n",
+ " - level 3\n",
+ " - level 4\n",
+ " - level 5\n",
+ " - level 6\n",
+ " - level 2\n",
+ "\n",
+ "- level 1\n",
+ "- level 1\n",
+ "- level 1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Ordered list \n",
+ "\n",
+ "1. level 1\n",
+ " 2. level 1\n",
+ " 3. level 1\n",
+ " 4. level 1\n",
+ " 1. level 1\n",
+ " 2. level 1\n",
+ " 2. level 1\n",
+ " 3. level 1\n",
+ " 4. level 1\n",
+ " 1. level 1\n",
+ " 2. level 1\n",
+ "3. level 1\n",
+ "4. level 1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "some Horizontal line\n",
+ "\n",
+ "***\n",
+ "\n",
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## copy past from Daring Fireball"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "link : \n",
+ "This is [an example](http://example.com/ \"Title\") inline link.\n",
+ "\n",
+ "[This link](http://example.net/) has no title attribute."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "inline Html\n",
+ "This is a regular paragraph.\n",
+ "\n",
+ "<table>\n",
+ " <tr>\n",
+ " <td>Foo</td>\n",
+ " </tr>\n",
+ "</table>\n",
+ "\n",
+ "This is another regular paragraph."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,\n",
+ "> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.\n",
+ "> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.\n",
+ "> \n",
+ "> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse\n",
+ "> id sem consectetuer libero luctus adipiscing.\n",
+ "\n",
+ "---\n",
+ "\n",
+ "> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,\n",
+ "consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.\n",
+ "Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.\n",
+ "\n",
+ "> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse\n",
+ "id sem consectetuer libero luctus adipiscing."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "> This is the first level of quoting.\n",
+ ">\n",
+ "> > This is nested blockquote.\n",
+ ">\n",
+ "> Back to the first level."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "> ## This is a header.\n",
+ "> \n",
+ "> 1. This is the first list item.\n",
+ "> 2. This is the second list item.\n",
+ "> \n",
+ "> Here's some example code:\n",
+ "> \n",
+ "> return shell_exec(\"echo $input | $markdown_script\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "1. This is a list item with two paragraphs. Lorem ipsum dolor\n",
+ " sit amet, consectetuer adipiscing elit. Aliquam hendrerit\n",
+ " mi posuere lectus.\n",
+ "\n",
+ " Vestibulum enim wisi, viverra nec, fringilla in, laoreet\n",
+ " vitae, risus. Donec sit amet nisl. Aliquam semper ipsum\n",
+ " sit amet velit.\n",
+ "\n",
+ "2. Suspendisse id sem consectetuer libero luctus adipiscing."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "* This is a list item with two paragraphs.\n",
+ "\n",
+ " This is the second paragraph in the list item. You're\n",
+ "only required to indent the first line. Lorem ipsum dolor\n",
+ "sit amet, consectetuer adipiscing elit.\n",
+ "\n",
+ "* Another item in the same list."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "* A list item with a blockquote:\n",
+ "\n",
+ " > This is a blockquote\n",
+ " > inside a list item."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "* A list item with a code block:\n",
+ "\n",
+ " <code goes here>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "1986. What a great season.\n",
+ "\n",
+ "1986\\. What a great season."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "See my [About](/about/) page for details. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "ref link\n",
+ "This is [an example][id] reference-style link.\n",
+ "\n",
+ "[id]: http://example.com/ \"Optional Title Here\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "*single asterisks*\n",
+ "\n",
+ "_single underscores_\n",
+ "\n",
+ "**double asterisks**\n",
+ "\n",
+ "__double underscores__\n",
+ "\n",
+ "un*frigging*believable // should render partially as bold\n",
+ "\n",
+ "\\*this text is surrounded by literal asterisks\\*\n",
+ "\n",
+ "``There is a literal backtick (`) here.``"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Other Notebook element"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A small tooltip\n",
+ "\n",
+ "<div id=\"tooltip_p\" class=\"ipython_tooltip\" style='position:relative'><div class=\"tooltipbuttons\"><a href=\"#\" role=\"button\" class=\"ui-button\"><span class=\"ui-icon ui-icon-close\">Close</span></a><a href=\"#\" class=\"ui-corner-all\" role=\"button\" id=\"expanbutton\" title=\"Grow the tooltip vertically (press tab 2 times)\" style=\"\"><span class=\"ui-icon ui-icon-plus\">Expand</span></a><a href=\"#\" role=\"button\" class=\"ui-button\" title=\"show the current docstring in pager (press tab 4 times)\"><span class=\"ui-icon ui-icon-arrowstop-l-n\">Open in Pager</span></a><a href=\"#\" role=\"button\" class=\"ui-button\" title=\"Tootip is not dismissed while typing for 10 seconds\" style=\"display: none;\"><span class=\"ui-icon ui-icon-clock\">Close</span></a></div><div class=\"pretooltiparrow\"></div><div class=\"tooltiptext smalltooltip\">And some text inside</div></div>"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ "<div id=\"tooltip_p\" class=\"ipython_tooltip\" style='position:relative'>\n",
+ " <div class=\"tooltipbuttons\">\n",
+ " <a href=\"#\" role=\"button\" class=\"ui-button\">\n",
+ " <span class=\"ui-icon ui-icon-close\">Close</span>\n",
+ " </a>\n",
+ " <a href=\"#\" class=\"ui-corner-all\" role=\"button\" id=\"expanbutton\" title=\"Grow the tooltip vertically (press tab 2 times)\" style=\"\">\n",
+ " <span class=\"ui-icon ui-icon-plus\">Expand</span>\n",
+ " </a>\n",
+ " <a href=\"#\" role=\"button\" class=\"ui-button\" title=\"show the current docstring in pager (press tab 4 times)\">\n",
+ " <span class=\"ui-icon ui-icon-arrowstop-l-n\">Open in Pager</span></a>\n",
+ " <a href=\"#\" role=\"button\" class=\"ui-button\" title=\"Tootip is not dismissed while typing for 10 seconds\" style=\"display: none;\">\n",
+ " <span class=\"ui-icon ui-icon-clock\">Close</span>\n",
+ " </a>\n",
+ " </div>\n",
+ " <div class=\"pretooltiparrow\">\n",
+ " </div>\n",
+ " <div class=\"tooltiptext bigtooltip\">This one should be big</div>\n",
+ "</div>"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": [
+ "iVBORw0KGgoAAAANSUhEUgAABDEAAAJXCAYAAACKd8/wAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\n",
+ "AAALEgAACxIB0t1+/AAAIABJREFUeJzsvXm0ZUV1Bv5VnemO773ufj23gB0QBAFbUBORiEaFxgmX\n",
+ "MvgDASE/ZZmAicbg0iCYYFDR5S+QmCUosQFBooBGHICYgKDBGRUkoARkapqe3njvmarq90fVrqpz\n",
+ "3+vu180TGjjfWrfv6zuec+45u/b+9rf3ZkophRo1atSoUaNGjRo1atSoUaNGjd0c/OnegBo1atSo\n",
+ "UaNGjRo1atSoUaNGjbmgJjFq1KhRo0aNGjVq1KhRo0aNGs8I1CRGjRo1atSoUaNGjRo1atSoUeMZ\n",
+ "gZrEqFGjRo0aNWrUqFGjRo0aNWo8I1CTGDVq1KhRo0aNGjVq1KhRo0aNZwRqEqNGjRo1atSoUaNG\n",
+ "jRo1atSo8YzAM5LEOPXUU3HOOef8Qb/jiCOOwBe/+MV5/9wHH3wQnHNIKef0+ltuuQXPe97z5n07\n",
+ "atSo8eRR26IaNWrsDqhtUY0aNXYH1LaoxlOFZySJwRgDY2yn3lMUBd7+9rfj+c9/PjjnuPXWW+f9\n",
+ "O55JuOqqq7Dnnnui0+ngrW99K7Zu3brN17761a/GkiVLMDQ0hBe+8IW49NJLK89//OMfx5577onh\n",
+ "4WG84x3vwOTkpH3ub/7mb/CCF7zAvveKK66ovFcIgb/7u7/DypUrMTQ0hJe85CUYHx8HAGRZhr/+\n",
+ "67/GypUrsXDhQvzFX/wFyrK0773nnnvwmte8BiMjI9hnn33w9a9/fT4OTY0ac0Zti5485mqLHnro\n",
+ "IXS73cqNc47Pfvaz9jWXXHIJ9t57bwwPD+OlL30pfvCDH9jnTj31VCRJYt87NDQEpRQAYPPmzTjs\n",
+ "sMMwOjqK4eFhrFmzpmJPdmSLHnnkEbzpTW/CokWLsHz5cpx55pkQQsz3oapRY5uobdGTw+OPP443\n",
+ "v/nNWLlyJTjneOihh7b7+nPOOQcHHnggoijCxz72scpz//iP/1ixU61WC0EQYMuWLQCAiYkJnHTS\n",
+ "SVi8eDEWL16Mk046qeI33XnnnTjkkEPQbrdx6KGH4pe//KV9bke2qNPpVL47DEOcddZZ83GIatSY\n",
+ "E2pb9OSws7Zor732QqvVstf8UUcdZZ/bkS3KsgynnXYahoeHsXz58oo/ddttt83qc11//fX2vduz\n",
+ "RQDwla98BS984QvR6XSw99574/bbb5+vwwTgGUpiALDO587gT//0T3HllVdi2bJlz9qTfy64++67\n",
+ "ccYZZ+DLX/4yNmzYgFarhfe+973bfP1FF12ERx99FBMTE1i3bh3OPPNM3HvvvQCAdevW4corr8QP\n",
+ "f/hDPPbYY+j3+zjzzDPtezudDm644Qb73ve97334n//5H/v8ueeeizvuuAN33HEHJiYmcOWVV6LR\n",
+ "aAAAPvGJT+DnP/857r77btx33334+c9/jvPPPx8AUJYl3vKWt+DNb34ztm7diksuuQQnnXQSfvvb\n",
+ "3/4hDlmNGttEbYt2HTtji/bYYw9MTk7a269//WtwzvG2t70NgHb8P/CBD+CrX/0qxsfHcfrpp+Ot\n",
+ "b32r/X0YYzj77LPt+ycmJuyx73Q6uOyyy/DEE09gfHwc5513Ho477jhMTU0B2L4tAoCzzjoLo6Oj\n",
+ "WL9+Pe68807ceuut+NznPveHPHQ1asxAbYt2HZxzHH300bj22mvn9Pp99tkHF154Id7whjfMOG4f\n",
+ "/vCHK7bq7LPPxqtf/WosXLgQAHDeeedh06ZNeOCBB3D//fdjw4YNOO+88wAAeZ7jLW95C04++WSM\n",
+ "jY3hlFNOwVve8hYbHOzIFk1NTdnvffzxx9FsNnHcccfNwxGqUWPuqG3RrmNnbRFjDDfccIO97r/7\n",
+ "3e/a5+Zii+6//3489NBD+O///m986lOfwo033ggAOPzwwyvvveGGG9DpdCxJsiNbdPPNN+NDH/oQ\n",
+ "1q1bh6mpKdx2221YvXr1fB0mAM8QEuMXv/gFXvKSl2BoaAgnnHAC0jTd6c+IoghnnXUWDjvsMARB\n",
+ "sFPvvf/++/Ga17wGo6OjljUntQCgWbBPf/rTOOigg9DtdnH66adjw4YNWLt2LYaHh/G6170OY2Nj\n",
+ "lc/84he/iJUrV2LFihX4zGc+Yx/v9/s49dRTsXDhQhxwwAH4yU9+UnnfJz7xCey9994YGhrCAQcc\n",
+ "sEvqgy9/+ct485vfjFe+8pVot9v4h3/4B1x33XWYnp6e9fWUbSB0Oh0MDQ0BAL75zW/i9NNPx8qV\n",
+ "K9Fut3H22Wfjmmuusb/Reeedhxe84AUAgJe97GU4/PDDLYmxdetW/NM//RMuvfRSK8faf//9kSQJ\n",
+ "AOCGG27AmWeeiZGREYyOjuKss87CZZddBgD43//9X6xfvx5/9Vd/BcYYXv3qV+Owww6bofSoUWM+\n",
+ "Udsih6fDFvlYt24dXvWqV2GPPfYAAPzmN7/B/vvvjzVr1gAA3vnOd2LTpk144okn7Hu25VglSYJ9\n",
+ "993Xykg55xgdHUUcxwC2b4sATcYcf/zxiOMYS5cuxVFHHYW77757p49HjRpzRW2LHObDFi1ZsgRn\n",
+ "nHEGDj300Dm9/uSTT8ZRRx2Fbre73YBNKYV169bhlFNOsY/dfffdOOaYY6wvdcwxx1h7ccstt0AI\n",
+ "gfe9732IoghnnnkmlFL4r//6LwA7tkU+vva1r2Hp0qV45StfOdfDUKPGTqO2RQ5Phy0C5kYazWaL\n",
+ "Lr/8cpxzzjkYHh7Gfvvth3e/+9340pe+NOv7v/SlL+HYY49Fs9kEsGNbdO655+Lcc8/Fy172MgDA\n",
+ "8uXLsWLFijnv01yw25MYeZ7jmGOOwSmnnIKtW7fi2GOPxbXXXmtZuoceeggLFizY5u0rX/nKvGzH\n",
+ "Rz7yEaxfvx733HMPHn74YcuaA5oFu+666/C9730P9957L2644QasXbsWn/jEJ/DEE09ASomLLrqo\n",
+ "8nm33HILfve73+Gmm27CJz/5SXzve98DAHzsYx/DAw88gP/7v//DjTfeiHXr1lUYSZLjTExM4Nxz\n",
+ "z8VJJ52EDRs2AABuv/327R6LH/7whwC0s3/wwQfbz1y9ejWSJMF99923zf1/4xvfiGaziSOOOAKX\n",
+ "XXYZli9fbvfdv3iklMiybFZFRL/fx09+8hO86EUvAgD8+te/RhiG+OpXv4rly5dj3333nZG9HPzs\n",
+ "Rx55pCK79CGlxF133bXNfahR48mgtkW7hy0CtF24/PLLK4vx4YcfjgceeAA//vGPIYTAZZddhjVr\n",
+ "1mDp0qX2NZ/73OewaNEiHHroobjuuutmfO5BBx2EZrOJU089Fddff70lMeg7CYO26Mgjj8RVV12F\n",
+ "fr+PRx99FN/5znewdu3a7e5DjRq7itoWzb8t+kPhtttuw8aNG61iDND24tprr8XY2Bi2bt2Ka6+9\n",
+ "FkcffTQATXAcdNBBlc84+OCDK6ToXP2idevW4eSTT57vXapRw6K2RbuHLTrxxBOxZMkSHHnkkfjV\n",
+ "r34162sGbdHWrVuxfv36ig920EEHzZqAmZ6exrXXXlvxuYBt2yIhBH72s5/hiSeewD777IPnPe95\n",
+ "OPPMM3eJ4Nou1G6OW2+9Va1YsaLy2Cte8Qp1zjnn7PJnrlq1St16663bfc0RRxyhvvjFL8763PXX\n",
+ "X6/WrFlj/7/XXnupq666yv7/bW97m3rve99r/3/xxRerY445Riml1AMPPKAYY+ree++1z//t3/6t\n",
+ "Ov3005VSSq1evVrdeOON9rlLLrlErVq1apvb+eIXv1h94xvf2O6+DOLP/uzP1Oc///nKYytXrtzh\n",
+ "MSnLUn31q19VCxYsUL///e+VUkp94QtfUC94wQvUgw8+qMbGxtSb3vQmxRhTd9xxx4z3n3zyyWrt\n",
+ "2rX2/1/+8pcVY0z9+Z//uUrTVP3qV79SixcvVjfffLNSSqm/+7u/U4cddpjauHGjWr9+vXrZy16m\n",
+ "OOfq8ccfV3meq9WrV6tPfepTKs9zdeONN6o4jtVRRx21U8eiRo25orZFu48t+v73v686nY6anp6u\n",
+ "PP75z39ehWGowjBUixcvVj/5yU/scz//+c/Vli1blBBCffvb31bdblf94Ac/mPHZWZapiy66SK1c\n",
+ "uVJNTk4qpbZvi5RSavPmzWrNmjUqDEPFGFPvete7duo41KixM6ht0fzbIkJRFIoxZn2cHeGkk05S\n",
+ "55133jafP+2002bYgzRN1Wtf+1rFOVecc/X6179e5XmulFLq7//+79UJJ5xQef2JJ56oPvaxjyml\n",
+ "dmyLCA8++KAKgkA9+OCDc9qPGjV2BbUtevpt0Q9/+EOVpqnq9XrqggsuUMuWLVNjY2MzXjdoix56\n",
+ "6CHFGFNZltnHbrrpJrXXXnvNeO/ll1+uVq9eXXlse7bo0UcfVYwx9dKXvlQ9/vjjatOmTeqwww5T\n",
+ "H/nIR3b2MGwXu70S47HHHsPKlSsrj+255567VG+1q9iwYQNOOOEErFq1CsPDw3jnO9+JzZs3V17j\n",
+ "Z/uazWbl/41Gw9ZWE/xutnvssQfWr18PQO/v4HM+Lr/8cqxZs8Yyd3fdddeMbdkROp1ORWoFAOPj\n",
+ "4+h2u9t9XxAEePvb346Xv/zltrHLaaedhne84x044ogjcOCBB+I1r3kNAGDVqlWV937wgx/Eb37z\n",
+ "G/z7v/+7fYwkSR/96EeRJAkOPPBAnHDCCfj2t78NQDOra9aswYtf/GK88pWvxFvf+laEYYilS5ci\n",
+ "iiJ8/etfx7e+9S3bjOa4446b8b01aswXalu0+9iidevW4e1vfztarZZ97D/+4z/wmc98Bvfccw+K\n",
+ "osAVV1yBN77xjXZ/aFs551i7di1OPPHEWdUYcRzjzDPPRLfbtdmX7dkipRSOPPJIHHvssej1eti0\n",
+ "aRO2bNmCs88+e6eORY0ac0Vti+bfFv0h0Ov18LWvfW1G9vLEE0/Evvvui6mpKUxMTGD16tU46aST\n",
+ "AGibODExUXm9bxO3Z4t8XHHFFTj88MOx5557/gH3sMZzHbUtevpt0Z/8yZ8gSRI0m0186EMfwsjI\n",
+ "CG677bbKa2azRZ1OBwAq9mZb/tdsqq7t2SKK784880wsXboUixYtwvvf/34b380XdnsSY/ny5Xj0\n",
+ "0Ucrj/3+97+vSJUGu6f6t6uvvvpJb8OHP/xhBEGAu+66C+Pj47jiiit2OH5nRxew3232oYcesnVC\n",
+ "y5cvn/Ec4fe//z3e/e5341/+5V+wZcsWbN26FS960Yvsd83WSda/Uaf+Aw44oNLt+v7770ee57Z3\n",
+ "xY5QFAXa7TYALdM677zz8MADD+Chhx7C/vvvj1WrVlWM2rnnnosbb7wRN910k71oAMyQTBLot200\n",
+ "Grj44ovxyCOP4He/+x0WLlxYqRE78MADccstt2DTpk34zne+g/vvv9/WXtWoMd+obdHuYYv6/f6s\n",
+ "gcGNN96IN7zhDdh7770BaMn28uXLK42EdwZlWVo7tz1btGnTJvzsZz/DX/7lXyKKIixcuBCnnnrq\n",
+ "vC/WNWoQals0/7boyWBbTQivv/56LFq0CK961asqj3/3u9/Fe97zHjSbTbTbbbznPe+x9uKAAw6Y\n",
+ "IQf/1a9+hQMOOADAjv0iwmC5XY0afwjUtmj3skXA7PZoNlu0YMECLF++HHfeead97Je//KUt+Sc8\n",
+ "/PDDuPXWW2eQGNuzRQsWLHhKksq7PYnxile8AmEY4qKLLkJRFLjuuusqjVQGO9YP3t7xjnfY12ZZ\n",
+ "Zutx/L93hKmpKbTbbQwNDeHRRx/FhRde+KT36/zzz0e/38fdd9+NL33pSzj++OMBAMcddxwuuOAC\n",
+ "jI2N4ZFHHsHFF19s3zM9PQ3GGEZHRyGlxL/9279VekAMdpIdvB122GEAdBbgm9/8Jm6//XZMT0/j\n",
+ "nHPOwdve9jbrsPu499578Z3vfAf9fh9FUeDKK6/ET3/6U7z+9a8HoGuq7r//fiil8Jvf/AYf+MAH\n",
+ "8NGPftS+/4ILLsDVV1+Nm2++GQsWLKh89h/90R/h8MMPx8c//nHkeY577rkH11xzDd74xjcC0Izn\n",
+ "Y489BqUU7rjjDpx//vmVUWa//vWvkaYper0ePv3pT2PDhg049dRTn+QvU6PG7Kht0dNriwjXX389\n",
+ "Fi5ciCOOOKLy+MEHH4xvfetbeOCBB6CUws0334z77rvPLshf+9rXMDU1BSklbrrpJttUFAB+9KMf\n",
+ "4fbbb0ee5+j3+/jkJz+JNE3xx3/8xwC2b4tGR0exfPly/Ou//iuEEBgbG8O6desqdaY1aswnals0\n",
+ "/7YIANI0tfvv/z0byrJEmqYQQqAoCqRpOiNw2lZPioMOOgiXXnop0jRFv9/HJZdcYu3FEUccgSAI\n",
+ "cNFFFyHLMlx00UXgnFuV6478IgB2Wtyxxx47p+Neo8auorZFT68tevjhh/GDH/wAeZ4jTVNceOGF\n",
+ "dmS8j23ZopNPPhnnn38+xsbGcM899+ALX/jCjDjqiiuuwGGHHYbnP//5lcd3ZIve9a534eKLL8bG\n",
+ "jRuxdetWfPazn8Wb3vSm7R32nce8Fqf8gfDTn/5UrVmzRnW7XXX88cerE044YZfqrfbcc0/FGFOc\n",
+ "c3u/rVojv97q7rvvVocccojqdDpqzZo16jOf+Yx63vOeZ1+71157qe9973v2/yeddJKtX1RK9414\n",
+ "3etep5TS9Vacc3XppZeqFStWqGXLlqkLL7zQvrbX66mTTz5ZjYyMqAMOOEBdeOGFle/6yEc+ohYu\n",
+ "XKhGR0fV+9///u3WhW0PV111ldpjjz1Uu91WxxxzjNq6dat97owzzlBnnHGGUkqpe+65R7385S9X\n",
+ "3W5XLVy4UL3qVa9St99+u33tfffdp/bdd1/VarXUnnvuqT772c9WvocxphqNhup0OvZ2wQUX2Ocf\n",
+ "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\n",
+ "ttFi94WNRdsOG4ssLLYdNhZtO2wssrDYdthYtO2wsWjnY6eRGI7j7Ky32mL8/ve/x6OPPoo777wT\n",
+ "11xzDR544IHC/Y7jTLvtn2ybdtX2fuxjH8Pq1avx2GOPoa+vD5/61Kc2+9jptk8tdg9M5+POxqLt\n",
+ "BxuLLKY7pvNxZ2PR9oONRRbTHdP5uLOxaPvhtRaLdhqJsWDBArz44ovq9xdffLHA+uxK9PX1AQBm\n",
+ "z56Nk046CatWrcLcuXOxbt06AMDatWsxZ86cXbmJALDZbWrfty+99BIWLFiwS7Zxzpw56uT9yEc+\n",
+ "ouRI02kbLXZv2Fi07bCxyMJi22Fj0bbDxiILi22HjUXbDhuLdj52Golx2GGH4emnn8Zzzz2HOI7x\n",
+ "4x//GCeccMLOevvNotFoYGRkBABQr9dxzz334KCDDsIJJ5yAlStXAgBWrlyJE088cVduJgBsdptO\n",
+ "OOEE3HTTTYjjGKtXr8bTTz+tHHx3NtauXatu33rrrcoVdzpto8XuDRuLth02FllYbDtsLNp22Fhk\n",
+ "YbHtsLFo22Fj0S7AznQR/eUvfyle//rXi7333lt89atf3ZlvvVk8++yz4uCDDxYHH3ywOOCAA9R2\n",
+ "bdy4UbzjHe8Q++yzjzjmmGPEwMDATt2uU089VfT19YkgCMTChQvF9ddfP+E2feUrXxF777232Hff\n",
+ "fcVdd921S7bxuuuuE6effro46KCDxNKlS8X73vc+sW7dul26jRYW48HGoqnDxiILix0HG4umDhuL\n",
+ "LCx2HGwsmjpsLJoecIQYx6bUwsLCwsLCwsLCwsLCwsLCYpphp7WTWFhYWFhYWFhYWFhYWFhYWGwL\n",
+ "LIlhYWFhYWFhYWFhYWFhYWHxqoAlMSwsLCwsLCwsLCwsLCwsLF4VsCSGhYWFhYWFhYWFhYWFhYXF\n",
+ "qwKWxLCwsLCwsLCwsLCwsLCwsHhV4FVJYnz4wx/GJZdcskPfY/ny5bjuuuu2++s+99xzcF0XeZ5P\n",
+ "6fH33nsv9thjj+2+HRYWFtsOG4ssLCymA2wssrCwmA6wschiZ+FVSWI4jgPHcbboOUmS4JRTTsGe\n",
+ "e+4J13Vx3333bff3eLVg3bp1OOGEE7BgwQK4rosXXnhhwsc/+OCDOPzww9HV1YWDDz4Yv//97wv3\n",
+ "r1+/Hn//93+Pnp4ezJgxAytWrFD3ffrTn8aiRYvQ1dWFhQsX4qKLLkKapmPe48Ybb4TruoWgFEUR\n",
+ "/umf/gkLFizAjBkz8PGPf7zw3OXLl6NSqaCzsxOdnZ3Yf//9t3aXWFhsFWws2jb84he/wJFHHone\n",
+ "3l709fXhox/9KEZHRzf7+Le97W2YM2cOurq6sP/+++Paa69V9917771wXVfFg87OTnz/+98vPP9X\n",
+ "v/oVDj30UHR0dGCPPfbAT3/6U3Xfueeei/322w+e52HlypWF500WixhPP/00yuUyTj/99K3dJRYW\n",
+ "WwUbi7YNWxKLXnjhhUKc6ezshOu6+OY3vznmsWeffTZc18Wzzz6r/jY8PIwVK1Zg9uzZmD17Nlas\n",
+ "WIGRkRF1/+9+9zu84Q1vQHd3N/bee+9CnLOxyGK6w8aibcNvf/tbLF26FL29vZgxYwaOPfZYPPnk\n",
+ "k5t9/CWXXIKDDjoIQRDgy1/+8mYfN14s+slPfoI3v/nNqNVqeNvb3jbmOa7roqOjQ8W5c889V903\n",
+ "WSxasWIF+vr60NXVhb322gtf+cpXtnRXTIpXJYkBAEKILX7OUUcdhR/84AeYN2/ea/bgnwpc18W7\n",
+ "3/1u3HzzzZM+dtOmTTj++OPxmc98BkNDQ/j0pz+N448/HoODg+ox73//+zF//ny8+OKLWL9+PS6+\n",
+ "+GJ13znnnIMnn3wSw8PDWLVqFe655x5897vfLbzHwMAAvvrVr+LAAw8sfC+XX345HnnkETzxxBP4\n",
+ "y1/+gkceeQSXXXaZut9xHFxzzTUYGRnByMgI/vSnP23LbrGw2CrYWLT1GB4exhe/+EWsXbsWf/rT\n",
+ "n7BmzZpC/GjHt771LaxZswbDw8NYuXIlPvGJT+Cpp55S9y9YsEDFg5GRkcIC/sknn8Rpp52Gr33t\n",
+ "axgeHsYf/vAH/O3f/q26/5BDDsG3v/1tHHrooWO+k8liEePjH/84Dj/88N36O7XYdbCxaOuxJbFo\n",
+ "0aJFhTjzxz/+Ea7r4uSTTy487ne/+x2effbZMfv10ksvxYYNG7B69Wo888wz6O/vx6WXXgoAyLIM\n",
+ "J510Es4991wMDQ3hxz/+MS666CL88Y9/BGBjkcWrAzYWbT0OOOAA3HnnnRgYGEB/fz+WLVuGs88+\n",
+ "e7OP32efffD1r38d73nPeza73zYXi2bOnImLLroI/+///b/Nvv4f//hHFeu+853vqL9PFos++9nP\n",
+ "YvXq1RgeHsadd96Jq6++GnfddddUd8OU8KogMR599FEceuih6OrqwqmnnopWq7XFrxEEAS688EIc\n",
+ "ccQR8Dxvi577zDPP4O1vfztmzZqlWPOhoSF1/+LFi/GNb3wDS5cuRWdnJ8455xz09/fjXe96F7q7\n",
+ "u3HMMccUkn4AuO6667BgwQLMnz8fV111lfp7s9nEhz/8YcyYMQMHHHAA/ud//qfwvMsvvxxLlixB\n",
+ "V1cXDjjgANx2221bvC/mzJmD888/H4cddtikj33wwQcxb948nHzyyXAcB6eddhpmz56NW265BQBw\n",
+ "zz334KWXXsKVV16Jzs5OeJ6Hgw8+WD1/3333RUdHBwAZ1FzXRV9fX+E9PvvZz+If//EfMXPmzELg\n",
+ "u+OOO/CJT3wCPT09mDVrFi688EJcf/31heduTaC0sNha2FiksT1i0Yc+9CEce+yxKJfL6OnpwUc/\n",
+ "+tExSi8TXG1gdHR0oKura0rvddlll+H888/HO9/5Triui97eXuy1117q/gsuuABvf/vbUS6Xxzx3\n",
+ "KrHopptuQm9vL97xjnfYuGSxw2FjkcauiEUmVq5cibe+9a1YtGiR+luaprjwwgtx9dVXj4kHTzzx\n",
+ "BE488UQVv0488UQ88cQTAID+/n5s3LhREbCHHXYY9t9/f1WJtbHIYrrBxiKN7ZWjLViwAACQ5/m4\n",
+ "eZOJM844A8cddxw6OzvHPd8nikXveMc7cMopp0z4+ptrrZksFh1wwAGF9ZTv+5gzZ85m32drMO1J\n",
+ "jDiOceKJJ+LMM8/EwMAAPvCBD+Dmm29WbNILL7yA3t7ezf7cdNNN22U7Pv/5zyuG/sUXX1SsOSAV\n",
+ "Abfccgt+/etf46mnnsIdd9yBd73rXbj88svxyiuvIM9zfOtb3yq83r333ou//vWvuOeee3DFFVfg\n",
+ "17/+NQDgy1/+MlavXo1nn30Wd999N1auXFlgzpYsWYLf/e53GB4expe+9CWsWLEC/f39ACTTNtG+\n",
+ "ePDBB7fLvsjzXF1wH3roIey7774488wzMWvWLBx++OG4//77C4+//PLL0dnZiT322APvfe978b73\n",
+ "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\n",
+ "glqthvPOO0+RFA8++CAWLlyIY445BgDw+te/Hu95z3tw5513ArCxyGJ6wcaiHZ+jJUkC13VRKpUm\n",
+ "3hEY25I/WSza3PPGQzsJsqU52mTrta3BtCcx3vzmN8P3fXzrW99CkiS45ZZbCkYq7S7R7T8f+tCH\n",
+ "1GOjKFKGM+btyTA6OoparYauri6sWbMGX//617f5c1122WVoNpt44okncMMNN+CDH/wgAODv/u7v\n",
+ "8LWvfQ2Dg4N46aWXcPXVV6vn1Ot1OI6DWbNmIc9zfO9738Pjjz+u7n/LW94y4b444ogj1GNbrZb6\n",
+ "/Obt8fDoo48iSRIMDw/jn//5n7Fo0SJ1gT3ppJMwMDCAG2+8EVmW4Wc/+xnWrFmDI444AkII/Od/\n",
+ "/icGBwchhMCqVavw7W9/G+9///sBADfccAP+/Oc/4//+7//w2GOP4bDDDsOll16qxvC8/PLLePnl\n",
+ "lyGEwEMPPYTLLrtMjQ8aGhrC3XffjVarhTRN8V//9V944IEHcNxxx23zd2NhMR5sLNr+sejxxx/H\n",
+ "cccdh3/7t39TPeGbw1NPPYU777wTzWYTSZLgBz/4Af73f/8Xxx57LAC5gH/++echhMCLL76Iz3zm\n",
+ "MzjxxBPV88866yx873vfw+rVq9FoNHD55Zfj+OOPV/cnSYJWq4U8zxHHMVqtlrpYTxSLzjvvPDz7\n",
+ "7LMqjp1//vl4z3veg7vvvntbvhYLi83CxqJdG4sYt956K2bMmIHly5cX/v7000/jD3/4g4oJgDTB\n",
+ "43i0dOlSXHvttWi1Wmg2m/jOd76jCkIHHHAAnnrqKfz2t7+FEALPPPMM7rjjDnW/jUUW0wk2Fm3/\n",
+ "WHTrrbfiL3/5C/I8x/r163HRRRfh3e9+92ZJjDRN0Wq1kGVZYR0DTB6L8jxHq9VCkiTI8xxRFCFJ\n",
+ "EgByottjjz2GLMswOjqKiy66CAsXLsT+++8PYOJYtH79etx0002o1+vIsgx33303fvrTnxY8EbcH\n",
+ "pj2JEQQBbrnlFtxwww2YOXMmfvKTn4wZYzVV7LvvvqhWq3j55Zfxzne+E7VarcCibQ5f+tKX8Mgj\n",
+ "j6C7uxvHH3+8mtQxEcz725UJjuPgrW99K5YsWYKjjz4aF198MY4++mj1Xq973euw55574rjjjsMZ\n",
+ "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\n",
+ "1+t1/Ou//itmzZqFiRMn4uyzz0aapgCARqOBM888E3PnzkVnZycWLlyIe++9dzROj4fHsOFj0ZZh\n",
+ "pLHo4Ycfxj777IPOzk4sWLAAv/71r81td999N/bff390d3djxowZ+MhHPoK+vr7c43/2s5/hne98\n",
+ "J9rb27HddtvhRz/6kbntzjvvxB577IGOjg7st99+eOqpp8xtQ8UiAPjGN76BvfbaC6VSCaeffvpo\n",
+ "nBoPjxHBx6LRwxlnnIEgCPD8889v9D4HHXQQpk6dis7OTrz97W/H1VdfbW579dVXccwxx2DWrFkI\n",
+ "ggAvvvhi7rErVqzAokWLMGnSJGy33Xb4n//5n9ztWZbhggsuwKxZs9DZ2Yl3vvOd2LBhA4BNx6K1\n",
+ "a9fiuOOOQ3t7O+bOnYsbb7xxNE6Jh8ew4WPR6GFTsejFF180uRn/C4IAX/va1wAAK1euHDIW/fu/\n",
+ "/zt23nlnE8e+973vmdvWrFmD/fbbD5MnT0ZXVxcWLlyI22+/Pff4z3/+89huu+0wYcIEHHTQQbn9\n",
+ "21NPPYX3ve99mDBhAnbaaadBjx0NvClJDABQSo34Me95z3vw/e9/H9OnT98mPvwbw+6774577rkH\n",
+ "69atw6pVq7Bw4UKcccYZG72/EAJ33XUXent70dvbmyMLhvtc559/Pnbbbbfced+wYQOOPfZYPPvs\n",
+ "s1i1ahX22WcfLFq0yNx+2WWX4dFHH8UTTzyBZ599Fo8++iguueQSAECappgzZw4efPBB9PT04JJL\n",
+ "LsGJJ56IF154YTROkYfHsOFj0eZjJLFo7dq1OProo3H++edjw4YN+NSnPoWjjz4a69evBwD09PTg\n",
+ "wgsvxMqVK/HUU09hxYoVOO+888zjn3zySZx88sm49NJL0dPTg7/85S/4u7/7OwDAsmXLsHjxYlx1\n",
+ "1VXYsGEDjj76aBxzzDGmGjNULAKAWbNm4bOf/eyQcdTD442Gj0Vbjl/96ld4/vnnN3kurrjiCqxY\n",
+ "sQI9PT1YsmQJzjnnHDzzzDMAgCAIcNRRR+GWW25p+djFixdjxx13xGuvvYa7774bn/70p3H//feb\n",
+ "2y+66CL89re/xW9/+1v09PTg+9//PkqlEoBNx6Kzzz4bpVIJr732Gm644QacddZZQxLDHh5vBHws\n",
+ "2nIMJxbNmTPH5Ga9vb147LHHEAQBjj/+eABAGIZDxqL29nbcddddJo79y7/8C37zm9+Y26699lq8\n",
+ "9tpr2LBhAy6++GKceOKJpjj04x//GN/+9rfx0EMPYe3atdh3331xyimnAKAcbdGiRTjmmGOwbt06\n",
+ "XHXVVVi8eDGWLVs2mqfozUFi/OlPf8I73/lOdHZ24qSTTkKtVhvxc8RxjE984hPYb7/9EIbhiB77\n",
+ "3HPP4X3vex8mT56MKVOmYPHixYYVB0ip8OUvfxnz589HR0cHzjzzTKxatQpHHnkkurq6cOihh5qN\n",
+ "NuOaa67BrFmzMHPmTHzlK18xv69WqzjttNMwceJE7L777vj973+fe9xll12GefPmobOzE7vvvvtm\n",
+ "MVtTp07FrFmzAABSSgRBgBkzZgz5mI0FpOE818MPP4wnnngCp59+eu559t57b5x++umYMGECoijC\n",
+ "ueeei2eeeQbr1q0DANx1110455xzMGHCBEyePBmf+MQncO211wIAKpUKLrroIsyZMwcA8A//8A/Y\n",
+ "fvvt8eijj474fHh4DBc+FlmMdSx6+OGHMX36dBx//PEQQuDkk0/GlClTcOuttwIAPvjBD+Kwww5D\n",
+ "qVTChAkT8JGPfCSn1Ljkkkvw0Y9+FIcffjiCIEB3dzd22GEHAMDSpUtxwAEH4N3vfjeCIMD555+P\n",
+ "FStWmGrQULEIAI477jhTWfXwGAv4WGQxGrEIoI33Jz7xCVx55ZWbTML23HNPxHFsfm5vb0dnZycA\n",
+ "imsf/ehHsddeew16XF9fHx544AF8+tOfRhiGmD9/Pk444QQTT9atW4evf/3ruPrqq41MfbfddkOx\n",
+ "WAQwdCzq7+/Hrbfeis9//vOoVCrYb7/9sGjRolx11cNjtOFjkcXWiEUulixZgve+970mNxoqFgHA\n",
+ "xRdfjJ133hkAsM8+++CAAw4wJEaxWMQuu+xi2muCIMDkyZNRKBQAAE888QT2339/zJ07F0EQ4OST\n",
+ "TzaE6dNPP42VK1fi3HPPhRACBx10EPbbb79Rj0XjnsRoNBo49thjceqpp2LdunX4p3/6J9xyyy2G\n",
+ "mXrxxRfR3d290X833XTTqBzHZz7zGVPhe+mll3DxxReb24QQuPXWW/Hzn/8czzzzDO666y4ceeSR\n",
+ "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\n",
+ "lSux2267YcaMGTj33HPxyCOPYObMmeaY161bh8MOOwzHHnss/vM//zP3XKVSCVdeeSVefvll/PWv\n",
+ "f8XEiRNzfV1KKZx55plYvXo1brnllhH30Xl4jAQ+Fm39WPSe97wHjzzyCNasWYPrr78eTz/9NPbZ\n",
+ "Zx9z+5/+9CcsWrQI1113HQ466KDcY+fPnz/keTj++OPx2GOP4fXXX8fFF1+Mv/3tb9h7770BbDoW\n",
+ "MbwZmcdYwMei0Y9Fv/jFL3DeeedhxowZ5nX33XffYcvdkyRBW1vbsO47Z84c3HnnnXjttdfwm9/8\n",
+ "BqtXrzZxbGNxiv+2Q8WinXfeGWma4q9//at53J///OdB8nAPj9GCj0XjIxZVq1X87//+72a1klx0\n",
+ "0UVYunQp7rvvPrS3tw953zRNDUly77334oMf/CBmzpyJIAhMSxFPdttzzz1x//334/XXX8c999yD\n",
+ "5557LrdfGw1Eo/psbwDe/e53I4oiXHHFFTjrrLNw55134ve//z0OPvhgANaZdTio1+vmw1Sv11Gr\n",
+ "1Yzj81Do6+tDV1cXOjs7sWLFCnzpS1/a/Dekcckll+Cqq67C888/j+uuuw433HADAODEE0/EpZde\n",
+ "ine9613o6+vDlVdeaR7T398PIQQmT54MKSWuv/76XH/RAQccMKxzcdttt2H33XfHvHnzsGbNGvzb\n",
+ "v/0bjjrqqJaJw0svvYQXX3wRe++9N6SUuPLKK83YnU0911FHHZWbFnLTTTfhBz/4AX784x9DCIGe\n",
+ "nh4cfvjh2H///fHFL35x0Gu/8sorACho/O53v8Mll1ySM9M766yz8PTTT+NnP/vZsAkYD4/NhY9F\n",
+ "WzcWAURS7LHHHqhWq7jwwgsxZ84cHHrooQCAxx9/HEcccQS+8Y1v4Kijjhr02NNPPx2f//znsXjx\n",
+ "YkybNg2XXXYZjj76aHP7H//4R7zjHe/A2rVrcfbZZ2PRokXG8GpTsSjLMiRJgjRNkWUZ6vU6oijy\n",
+ "xKrHGwIfi0Y/Fi1btswkPkopzJgxA3fddVdLUuGZZ57B888/jwMPPBBRFOGHP/wh/vCHP+RiQq1W\n",
+ "M6NPa7Va7rw+/fTTmDVrForFIm6++Wb89Kc/xdNPPw0A2HHHHXHAAQfgC1/4Aq644go899xz+OEP\n",
+ "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\n",
+ "mJtMBcAFmf0AAAAASUVORK5CYII=\n"
+ ],
+ "text/plain": [
+ "<matplotlib.figure.Figure at 0x10d54f550>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "\n",
+ "# a code cell \n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "from matplotlib.colors import LightSource\n",
+ "\n",
+ "# example showing how to make shaded relief plots\n",
+ "# like Mathematica\n",
+ "# (http://reference.wolfram.com/mathematica/ref/ReliefPlot.html)\n",
+ "# or Generic Mapping Tools\n",
+ "# (http://gmt.soest.hawaii.edu/gmt/doc/gmt/html/GMT_Docs/node145.html)\n",
+ "\n",
+ "# test data\n",
+ "d= 1\n",
+ "def maltc(ax, lambd=1, n=1):\n",
+ " I0=1\n",
+ " I= lambda theta,d : I0*(sin(2*theta)*sin(pi*n*d/lambd))**2\n",
+ " X,Y=np.mgrid[-5:5:0.05,-5:5:0.05]\n",
+ " Z=np.sqrt(X**2+Y**2)+np.sin(X**2+Y**2)\n",
+ " \n",
+ " r= np.sqrt(X**2+Y**2)\n",
+ " theta = np.angle(X+1.0j*Y)\n",
+ " \n",
+ " Iv= np.vectorize(I)\n",
+ " Z = Iv(r,theta)\n",
+ " \n",
+ " # create light source object.\n",
+ " #ls = LightSource(azdeg=0,altdeg=65)\n",
+ " # shade data, creating an rgb array.\n",
+ " #rgb = ls.shade(Z,plt.cm.copper)\n",
+ " # plot un-shaded and shaded images.\n",
+ " #plt.figure(figsize=(12,5))\n",
+ " #plt.subplot(121)\n",
+ " ax.imshow(Z,cmap=plt.cm.copper)\n",
+ " ax.set_title('d=%d lambda=%f'%(d,lambd))\n",
+ " \n",
+ "fig, (axes) = plt.subplots(3,4)\n",
+ "fig.set_figheight(10)\n",
+ "fig.set_figwidth(20)\n",
+ "\n",
+ "flatten = [item for sublist in axes for item in sublist]\n",
+ "\n",
+ "for ax,l in zip(flatten,range(len(flatten))):\n",
+ " maltc(ax,lambd=(l+1)*pi/8.0)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "stdout\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "stderr\n"
+ ]
+ }
+ ],
+ "source": [
+ "from __future__ import print_function\n",
+ "import sys\n",
+ "print('stdout')\n",
+ "print('stderr',file=sys.stderr)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "<table class='table-striped table-condensed'>\n",
+ " <thead>\n",
+ " <tr>\n",
+ " <td>This is table</td>\n",
+ " <td>Head</td>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <td>but thi</td>\n",
+ " <td>is the </td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <td>body</td>\n",
+ " <td>of </td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <td>The</td>\n",
+ " <td>table</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/tools/tests/Confined Output.ipynb b/tools/tests/Confined Output.ipynb
new file mode 100644
index 0000000..8ba354d
--- /dev/null
+++ b/tools/tests/Confined Output.ipynb
@@ -0,0 +1,307 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Test notebook for overflowing content"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "markdown image:\n",
+ "\n",
+ "<img src=\"http://placehold.it/800x200.png\">\n",
+ "\n",
+ "unconfined markdown image:\n",
+ "\n",
+ "<img src=\"http://placehold.it/800x200.png\" class=\"unconfined\">"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.display import Image, IFrame"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Overflow image in HTML (non-embedded)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "<img src=\"http://placehold.it/800x200.png\" />"
+ ],
+ "text/plain": [
+ "<IPython.core.display.Image object>"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "Image(url=\"http://placehold.it/800x200.png\", embed=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Overflow image:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\nLzkAAAAASUVORK5CYII=\n",
+ "text/plain": [
+ "<IPython.core.display.Image object>"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "Image(url=\"http://placehold.it/800x200.png\", embed=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Overflow, unconfined"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\nLzkAAAAASUVORK5CYII=\n",
+ "text/plain": [
+ "<IPython.core.display.Image object>"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {
+ "image/png": {
+ "unconfined": true
+ }
+ },
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "Image(url=\"http://placehold.it/800x200.png\", embed=True, unconfined=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Overflow with explicit height, width (retina):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\nSlBFRyB2NjIpLCBkZWZhdWx0IHF1YWxpdHkK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMP\nFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEc\nITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgA\nyAcIAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMC\nBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYn\nKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeY\nmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5\n+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwAB\nAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpD\nREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ip\nqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMR\nAD8A9MooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA\nCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA\nCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA\nCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA\nCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA\nCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA\nCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA\nCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA\nCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK\nKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo\nooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii\ngAooooAKKKjmnit03yuEX3oAkoqC3vILrd5L7tuM8EYz9anoAKKZJKkMZeRgqjqTVA65aBsASEeo\nXigDSoqK3uYbpN8Lhh39RUtABRTJZo4IzJK4VR3NZ/8AblpuxiXHrt4/nQBp0VHBcRXEe+Jwy+3a\npKACimu6xoXdgqjkk1ntrlorYHmMPULx+tAGlRUNvdw3SloXDY6juKmoAKKKKACiiigAooooAKKK\nKACiqtzqFtaHbI+X/uryagj1u0dsHzE92Xj9KANGikVldQykFTyCKWgAoqvc3kFoAZXwT0Uck1Vj\n1u0dsHzE92Xj9KANKikVldQykFTyCO9LQAUVFcXMNqm+Zwo7epqkuuWhbBEgHqV4/nQBpUUyOVJo\nw8bBlPQin0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAF\nFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU\nUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRR\nQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFA\nBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAF\nFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU\nUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBnahqqWhMSLulx3\n6CufnnmuXMsrFucZ7D2rqLiwt7qVJJVJK8cHr9az9dRY7aBUUKoY4AGB0oAb4e/5ef8AgP8AWtus\nTw9/y8/8B/rW3QBzWsXTTXhiB+SPgD37mprfRPOsxI0hWRhlRjj2zVLUomi1CYN/ExYfQ810VpdR\nPYJKXUKqgNk9CKAObtZ5LK7D8jacOvqO4rrgQQCOhrjnzc3bFF5kckD6mutY+TbE9difyFAHOard\nm5u2UH93Gdqj37mrKaEzWgcyETEZC44+lZtqnm3kKn+Jxn867CgDlNPums7tSSQhO1x7V1dclqKe\nXqE6jj5s/nzXT2j+ZZwuTklBn64oAxNbuzJcfZ1PyJ973NLZ6N9otRLJIUZhlQB296zp2Mt1I3dn\nJ/WuwRQiKg6KMCgDkopJbC9z0ZGww9R3FdYjiRFdTlWAIPtXN60mzUWP95Qf6f0rZ0l9+mw5PIyP\nyNAF2iimTSeTBJJjOxS2PXAoAfRWJ/wkP/Tr/wCRP/rVqTXSRWZueq7dwHrnpQBPRWJ/wkP/AE6/\n+RP/AK1bdABVXULk2lm8i/e6L9atUjKGUqwBB6g0Acna2st/cFQfd3POKuX+ji2tzNFIWC/eDD9a\n3IbeG3DCKMIGOTiqWtXKxWZhzl5OAPQetAFLQ7plmNsxyjAlR6GtyWRYYXkbooJNc7osZfUVYdEU\nk/lj+tautPs05h/eYD+v9KAMEmXUL3k5eRvy/wD1VdvtH+y23nRyF9v3gR+opuhJuvmY/wAKEj9K\n37hPMt5U/vKR+lAGNod2RIbVj8rcp7HvW6SACT0FchZP5d9A2f4xn6V02oP5enzsP7hH58UAc5d3\nD314WGTk7UX27Vdn0QxWhkWQtIoyy44PriqulJv1KEHoCT+QrqetAHN6PdmC6ETH93Jxj0Paukrj\nXBguWA4KPx+BrsQQQCOhoAWiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA\nooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi\niigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK\nKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoooo\nAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA\nooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi\niigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKx9\nf/1EP+8f5VsVj6//AKiH/eP8qAGeHv8Al5/4D/WtusTw9/y8/wDAf61t0AYmuXEBIh2Bphzuz932\nrKS0uJITKkLlB3Apru1xclmPzO3P4muwRFjjVFGFUYAoA5vSLiCG6AlQbm4V8/drfvP+PG4/65t/\nKua1GJYdQmRRgZyB9Rn+tdDEzXGkgnlniIPucYoA57Tv+QjB/v11lcjYttvoD/00H8666gDltX/5\nCk3/AAH/ANBFb+m/8g6D/drntTbdqU5/2sfkMV0diuywgH+wD+lAHKRczpn+8P512dca4MVwwPVW\n/ka7IHIyKAOd17/j+T/rmP5mtHRf+Qcv+8azNcbdqGP7qAf1/rWro67dNjPqSf1oAv010WSNkYZV\ngQR7U6igDndW05LXZJCCIzwRnODUEl6X0uK2ycqxz9O38/0rf1ExDT5vN+7t4Hv2/WuUGMjPTvig\nDY0vS454DNcKSGPyDOOPWt2o4DGbeMxf6vaNv0qSgAooooAhurlLWBpZOg6D1PpXKzzSXdwZH5dj\ngAfyFWNUvTd3JCn90nC+/vV/RtP2qLqUfMfuA9h60AXNNshZ2+G5kflj/SoNe/48U/66D+RrUrN1\ntd2n5/uuD/T+tAFLQP8Aj4m/3B/Ot/rXPaC2LyRfWP8AqK35W2ROx7KTQBx0P+vj/wB4fzrptX/5\nBc3/AAH/ANCFc5arvu4V9XUfrXS6mu7TZwP7ufyOaAMTRf8AkIr/ALprpq5fR226nF7gj9K6igDk\nLzi+uP8Aro3866yH/UR/7o/lXIznzLqVh/E5I/OuwRdqKvoMUALRRRQAUUUUAFFFFABRRRQAUUUU\nAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQA\nUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABR\nRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFF\nFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU\nAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQA\nUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABR\nRRQAUUUUAFFFFABRRRQAUUUUANeRIkLyMFUdSTxWLrVzDPDEIpUchjnac1qX1u11ZyQoQGbGCenU\nGsX+wbr/AJ6Q/mf8KAH6JcQwef5sipnbjccZ61vKwZQykFSMgjvXPf2Ddf8APSH8z/hW9bxmK2ij\nYglECnHsKAOVvIWtrySPphsqfbtXRQ6nbSWwlaVVIHzKTyD9KL/T475Bk7ZF+62P0rHOh3YbAMZH\nruoAqXErXd48iqcyN8o/lXV28Xk28cX91QKo6fpK2jebIweXtjotaVAHJXkDWl66DgA5U+3at1NX\ntjaeazjeBzH3zU17YRXsYD/K4+647Vk/2Bcbv9bFt9ec0AUIo3vbwL1aRsk+nqa64AKAAMAcCqlj\np0dipIO+Q9WI/lVygDmNXtzBfO2Plk+Yf1rUstUtzZL50gV0GCD1OPSrl1aR3kJjkHuCOoNYzaBO\nG+SaMr6nINAFCeR729Z1UlpG+UfoK6uCIQQRxDoigVTsNKjs28xm8yXscYA+laFABTXdI0LuwVR1\nJNOqvfW7XVnJChAZsYJ6dQaAMTWL1biVYomDRpzkdzSTafs0iO4x+8zub6Hp/T86kj0GfzF8ySLZ\nn5sE5x+VbskSywtER8rLtoAxdGv0iRoJnCqPmUsfzFbisGUMpBBGQR3rnv7Buv8AnpD+Z/wregjM\nVvFGSCUQKSPYUASVR1a4+z2LbThn+Uf1q9WfqlhLfLEI3UbCchu+aAMKxjikulE7qkY5O44z7V0n\n2+0H/LxF/wB9Vjf2Ddf89IfzP+FH9g3X/PSH8z/hQBuxXMEzFYpUdgM4U5pLuD7TaSRd2HH17VQ0\nzTJrK5aSRoyChX5SfUe3tWrQByVnObO9WRgflOGHt3rY1LU4DZtHDIHeQY47Dvmn3+kpduZY28uQ\n9eODVJNAmLfvJowv+zkmgCLRbcy3vmEfLGM/j2ropEEkbI3RgQajtraO1hEUY4HUnqT61NQByHz2\nV6Mj54n/ADreuNWt1sy8UgMjD5V7g+9Pv9MjvcNnZKON2Ov1rNXQJ93zSxhfUZJoAqabbm4vo1x8\nqnc30FdXVazso7KLYnLH7zHqas0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF\nABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA\nFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU\nUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR\nRQAUUUUAFFFFABRRRQAUUUUAFFFFAH//2Q==\n",
+ "text/plain": [
+ "<IPython.core.display.Image object>"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {
+ "image/jpeg": {
+ "height": 100,
+ "width": 900
+ }
+ },
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "Image(url=\"http://placehold.it/1800x200.jpg\", embed=True, retina=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Overflowing IFrame:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " <iframe\n",
+ " width=\"900\"\n",
+ " height=\"400\"\n",
+ " src=\"http://ipython.org\"\n",
+ " frameborder=\"0\"\n",
+ " allowfullscreen\n",
+ " ></iframe>\n",
+ " "
+ ],
+ "text/plain": [
+ "<IPython.lib.display.IFrame at 0x105239dd8>"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "IFrame(src=\"http://ipython.org\", width=900, height=400)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Overflowing table:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+ "<table border=\"1\" class=\"dataframe\">\n",
+ " <thead>\n",
+ " <tr style=\"text-align: right;\">\n",
+ " <th></th>\n",
+ " <th>0</th>\n",
+ " <th>1</th>\n",
+ " <th>2</th>\n",
+ " <th>3</th>\n",
+ " <th>4</th>\n",
+ " <th>5</th>\n",
+ " <th>6</th>\n",
+ " <th>7</th>\n",
+ " <th>8</th>\n",
+ " <th>9</th>\n",
+ " <th>10</th>\n",
+ " <th>11</th>\n",
+ " <th>12</th>\n",
+ " <th>13</th>\n",
+ " <th>14</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " <td> column</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " 0 1 2 3 4 5 6 7 8 \\\n",
+ "0 column column column column column column column column column \n",
+ "\n",
+ " 9 10 11 12 13 14 \n",
+ "0 column column column column column column "
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "pd.DataFrame([['column'] * 15])"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/tools/tests/Test Output Callbacks.ipynb b/tools/tests/Test Output Callbacks.ipynb
new file mode 100644
index 0000000..94ec190
--- /dev/null
+++ b/tools/tests/Test Output Callbacks.ipynb
@@ -0,0 +1,291 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Basic Output"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.display import display"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "print('hi')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "display('hi')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "1"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%matplotlib inline\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.plot([1,3,2])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%javascript\n",
+ "console.log(\"I ran!\");"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%html\n",
+ "<b>bold</b>"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%latex\n",
+ "$$\n",
+ "a = 5\n",
+ "$$"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# input_request"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "raw_input(\"prompt > \")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# set_next_input"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%%writefile tst.py\n",
+ "def foo():\n",
+ " pass\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%load tst.py"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Pager in execute_reply"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "plt?"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# object_info"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "# press shift-tab after parentheses\n",
+ "int("
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# complete"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "# press tab after f\n",
+ "f"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# clear_output"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "import sys, time\n",
+ "from IPython.display import clear_output"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "for i in range(10):\n",
+ " clear_output()\n",
+ " time.sleep(0.25)\n",
+ " print(i)\n",
+ " sys.stdout.flush()\n",
+ " time.sleep(0.25)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "for i in range(10):\n",
+ " clear_output(wait=True)\n",
+ " time.sleep(0.25)\n",
+ " print(i)\n",
+ " sys.stdout.flush()\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.4.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}