├── .babelrc ├── .coveragerc ├── .dockerignore ├── .esdoc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── appveyor.yml ├── design └── screens.svg ├── environment.yml ├── nbpresent ├── __init__.py ├── _version.py ├── export.py ├── exporters │ ├── __init__.py │ ├── base.py │ ├── html.py │ ├── pdf.py │ ├── pdf_capture.py │ └── pdf_ghost.py ├── tasks │ ├── __init__.py │ ├── _env.py │ ├── build.py │ ├── clean.py │ ├── deps.py │ ├── index.py │ ├── less.py │ ├── notebook.py │ ├── requirejs.py │ └── standalone.py ├── templates │ └── nbpresent.tpl └── tests │ ├── __init__.py │ ├── js │ ├── _utils.js │ ├── test_export_html.js │ ├── test_notebook_basic.js │ ├── test_notebook_create.js │ └── test_notebook_no_regions.js │ ├── notebooks │ └── Basics.ipynb │ ├── test_export.py │ └── test_notebook.py ├── notebooks ├── Bokeh Mic Check.ipynb ├── Examples.ipynb ├── Extending nbpresent.ipynb ├── Importing revealjs themes.ipynb ├── README.ipynb ├── index.ipynb └── proposal.ipynb ├── package.json ├── screenshot.png ├── setup.cfg ├── setup.py ├── sphinx ├── Makefile ├── make.bat └── source │ ├── conf.py │ └── index.rst └── src ├── es6 ├── actions │ ├── base.es6 │ ├── notebook.es6 │ └── standalone.es6 ├── cells │ ├── base.es6 │ ├── notebook.es6 │ ├── overlay.es6 │ └── standalone.es6 ├── d3.bbox.js ├── editor.es6 ├── help │ ├── helper.es6 │ └── tours │ │ ├── base.es6 │ │ ├── intro.es6 │ │ ├── slide-editor.es6 │ │ └── sorter.es6 ├── icons.es6 ├── layout │ ├── grid.es6 │ ├── manual.es6 │ └── treemap.es6 ├── less.es6 ├── logger.es6 ├── mini.es6 ├── mode │ ├── base.es6 │ ├── notebook.es6 │ └── standalone.es6 ├── package.es6 ├── parts.es6 ├── presenter │ ├── base.es6 │ ├── notebook.es6 │ └── standalone.es6 ├── regiontree.es6 ├── sorter.es6 ├── speaker │ ├── base.es6 │ └── notebook.es6 ├── templates │ ├── _util.es6 │ ├── library.es6 │ └── manual.es6 ├── theme │ ├── base.es6 │ ├── card.es6 │ ├── fonts.es6 │ ├── manager.es6 │ └── theme │ │ ├── plain.es6 │ │ └── reveal.es6 ├── tome │ ├── arise.es6 │ ├── base.es6 │ └── basic.es6 ├── toolbar.es6 ├── tree.es6 └── vendor.es6 ├── js ├── build.js ├── index.js └── main.js └── less ├── app.less ├── editor.less ├── help.less ├── index.less ├── layouts.less ├── link-overlay.less ├── mini.less ├── mixins.less ├── presenter.less ├── sorter.less ├── speaker.less ├── theme-card.less ├── theme-ui.less ├── toolbar.less ├── tour.less └── variables.less /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */templates/* 4 | */static/* 5 | */tasks/* 6 | */pdf* 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .git/ 3 | *.egg-info 4 | *.pyc 5 | \.ipynb_checkpoints/ 6 | build/ 7 | dist/ 8 | screenshots/ 9 | nbpresent/static/nbpresent/ 10 | node_modules/ 11 | *.xunit.xml 12 | .coverage 13 | *.log 14 | -------------------------------------------------------------------------------- /.esdoc: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src/es6", 3 | "destination": "./nbpresent/static/docs/esdoc" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .coverage 3 | .DS_Store 4 | .eggs/ 5 | *.egg-info 6 | *.pyc 7 | *.xunit.xml 8 | .coverage 9 | screenshots/ 10 | sphinx/source/auto 11 | \.ipynb_checkpoints 12 | build/ 13 | dist/ 14 | nbpresent/static/nbpresent/ 15 | node_modules 16 | npm-debug.log* 17 | screenshots/ 18 | sphinx/source/auto 19 | *.log 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: true 4 | 5 | python: 6 | # We don't actually use the Travis Python, but this keeps it organized. 7 | - "2.7" 8 | - "3.4" 9 | - "3.5" 10 | 11 | install: 12 | - sudo apt-get update 13 | - sudo apt-get install -y libfreetype6-dev libfontconfig1-dev 14 | - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then 15 | wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; 16 | else 17 | wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 18 | fi 19 | - bash miniconda.sh -b -p $HOME/miniconda 20 | - export PATH="$HOME/miniconda/bin:$PATH" 21 | - hash -r 22 | - conda config --set always_yes yes --set changeps1 no 23 | - conda config --add channels conda-forge 24 | - conda update -q conda 25 | - conda info -a 26 | - conda create -n nbpresent python=$TRAVIS_PYTHON_VERSION 27 | - conda env update 28 | - source activate nbpresent 29 | - conda install python-coveralls 30 | - npm install 31 | 32 | script: 33 | - npm run lint 34 | - python setup.py develop 35 | - npm run build 36 | - jupyter nbextension install nbpresent --py --sys-prefix --symlink 37 | - jupyter nbextension enable nbpresent --py --sys-prefix 38 | - jupyter serverextension enable nbpresent --py --sys-prefix 39 | - npm run test 40 | 41 | after_success: 42 | - coveralls 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Continuum Analytics, Inc. and contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Continuum Analytics nor the names of any contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 28 | THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.rst 3 | include *.md 4 | include package.json 5 | 6 | recursive-include nbpresent * 7 | 8 | exclude nbpresent/static/.git 9 | exclude sphinx 10 | exclude design 11 | exclude Dockerfile 12 | 13 | recursive-exclude * __pycache__ 14 | recursive-exclude * *.py[co] 15 | recursive-exclude node_modules *.* 16 | recursive-exclude * node_modules 17 | recursive-exclude * .git 18 | 19 | prune nbpresent/tests/notebooks/.ipynb_checkpoints 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # nbpresent 3 | 4 | [![](https://anaconda.org/anaconda-nb-extensions/nbpresent/badges/installer/conda.svg 5 | )](https://anaconda.org/anaconda-nb-extensions/nbpresent) [![](https://img.shields.io/pypi/v/nbpresent.svg)](https://pypi.python.org/pypi/nbpresent) 6 | [![Build Status (Lin64)](https://travis-ci.org/Anaconda-Platform/nbpresent.svg)](https://travis-ci.org/Anaconda-Platform/nbpresent) [![Build Status (Win64)](https://ci.appveyor.com/api/projects/status/aa6g8ya0oy1m6r9r?svg=true)](https://ci.appveyor.com/project/bollwyvl/nbpresent) 7 | [![Coverage Status](https://coveralls.io/repos/github/Anaconda-Platform/nbpresent/badge.svg?branch=master)](https://coveralls.io/github/Anaconda-Platform/nbpresent?branch=master) 8 | 9 | > remix your [Jupyter Notebooks](http://jupyter.org) as interactive slideshows 10 | 11 | ![](./screenshot.png) 12 | 13 | ## Using 14 | After [installing](#install) (and potentially enabling) as appropriate for your environment, relaunch the Jupyter Notebook: in the main toolbar, you will get two new buttons that toggle the _Authoring_ and _Presenting_ modes. 15 | 16 | ## User Documentation and Community 17 | When _Authoring_, you can click on the `(?)` icon to see a number of in-Notebook guided tours that show all the features, as well as see links to community pages: 18 | 19 | - [mailing list](https://groups.google.com/forum/#!forum/nbpresent) for general or long-term discussion and announcements 20 | - [issues](https://github.com/Anaconda-Platform/nbpresent/issues) for technical issues, as well as feature requests 21 | - [chat](https://gitter.im/Anaconda-Platform/nbpresent) for quickly connecting with other users 22 | 23 | ## Related Projects 24 | - [live_reveal/RISE](https://github.com/damianavila/RISE), the original inspiration for this work, based on [reveal.js](https://github.com/hakimel/reveal.js/). 25 | - [RMarkdown](http://rmarkdown.rstudio.com/ioslides_presentation_format.html) presentations 26 | 27 | ## Publishing 28 | When you are happy with your presentation, you can download the standalone HTML file from the _File -> Download as -> Presentation (.html)_ menu item. 29 | 30 | ## Install 31 | 32 | > Note: installing directly off this repo won't work, as we don't ship the built JavaScript and CSS assets. See more about [developing](#develop) below. 33 | 34 | ### `pip` 35 | ```shell 36 | pip install nbpresent 37 | jupyter nbextension install nbpresent --py --overwrite 38 | jupyter nbextension enable nbpresent --py 39 | jupyter serverextension enable nbpresent --py 40 | ``` 41 | 42 | ### `conda` 43 | ```shell 44 | conda install -c conda-forge nbpresent 45 | ``` 46 | 47 | This will enable the `nbpresent` `nbextension` and `serverextension` automatically! 48 | 49 | ## Export 50 | ### HTML 51 | Stock `nbconvert` doesn't store quite enough information, so you'll need to do something like this: 52 | ```shell 53 | nbpresent -i notebooks/README.ipynb -o README.html 54 | ``` 55 | The resulting file can be hosted and viewed (but not edited!) on any site. 56 | 57 | You can also pass in and get back streams: 58 | ```shell 59 | cmd_that_generates_ipynb | nbpresent > README.html 60 | ``` 61 | 62 | ### PDF (Experimental) 63 | If you have installed [nbbrowserpdf](https://github.com/Anaconda-Platform/nbbrowserpdf), you can also export to pdf: 64 | ```shell 65 | nbpresent -i notebooks/README.ipynb -f pdf -o README.pdf 66 | ``` 67 | 68 | Here's the whole doc: 69 | 70 | 71 | ```python 72 | !nbpresent --help 73 | ``` 74 | 75 | usage: nbpresent [-h] [-i IPYNB] [-o OUTFILE] [-f {html,pdf}] 76 | 77 | Generate a static nbpresent presentation from a Jupyter Notebook 78 | 79 | optional arguments: 80 | -h, --help show this help message and exit 81 | -i IPYNB, --ipynb IPYNB 82 | Input file (otherwise read from stdin) 83 | -o OUTFILE, --outfile OUTFILE 84 | Output file (otherwise write to stdout) 85 | -f {html,pdf}, --out-format {html,pdf} 86 | Output format 87 | 88 | 89 | ## Develop 90 | This assumes you have cloned this repository locally: 91 | ``` 92 | git clone https://github.com/Anaconda-Platform/nbpresent.git 93 | cd nbpresent 94 | ``` 95 | 96 | ### Repo Architecture 97 | 98 | The `nbpresent` nbextension is built from `./src` into `./nbpresent/static/nbresent` with: 99 | - `less` for style 100 | - `es6` (via `babel`) for javascript 101 | - `browserify` for packaging 102 | 103 | The `nbpresent` python module (server component) is stored in the `/nbpresent` folder 104 | 105 | ### Getting Started 106 | You'll need conda installed, either from [Anaconda](https://www.continuum.io/downloads) or [miniconda](http://conda.pydata.org/miniconda.html). You can create a Python development environment named `nbpresent` from `./environment.yml`. 107 | 108 | ```shell 109 | conda create -n nbpresent python=YOUR_FAVORITE_PYTHON 110 | conda update env 111 | source activate nbpresent 112 | ``` 113 | 114 | We _still_ use `npm` for a lot of dependencies, so then run: 115 | ```shell 116 | npm install 117 | ``` 118 | 119 | Finally, you are ready to build the assets! 120 | ```shell 121 | npm run build 122 | ``` 123 | 124 | ### Ensure development asset loading 125 | To ensure that you always get the right assets, install the nbextension with the `symlink` options: 126 | ```shell 127 | jupyter nbextension install nbpresent --overwrite --symlink --sys-prefix 128 | jupyter nbextension enable nbpresent --sys-prefix 129 | jupyter serverextension enable nbpresent --sys-prefix 130 | ``` 131 | 132 | See [chore automation](#chore-automation) below for more good times. 133 | 134 | ### Chore Automation 135 | | Task | Command | 136 | |------|---------| 137 | | Build all of the front end assets with sourcemaps for development | `npm run build` | 138 | | Rebuild on every save | `npm run watch` | 139 | | Rebuild all of the front end assets, and optimize it | `npm run dist` | 140 | | Run the CasperJS and `nose` tests | `npm run test` | 141 | | Check code style | `npm run lint` | 142 | | Build **and upload** the pypi **test** package | `npm run pkg:pypi` | 143 | | Build **and upload** the pypi **release** package | `npm run pkg:pypi:release` | 144 | | Build the ESDoc and Sphinx documentation | `npm run docs` | 145 | 146 | ## Changelog 147 | 148 | ### 3.0.2 149 | - use [Travis-CI](https://travis-ci.org/Anaconda-Platform/nbpresent) for continuous integration 150 | - use [Coveralls](https://coveralls.io/github/Anaconda-Platform/nbpresent) for code coverage 151 | - use a [conda-forge](https://github.com/conda-forge/nbpresent-feedstock) for cross-platform `conda` package building 152 | 153 | ### 3.0.1 154 | - minor build changes 155 | 156 | ### 3.0.0 157 | - Update to notebook 4.2 158 | 159 | ### 2.0.0 160 | - Theme editor removed. Significant work required to stabilize to public release quality. 161 | - Adding some themes extracted from reveal.js 162 | 163 | ### 1.1.1 164 | - fixing enabling on windows with `nb_config_manager` 0.1.3 165 | - trimming down conda packages 166 | - more reproducible builds 167 | 168 | ### 1.1.0 (Unreleased) 169 | - fixing issue with slides without regions and some layouts crashing editor [#58](https://github.com/Anaconda-Platform/nbpresent/issues/58) 170 | - adding JS extensibility of themes (partial [#44](https://github.com/Anaconda-Platform/nbpresent/issues/44)) 171 | - see [Extending nbpresent](https://github.com/Anaconda-Platform/nbpresent/blob/master/notebooks/Extending%20nbpresent.ipynb) 172 | 173 | ### 1.0.0 174 | - [Theme editor](https://github.com/Anaconda-Platform/nbpresent/pull/41) 175 | - Much more consistent UI 176 | - Mnay bug fixes and more testing 177 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | 3 | # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the 4 | # /E:ON and /V:ON options are not enabled in the batch script interpreter 5 | # See: http://stackoverflow.com/a/13751649/163740 6 | CMD_IN_ENV: "cmd /E:ON /V:ON /C obvci_appveyor_python_build_env.cmd" 7 | 8 | # Workaround for https://github.com/conda/conda-build/issues/636 9 | PYTHONIOENCODING: "UTF-8" 10 | 11 | matrix: 12 | # Note: Because we have to separate the py2 and py3 components due to compiler version, we have a race condition for non-python packages. 13 | # Not sure how to resolve this, but maybe we should be tracking the VS version in the build string anyway? 14 | - TARGET_ARCH: "x64" 15 | CONDA_PY: "27" 16 | PY_CONDITION: "python >=2.7,<3" 17 | - TARGET_ARCH: "x64" 18 | CONDA_PY: "34" 19 | PY_CONDITION: "python >=3.4,<3.5" 20 | - TARGET_ARCH: "x64" 21 | CONDA_PY: "35" 22 | PY_CONDITION: "python >=3.5" 23 | 24 | 25 | # We always use a 64-bit machine, but can build x86 distributions 26 | # with the TARGET_ARCH variable (which is used by CMD_IN_ENV). 27 | platform: 28 | - x64 29 | 30 | install: 31 | # If there is a newer build queued for the same PR, cancel this one. 32 | # The AppVeyor 'rollout builds' option is supposed to serve the same 33 | # purpose but it is problematic because it tends to cancel builds pushed 34 | # directly to master instead of just PR builds (or the converse). 35 | # credits: JuliaLang developers. 36 | - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` 37 | https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` 38 | Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` 39 | throw "There are newer queued builds for this pull request, failing early." } 40 | 41 | # Use the pre-installed Miniconda for the desired arch 42 | - ps: if($env:TARGET_ARCH -eq 'x86') 43 | {$root = "C:\Miniconda"} 44 | else 45 | {$root = "C:\Miniconda-x64"} 46 | $env:path="$root;$root\Scripts;$root\Library\bin;$($env:path)" 47 | - cmd: conda config --set show_channel_urls true 48 | - cmd: conda config --set always_yes yes --set changeps1 no 49 | - cmd: conda update --yes --quiet conda 50 | - cmd: set PYTHONUNBUFFERED=1 51 | - cmd: conda config --add channels conda-forge 52 | - cmd: conda info 53 | - cmd: conda create -n nbpresent "%PY_CONDITION%" 54 | - cmd: conda env update 55 | - cmd: activate nbpresent 56 | - cmd: npm install 57 | 58 | # Skip .NET project specific build phase. 59 | build: off 60 | 61 | test_script: 62 | - cmd: python setup.py develop 63 | - cmd: npm run build 64 | - cmd: jupyter nbextension install nbpresent --py --sys-prefix 65 | - cmd: jupyter nbextension enable nbpresent --py --sys-prefix 66 | - cmd: jupyter serverextension enable nbpresent --py --sys-prefix 67 | - cmd: npm run test 68 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: nbpresent 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - anaconda-client 7 | - coverage 8 | - flake8 9 | - ipywidgets >=5.1.5 10 | - mock 11 | - nodejs 12 | - nose 13 | - notebook >=4.2.0 14 | - python 15 | - requests 16 | -------------------------------------------------------------------------------- /nbpresent/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # flake8: noqa 3 | from os.path import join 4 | 5 | from nbconvert.exporters.export import exporter_map 6 | 7 | from .exporters.html import PresentExporter 8 | from ._version import __version__, __version_info__ 9 | 10 | 11 | PDFPresentExporter = None 12 | pdf_import_error = None 13 | 14 | try: 15 | from .exporters.pdf import PDFPresentExporter 16 | except Exception as err: 17 | pdf_import_error = err 18 | 19 | 20 | # Jupyter Extension points 21 | def _jupyter_nbextension_paths(): 22 | return [dict( 23 | section="notebook", 24 | src=join("static", "nbpresent"), 25 | dest="nbpresent", 26 | require="nbpresent/js/nbpresent.min")] 27 | 28 | 29 | def _jupyter_server_extension_paths(): 30 | return [dict(module="nbpresent")] 31 | 32 | 33 | # serverextension 34 | def load_jupyter_server_extension(nbapp): 35 | nbapp.log.info("✓ nbpresent HTML export ENABLED") 36 | exporter_map.update( 37 | nbpresent=PresentExporter, 38 | ) 39 | 40 | if pdf_import_error: 41 | nbapp.log.warn( 42 | "✗ nbpresent PDF export DISABLED: {}" 43 | .format(pdf_import_error) 44 | ) 45 | else: 46 | nbapp.log.info("✓ nbpresent PDF export ENABLED") 47 | exporter_map.update( 48 | nbpresent_pdf=PDFPresentExporter 49 | ) 50 | -------------------------------------------------------------------------------- /nbpresent/_version.py: -------------------------------------------------------------------------------- 1 | __version_info__ = (3, 0, 2) 2 | __version__ = '.'.join(map(str, __version_info__)) 3 | -------------------------------------------------------------------------------- /nbpresent/export.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | import codecs 3 | import os 4 | import sys 5 | 6 | from .exporters import ( 7 | APP_ROOT 8 | ) 9 | 10 | BINARY_FORMATS = ["pdf"] 11 | 12 | 13 | def export(ipynb=None, outfile=None, out_format=None, verbose=None): 14 | if out_format in ["pdf"]: 15 | from .exporters.pdf import PDFPresentExporter as Exporter 16 | elif out_format in ["html"]: 17 | from .exporters.html import PresentExporter as Exporter 18 | 19 | exp = Exporter( 20 | template_file="nbpresent", 21 | template_path=[os.path.join(APP_ROOT, "templates")] 22 | ) 23 | if ipynb is not None: 24 | output, resources = exp.from_filename(ipynb) 25 | else: 26 | output, resources = exp.from_file(sys.stdin) 27 | 28 | mode, stream = (["wb+", sys.stdout.buffer] 29 | if out_format in BINARY_FORMATS 30 | else ["w+", sys.stdout]) 31 | 32 | if outfile is not None: 33 | if out_format in BINARY_FORMATS: 34 | with open(outfile, mode) as fp: 35 | fp.write(output) 36 | else: 37 | with codecs.open(outfile, mode, encoding="utf-8") as fp: 38 | fp.write(output) 39 | else: 40 | stream.write(output) 41 | 42 | 43 | def main(): 44 | parser = ArgumentParser( 45 | description="Generate a static nbpresent presentation from a Jupyter" 46 | " Notebook") 47 | parser.add_argument( 48 | "-i", "--ipynb", 49 | help="Input file (otherwise read from stdin)") 50 | parser.add_argument( 51 | "-o", "--outfile", 52 | help="Output file (otherwise write to stdout)") 53 | parser.add_argument( 54 | "-f", "--out-format", 55 | default="html", 56 | choices=["html", "pdf"], 57 | help="Output format" 58 | ) 59 | 60 | export(**parser.parse_args().__dict__) 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /nbpresent/exporters/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .base import ( 3 | ASSETS, 4 | APP_ROOT 5 | ) 6 | -------------------------------------------------------------------------------- /nbpresent/exporters/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from notebook import DEFAULT_STATIC_FILES_PATH 4 | 5 | APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 6 | ASSETS = os.path.join(APP_ROOT, "static", "nbpresent") 7 | NB_ASSETS = [os.path.join(DEFAULT_STATIC_FILES_PATH, *bits) for bits in [ 8 | ["components", "requirejs", "require.js"] 9 | ]] 10 | STANDALONE_ASSETS = [ 11 | os.path.join(ASSETS, "js", "nbpresent.static.min.js"), 12 | os.path.join(ASSETS, "css", "nbpresent.min.css"), 13 | ] 14 | -------------------------------------------------------------------------------- /nbpresent/exporters/html.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import json 4 | 5 | from collections import ( 6 | OrderedDict, 7 | defaultdict 8 | ) 9 | 10 | from nbconvert.exporters.html import HTMLExporter 11 | 12 | from .base import ( 13 | APP_ROOT, 14 | STANDALONE_ASSETS, 15 | NB_ASSETS 16 | ) 17 | 18 | 19 | class PresentExporter(HTMLExporter): 20 | def __init__(self, *args, **kwargs): 21 | # TODO: this probably isn't the right way 22 | kwargs.update( 23 | template_file="nbpresent", 24 | template_path=[os.path.join(APP_ROOT, "templates")] 25 | ) 26 | super(PresentExporter, self).__init__(*args, **kwargs) 27 | 28 | def from_notebook_node(self, nb, resources=None, **kw): 29 | bin_ext = ["woff", "ttf"] 30 | 31 | resources = self._init_resources(resources) 32 | 33 | assets = defaultdict(OrderedDict) 34 | 35 | to_inline = NB_ASSETS + STANDALONE_ASSETS 36 | 37 | for filename in to_inline: 38 | ext = filename.split(".")[-1] 39 | mode = "rb" if ext in bin_ext else "r" 40 | with codecs.open(filename, mode, encoding="utf-8") as fp: 41 | assets[ext].update({ 42 | os.path.basename(filename): fp.read() 43 | }) 44 | 45 | resources.update( 46 | nbpresent={ 47 | "metadata": json.dumps(nb.metadata.get("nbpresent", {}), 48 | indent=2, 49 | sort_keys=True) 50 | }, 51 | nbpresent_assets=assets 52 | ) 53 | 54 | return super(PresentExporter, self).from_notebook_node( 55 | nb, 56 | resources=resources, 57 | **kw 58 | ) 59 | -------------------------------------------------------------------------------- /nbpresent/exporters/pdf.py: -------------------------------------------------------------------------------- 1 | from .html import PresentExporter 2 | 3 | from nbbrowserpdf.exporters.pdf import BrowserPDFExporter 4 | 5 | 6 | class PDFPresentExporter(PresentExporter, BrowserPDFExporter): 7 | def pdf_capture_args(self): 8 | return [ 9 | "--capture-server-class", 10 | "nbpresent.exporters.pdf_capture:SlideCaptureServer" 11 | ] 12 | -------------------------------------------------------------------------------- /nbpresent/exporters/pdf_capture.py: -------------------------------------------------------------------------------- 1 | from nbbrowserpdf.exporters.pdf_capture import CaptureServer 2 | 3 | VIEWPORT = (1920, 1080) 4 | 5 | 6 | class SlideCaptureServer(CaptureServer): 7 | """ CaptureServer to generate multi-page PDF based on nbpresent metadata 8 | """ 9 | ghost_cmd = "nbpresent.exporters.pdf_ghost" 10 | -------------------------------------------------------------------------------- /nbpresent/exporters/pdf_ghost.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from ghost.bindings import ( 4 | QPainter, 5 | QPrinter, 6 | QtCore, 7 | ) 8 | 9 | from PyPDF2 import ( 10 | PdfFileReader, 11 | PdfFileMerger, 12 | ) 13 | 14 | from nbbrowserpdf.exporters.pdf_ghost import NotebookPDFGhost, parser 15 | 16 | 17 | # a notional default viewport... 18 | VIEWPORT = (1920, 1080) 19 | 20 | 21 | class PresentationPDFGhost(NotebookPDFGhost): 22 | embed_ipynb = True 23 | 24 | def print_to_pdf(self, path): 25 | """ Custom print based on metadata: generate one per slide 26 | """ 27 | merger = PdfFileMerger() 28 | 29 | for i, slide in enumerate(self.notebook.metadata.nbpresent.slides): 30 | print("\n\n\nprinting slide", i, slide) 31 | # science help you if you have 9999 slides 32 | filename = self.in_static("notebook-{0:04d}.pdf".format(i)) 33 | # this is weird, but it seems to always need it 34 | self.session.show() 35 | self.screenshot(filename) 36 | merger.append(PdfFileReader(filename, "rb")) 37 | 38 | # advance the slides 39 | result, resources = self.session.evaluate( 40 | """ 41 | console.log(window.nbpresent); 42 | console.log(window.nbpresent.mode.presenter.speaker.advance()); 43 | """) 44 | 45 | # always seem to get some weirdness... perhaps could inttegrate 46 | # ioloop... 47 | time.sleep(1) 48 | 49 | # all done! 50 | merger.write(path) 51 | 52 | def screenshot(self, filename): 53 | """ do an individual slide screenshot 54 | big thanks to https://gist.github.com/jmaupetit/4217925 55 | """ 56 | 57 | printer = QPrinter(mode=QPrinter.ScreenResolution) 58 | printer.setOutputFormat(QPrinter.PdfFormat) 59 | printer.setPaperSize(QtCore.QSizeF(*reversed(VIEWPORT)), 60 | QPrinter.DevicePixel) 61 | printer.setOrientation(QPrinter.Landscape) 62 | printer.setOutputFileName(filename) 63 | printer.setPageMargins(0, 0, 0, 0, QPrinter.DevicePixel) 64 | 65 | painter = QPainter(printer) 66 | painter.scale(1.45, 1.45) 67 | self.session.main_frame.render(painter) 68 | painter.end() 69 | 70 | 71 | def main(url, static_path): 72 | pdf_ghost = PresentationPDFGhost(url, static_path) 73 | 74 | pdf_ghost.render() 75 | 76 | 77 | if __name__ == "__main__": 78 | main(**parser.parse_args().__dict__) 79 | -------------------------------------------------------------------------------- /nbpresent/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaconda/nbpresent/6bf2f50b7abd27273dc9e7d469bf1866bafbf5bb/nbpresent/tasks/__init__.py -------------------------------------------------------------------------------- /nbpresent/tasks/_env.py: -------------------------------------------------------------------------------- 1 | from os.path import ( 2 | dirname, 3 | abspath, 4 | join, 5 | ) 6 | import platform 7 | 8 | 9 | PKG = "nbpresent" 10 | 11 | REPO_ROOT = abspath(join(dirname(__file__), "..", "..")) 12 | PKG_ROOT = join(REPO_ROOT, PKG) 13 | SRC = join(REPO_ROOT, "src") 14 | DIST = join(PKG_ROOT, "static", PKG) 15 | JS = join(DIST, "js") 16 | CSS = join(DIST, "css") 17 | 18 | IS_WIN = "Windows" in platform.system() 19 | 20 | 21 | def node_bin(*it): 22 | return join(REPO_ROOT, "node_modules", ".bin", *it) 23 | 24 | 25 | def external(*modules): 26 | # browserify --external 27 | return sum([["--external", m] for m in modules], []) 28 | 29 | transform = [ 30 | "--transform", "[", "babelify", "--sourceMapRelative", ".", "]", 31 | ] 32 | 33 | extension = ["--extension", "es6"] 34 | -------------------------------------------------------------------------------- /nbpresent/tasks/build.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from multiprocessing import Pool, cpu_count 3 | from importlib import import_module 4 | 5 | from nbpresent.tasks import ( 6 | requirejs, 7 | clean, 8 | ) 9 | 10 | 11 | try: 12 | CPU_COUNT = cpu_count() 13 | except NotImplementedError: 14 | CPU_COUNT = 1 15 | 16 | 17 | def _run(mod_opt, *args, **opts): 18 | print("started {0} with {1}".format(*mod_opt)) 19 | task = import_module("nbpresent.tasks.{}".format(mod_opt[0])) 20 | task.main(**mod_opt[1]) 21 | print("...completed {}".format(mod_opt[0])) 22 | return 0 23 | 24 | 25 | def main(**opts): 26 | clean.main() 27 | 28 | pool = Pool(processes=CPU_COUNT) 29 | 30 | tasks = [ 31 | "less", 32 | "deps", 33 | "index", 34 | "notebook", 35 | "standalone", 36 | ] 37 | 38 | pool.map(_run, zip(tasks, [opts] * len(tasks))) 39 | 40 | requirejs.main(**opts) 41 | 42 | return 0 43 | 44 | if __name__ == "__main__": 45 | opts = {} 46 | args = sys.argv[1:] 47 | if "release" in args: 48 | opts.update( 49 | browserify=["-g", "uglifyify"] 50 | ) 51 | if "dev" in args: 52 | opts.update( 53 | browserify=["--debug"], 54 | less=["--source-map-map-inline"] 55 | ) 56 | 57 | sys.exit(main(**opts)) 58 | -------------------------------------------------------------------------------- /nbpresent/tasks/clean.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import shutil 4 | 5 | from ._env import ( 6 | CSS, 7 | JS 8 | ) 9 | 10 | 11 | def main(**opts): 12 | for clean_dir in [JS, CSS]: 13 | os.path.exists(clean_dir) and shutil.rmtree(clean_dir) 14 | os.makedirs(clean_dir) 15 | 16 | 17 | if __name__ == "__main__": 18 | sys.exit(main()) 19 | -------------------------------------------------------------------------------- /nbpresent/tasks/deps.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen 2 | import sys 3 | 4 | from ._env import ( 5 | JS, 6 | SRC, 7 | join, 8 | node_bin, 9 | external, 10 | extension, 11 | transform, 12 | IS_WIN, 13 | ) 14 | 15 | 16 | def main(**opts): 17 | args = [ 18 | node_bin("browserify"), 19 | "--standalone", "nbpresent-deps", 20 | ] + extension + external( 21 | "jquery", "base/js/namespace", "notebook/js/celltoolbar" 22 | ) + transform + [ 23 | "--outfile", join(JS, "nbpresent.deps.min.js"), 24 | join(SRC, "es6", "vendor.es6") 25 | ] + opts.get("browserify", []) 26 | return Popen(args, shell=IS_WIN).wait() 27 | 28 | 29 | if __name__ == "__main__": 30 | sys.exit(main()) 31 | -------------------------------------------------------------------------------- /nbpresent/tasks/index.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import shutil 3 | 4 | from ._env import ( 5 | JS, 6 | SRC, 7 | join, 8 | ) 9 | 10 | 11 | def main(**opts): 12 | shutil.copyfile( 13 | join(SRC, "js", "index.js"), 14 | join(JS, "nbpresent.min.js") 15 | ) 16 | return 0 17 | 18 | if __name__ == "__main__": 19 | sys.exit(main()) 20 | -------------------------------------------------------------------------------- /nbpresent/tasks/less.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen 2 | import sys 3 | 4 | from ._env import ( 5 | CSS, 6 | SRC, 7 | IS_WIN, 8 | join, 9 | node_bin, 10 | ) 11 | 12 | 13 | def main(**opts): 14 | args = [ 15 | node_bin("lessc"), 16 | "--autoprefix", 17 | "--clean-css", 18 | "--include-path={}".format(node_bin("..")), 19 | join(SRC, "less", "index.less"), 20 | join(CSS, "nbpresent.min.css") 21 | ] + opts.get("less", []) 22 | return Popen(args, shell=IS_WIN).wait() 23 | 24 | 25 | if __name__ == "__main__": 26 | sys.exit(main()) 27 | -------------------------------------------------------------------------------- /nbpresent/tasks/notebook.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen 2 | import sys 3 | 4 | from ._env import ( 5 | JS, 6 | SRC, 7 | join, 8 | node_bin, 9 | external, 10 | extension, 11 | transform, 12 | IS_WIN, 13 | ) 14 | 15 | 16 | def main(**opts): 17 | args = [ 18 | node_bin("browserify"), 19 | "--standalone", "nbpresent-notebook", 20 | ] + extension + external( 21 | "bootstraptour", 22 | "base/js/namespace", 23 | "notebook/js/celltoolbar", 24 | "jquery", 25 | "d3", 26 | "html2canvas", 27 | "baobab", 28 | "uuid", 29 | "nbpresent-deps" 30 | ) + transform + [ 31 | "--outfile", join(JS, "nbpresent.notebook.min.js"), 32 | join(SRC, "es6", "mode", "notebook.es6") 33 | ] + opts.get("browserify", []) 34 | return Popen(args, shell=IS_WIN).wait() 35 | 36 | 37 | if __name__ == "__main__": 38 | sys.exit(main()) 39 | -------------------------------------------------------------------------------- /nbpresent/tasks/requirejs.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen 2 | import sys 3 | 4 | from ._env import ( 5 | SRC, 6 | join, 7 | node_bin, 8 | IS_WIN, 9 | ) 10 | 11 | 12 | def main(**opts): 13 | args = [ 14 | node_bin("r.js{}".format(".cmd" if IS_WIN else "")), 15 | "-o", join(SRC, "js", "build.js"), 16 | ] + opts.get("requirejs", []) 17 | return Popen(args, shell=IS_WIN).wait() 18 | 19 | 20 | if __name__ == "__main__": 21 | sys.exit(main()) 22 | -------------------------------------------------------------------------------- /nbpresent/tasks/standalone.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen 2 | import sys 3 | 4 | from ._env import ( 5 | JS, 6 | SRC, 7 | join, 8 | node_bin, 9 | external, 10 | extension, 11 | transform, 12 | IS_WIN 13 | ) 14 | 15 | 16 | def main(**opts): 17 | args = [ 18 | node_bin("browserify"), 19 | "--standalone", "nbpresent-standalone", 20 | ] + extension + external( 21 | "jquery", 22 | "nbpresent-deps" 23 | ) + transform + [ 24 | "--outfile", join(JS, "nbpresent.standalone.min.js"), 25 | join(SRC, "es6", "mode", "standalone.es6") 26 | ] + opts.get("browserify", []) 27 | return Popen(args, shell=IS_WIN).wait() 28 | 29 | 30 | if __name__ == "__main__": 31 | sys.exit(main()) 32 | -------------------------------------------------------------------------------- /nbpresent/templates/nbpresent.tpl: -------------------------------------------------------------------------------- 1 | {%- extends 'full.tpl' -%} 2 | 3 | 4 | {% macro nbpresent_id(cell) -%} 5 | {% if cell.metadata.nbpresent %} 6 | data-nbp-id="{{ cell.metadata.nbpresent.id }}" 7 | {% endif %} 8 | {%- endmacro %} 9 | 10 | 11 | {% block codecell %} 12 |
13 | {{ super() }} 14 |
15 | {%- endblock codecell %} 16 | 17 | 18 | {% block markdowncell scoped %} 19 |
20 | {{ self.empty_in_prompt() }} 21 |
22 |
23 | {{ cell.source | markdown2html | strip_files_prefix }} 24 |
25 |
26 |
27 | {%- endblock markdowncell %} 28 | 29 | 30 | {%- block html_head -%} 31 | 32 | {{resources['metadata']['name']}} 33 | 34 | {% for css in resources.inlining.css -%} 35 | 38 | {% endfor %} 39 | 40 | 52 | 53 | 54 | {% for fname, css in resources.nbpresent_assets.css.items() -%} 55 | 58 | {% endfor %} 59 | 60 | 61 | 62 | 63 | {{ mathjax() }} 64 | 65 | {% for fname, js in resources.nbpresent_assets.js.items() -%} 66 | 69 | {% endfor %} 70 | 71 | 72 | 75 | 76 | {%- endblock html_head -%} 77 | -------------------------------------------------------------------------------- /nbpresent/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaconda/nbpresent/6bf2f50b7abd27273dc9e7d469bf1866bafbf5bb/nbpresent/tests/__init__.py -------------------------------------------------------------------------------- /nbpresent/tests/js/_utils.js: -------------------------------------------------------------------------------- 1 | /* globals require casper Jupyter */ 2 | var system = require('system'); 3 | 4 | var root = casper, 5 | _img = 0, 6 | node_modules = system.env.NBPRESENT_TEST_MODULES || "../../../node_modules", 7 | vendor = root.vendor = root.vendor ? root.vendor : (root.vendor = {}), 8 | _ = vendor._ = require(node_modules + "/lodash"), 9 | _shotDir = "_unnamed"; 10 | 11 | function nextId(){ 12 | return ("000" + (_img++)).slice(-4); 13 | } 14 | 15 | function slug(text){ 16 | return text.replace(/[^a-z0-9]/g, "_"); 17 | } 18 | 19 | 20 | root.screenshot = function(message){ 21 | return this.captureSelector([ 22 | "screenshots/", 23 | _shotDir, 24 | "/", 25 | nextId(), 26 | "_", 27 | slug(message), 28 | ".png" 29 | ].join(""), 30 | "body" 31 | ); 32 | }; 33 | 34 | 35 | root.screenshot.init = function(ns){ 36 | _shotDir = ns; 37 | _img = 0; 38 | return root; 39 | }; 40 | 41 | 42 | root.canSeeAndClick = function(message, visible, click){ 43 | var things = message; 44 | 45 | if(arguments.length !== 1){ 46 | things = [[message, visible, click]]; 47 | } 48 | 49 | var that = this; 50 | 51 | things.map(function(thing){ 52 | var message = thing[0], 53 | visible = thing[1], 54 | click = thing[2]; 55 | 56 | that = that 57 | .waitUntilVisible(visible) 58 | .then(function(){ 59 | this.test.assertExists(click || visible, "I can see and click " + message); 60 | this.screenshot(message); 61 | this.click(click || visible); 62 | }); 63 | }); 64 | 65 | return that; 66 | } 67 | 68 | root.hasMeta = function(path, tests){ 69 | var meta; 70 | 71 | this.thenEvaluate(function () { 72 | require(['base/js/events'], function(events) { 73 | Jupyter._save_success = Jupyter._save_failed = false; 74 | events.on('notebook_saved.Notebook', function () { 75 | Jupyter._save_success = true; 76 | }); 77 | events.on('notebook_save_failed.Notebook', 78 | function (event, error) { 79 | Jupyter._save_failed = "save failed with " + error; 80 | }); 81 | Jupyter.notebook.save_notebook(); 82 | }); 83 | }); 84 | 85 | this.waitFor(function () { 86 | return this.evaluate(function(){ 87 | return Jupyter._save_failed || Jupyter._save_success; 88 | }); 89 | }); 90 | 91 | return this 92 | .then(function(){ 93 | meta = this.evaluate(function(){ 94 | return Jupyter.notebook.metadata; 95 | }); 96 | }) 97 | .then(function(){ 98 | _.map(tests, function(test, message){ 99 | var results = test(_.get(meta, path)); 100 | this.test.assertEquals(results[0], results[1], 101 | "I remember " + message + " in " + path); 102 | }, this); 103 | }); 104 | }; 105 | 106 | root.dragRelease = function(message, selector, opts){ 107 | var it, x, y, x1, y1; 108 | return this.then(function(){ 109 | it = this.getElementBounds(selector); 110 | x = it.left + it.width / 2; 111 | y = it.top + it.height / 2; 112 | x1 = x + (opts.right || -opts.left || 0); 113 | y1 = y + (opts.down || -opts.up || 0); 114 | }) 115 | .then(function(){ 116 | this.mouse.down(x, y); 117 | }) 118 | .then(function(){ 119 | this.screenshot("click " + message); 120 | this.mouse.move(x1, y1); 121 | }) 122 | .then(function(){ 123 | this.screenshot("drag " + message); 124 | this.mouse.up(x1, y1); 125 | }) 126 | .then(function(){ 127 | this.screenshot("release " + message); 128 | }); 129 | }; 130 | 131 | root.baseline_notebook = function(){ 132 | // the actual test 133 | 134 | this.append_cell(); 135 | this.set_cell_text(1, [ 136 | 'from IPython.display import Markdown', 137 | 'Markdown("# Hello World!")' 138 | ].join("\n")); 139 | this.execute_cell_then(1); 140 | 141 | this.append_cell(); 142 | this.set_cell_text(2, [ 143 | 'from ipywidgets import FloatSlider', 144 | 'x = FloatSlider()', 145 | 'x' 146 | ].join("\n")); 147 | this.execute_cell_then(2); 148 | } 149 | 150 | root.saveNbpresent = function (){ 151 | return this.thenEvaluate(function(){ 152 | window.nbpresent.mode.metadata(true); 153 | }); 154 | } 155 | -------------------------------------------------------------------------------- /nbpresent/tests/js/test_export_html.js: -------------------------------------------------------------------------------- 1 | /* global casper require */ 2 | 3 | var system = require('system'), 4 | host = system.env.NBPRESENT_TEST_HTTP_HOST || "localhost", 5 | port = system.env.NBPRESENT_TEST_HTTP_PORT || 8000, 6 | root = "http://" + host + ":" + port + "/"; 7 | 8 | casper.test.begin("Does exported HTML look okay?", function(test){ 9 | casper.start(root + "Basics.html", function(){ 10 | casper.screenshot.init("export_html"); 11 | casper.viewport(1440, 900, export_test) 12 | }).run(function(){ 13 | test.done(); 14 | }); 15 | }); 16 | 17 | function export_test(){ 18 | return this.canSeeAndClick("body", "body.nbp-presenting") 19 | .canSeeAndClick("markdown", ".text_cell h1") 20 | .canSeeAndClick("code source", ".code_cell .input_area") 21 | .canSeeAndClick("code output", ".code_cell .output_text") 22 | .canSeeAndClick("svg container", ".code_cell .output_svg") 23 | .then(function(){ return this.mouse.move(1430, 890); }) 24 | .wait(300) 25 | .canSeeAndClick("next slide button", ".fa-step-forward") 26 | .canSeeAndClick("embedded image", ".output_png") 27 | .then(function(){ return this.mouse.move(1430, 890); }) 28 | .canSeeAndClick("previous slide button", ".fa-step-backward") 29 | .wait(300) 30 | .canSeeAndClick("fin", "body.nbp-presenting"); 31 | } 32 | -------------------------------------------------------------------------------- /nbpresent/tests/js/test_notebook_basic.js: -------------------------------------------------------------------------------- 1 | /* global casper */ 2 | casper.notebook_test(function(){ 3 | casper.screenshot.init("basic"); 4 | casper.viewport(1440, 900) 5 | .then(basic_test); 6 | }); 7 | 8 | function basic_test(){ 9 | var t = casper.test; 10 | 11 | this.baseline_notebook(); 12 | 13 | this.then(function(){ 14 | this.canSeeAndClick("the body", "body"); 15 | }) 16 | .then(function(){ 17 | ["#nbp-app-btn", "#nbp-present-btn", ".download_nbpresent"] 18 | .map(function(selector){ t.assertExists(selector); }); 19 | 20 | loaded("nbpresent.min.css"); 21 | }); 22 | 23 | // TODO: move to utils? 24 | function loaded(pattern){ 25 | t.assertResourceExists(function(resource) { 26 | return resource.url.match(pattern); 27 | }); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /nbpresent/tests/js/test_notebook_create.js: -------------------------------------------------------------------------------- 1 | /* global casper */ 2 | 3 | casper.notebook_test(function(){ 4 | casper.screenshot.init("create"); 5 | casper.viewport(1440, 900) 6 | .then(create_test); 7 | }); 8 | 9 | function create_test(){ 10 | var _ = this.vendor._, 11 | freeRegion = ".nbp-slides-wrap .slide .nbp-region.nbp-content-null"; 12 | 13 | this.baseline_notebook(); 14 | 15 | this.canSeeAndClick([ 16 | ["the sorter button", "#nbp-app-btn"], 17 | ["the slides button", ".nbp-app-bar .fa-film"], 18 | ["the add slide button", ".nbp-deck-toolbar .fa-plus-square-o"], 19 | ["a slide template in the library", 20 | ".nbp-template-library .slide:last-of-type"] 21 | ]) 22 | .waitWhileVisible(".nbp-template-library") 23 | .saveNbpresent() 24 | .hasMeta("nbpresent.slides", { 25 | "one slide": function(slides){ 26 | return [_(slides).toArray().value().length, 1]; 27 | }, 28 | "four regions": function(slides){ 29 | var regions = _(_(slides).toArray().first().regions) 30 | .toArray() 31 | .value(); 32 | return [regions.length, 4]; 33 | } 34 | }) 35 | .canSeeAndClick([ 36 | ["a region in the sorter", ".nbp-slides-wrap .slide .nbp-region"], 37 | ["the link mode button", ".nbp-region-toolbar .fa-link"], 38 | ["a linkable source", ".nbp-part-overlay-source"], 39 | ["a region in the sorter", freeRegion], 40 | ["the link mode button", ".nbp-region-toolbar .fa-link"], 41 | ["a linkable output", ".nbp-part-overlay-outputs"], 42 | ["a region in the sorter", freeRegion], 43 | ["the link mode button", ".nbp-region-toolbar .fa-link"], 44 | ["a linkable widget", ".nbp-part-overlay-widgets"], 45 | ["a region in the sorter", freeRegion], 46 | ["the link mode button", ".nbp-region-toolbar .fa-link"], 47 | ["a linkable whole", ".nbp-part-overlay-whole"], 48 | ["the presenter button", "#nbp-present-btn"], 49 | ["the presenter", ".nbp-presenter"] 50 | ]) 51 | .then(function(){ 52 | return this.mouse.move(1430, 890); 53 | }) 54 | .wait(1) 55 | .canSeeAndClick([ 56 | ["the return to notebook button", ".fa-book"], 57 | ["the edit button", ".nbp-deck-toolbar .fa-edit"], 58 | ["a region in the region tree", ".nbp-regiontree .nbp-region"], 59 | ["the treemap layout button", ".nbp-regiontree .fa-tree"], 60 | ["the weight attribute", ".attr_name"] 61 | ]) 62 | .dragRelease("the weight attribute", ".attr_name", {right: 50}) 63 | .canSeeAndClick("the manual layout button", ".nbp-regiontree .fa-arrows") 64 | .dragRelease("a draggable region", 65 | ".nbp-editor .nbp-region.active .nbp-region-bg", {right: 50}) 66 | .canSeeAndClick([ 67 | ["the exit edit mode button", ".fa-chevron-circle-down"], 68 | ["the sorter button", "#nbp-app-btn"] 69 | ]) 70 | .waitWhileVisible(".nbp-sorter") 71 | .waitWhileVisible(".nbp-regiontree") 72 | .canSeeAndClick([ 73 | ["the presenter button", "#nbp-present-btn"], 74 | ["the presenter", ".nbp-presenter"] 75 | ]); 76 | } 77 | -------------------------------------------------------------------------------- /nbpresent/tests/js/test_notebook_no_regions.js: -------------------------------------------------------------------------------- 1 | /* global casper */ 2 | 3 | casper.notebook_test(function(){ 4 | casper.screenshot.init("empty"); 5 | casper.viewport(1440, 900) 6 | .then(empty_test); 7 | }); 8 | 9 | var freeRegion = ".nbp-slides-wrap .slide .nbp-region.nbp-content-null"; 10 | 11 | 12 | function empty_test(){ 13 | var _ = this.vendor._; 14 | 15 | this.baseline_notebook(); 16 | 17 | this.canSeeAndClick([ 18 | ["the sorter button", "#nbp-app-btn"], 19 | ["the slides button", ".nbp-app-bar .fa-film"], 20 | ["the add slide button", ".nbp-deck-toolbar .fa-plus-square-o"], 21 | ["a slide template in the library", 22 | ".nbp-template-library .slide:first-of-type"] 23 | ]) 24 | .waitWhileVisible(".nbp-template-library") 25 | .canSeeAndClick([ 26 | ["the edit button", ".nbp-deck-toolbar .fa-edit"], 27 | ["a region in the sorter", freeRegion], 28 | ["the unlink button", ".nbp-region-toolbar .fa-trash"] 29 | ]) 30 | .saveNbpresent() 31 | .waitWhileSelector([ 32 | ".nbp-slides-wrap .slide .nbp-region", 33 | ".nbp-regiontree .nbp-region", 34 | ".nbp-editor .nbp-region" 35 | ].join(", ")) 36 | .hasMeta("nbpresent.slides", { 37 | "one slide": function(slides){ 38 | return [_(slides).toArray().value().length, 1]; 39 | }, 40 | "no regions": function(slides){ 41 | var regions = _(_(slides).toArray().first().regions) 42 | .toArray() 43 | .value(); 44 | return [regions.length, 0]; 45 | } 46 | }) 47 | .canSeeAndClick([ 48 | ["fin", "body"] 49 | ]) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /nbpresent/tests/test_export.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import os 3 | import socket 4 | import subprocess 5 | import tempfile 6 | import sys 7 | import platform 8 | 9 | from ..export import export 10 | 11 | 12 | IS_WIN = "Windows" in platform.system() 13 | 14 | 15 | # utility function 16 | def join(*bits): 17 | return os.path.abspath(os.path.join(*bits)) 18 | 19 | 20 | here = os.path.dirname(__file__) 21 | 22 | http_module = None 23 | 24 | 25 | def node_bin(*bits): 26 | return os.path.abspath( 27 | os.path.join(here, "..", "..", "node_modules", ".bin", *bits)) 28 | 29 | try: 30 | import http.server as httpd 31 | http_module = "http.server" 32 | except ImportError: 33 | import SimpleHTTPServer as httpd 34 | http_module = "SimpleHTTPServer" 35 | 36 | print("nbpresent export test using", httpd) 37 | 38 | 39 | def unused_port(): 40 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 41 | sock.bind(('localhost', 0)) 42 | addr, port = sock.getsockname() 43 | sock.close() 44 | return port 45 | 46 | 47 | def test_export(): 48 | tmpdir = tempfile.mkdtemp() 49 | env = dict(os.environ) 50 | env.update( 51 | NBPRESENT_TEST_HTTP_PORT=str(unused_port()), 52 | PATH=os.pathsep.join([env["PATH"], node_bin()]) 53 | ) 54 | 55 | httpd = subprocess.Popen([ 56 | sys.executable, "-m", http_module, 57 | env["NBPRESENT_TEST_HTTP_PORT"], 58 | "--bind=127.0.0.1" 59 | ], 60 | cwd=tmpdir) 61 | 62 | try: 63 | export( 64 | join(here, "notebooks", "Basics.ipynb"), 65 | join(tmpdir, "Basics.html"), 66 | "html") 67 | args = [ 68 | "casperjs{}".format(".cmd" if IS_WIN else ""), 69 | "test", 70 | "--fail-fast", 71 | "--includes={}".format( 72 | ",".join(glob(join(here, 'js', '_*.js')))), 73 | ] + glob(join(here, 'js', 'test_export_*.js')) 74 | 75 | proc = subprocess.Popen( 76 | args, 77 | stderr=subprocess.PIPE, 78 | env=env 79 | ) 80 | proc.communicate() 81 | 82 | assert proc.returncode == 0 83 | finally: 84 | httpd.kill() 85 | 86 | 87 | if __name__ == '__main__': 88 | test_export() 89 | -------------------------------------------------------------------------------- /nbpresent/tests/test_notebook.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import glob 3 | import os 4 | import subprocess 5 | 6 | try: 7 | from unittest.mock import patch 8 | except ImportError: 9 | # py2 10 | from mock import patch 11 | 12 | from notebook import jstest 13 | 14 | import platform 15 | 16 | IS_WIN = "Windows" in platform.system() 17 | 18 | here = os.path.dirname(__file__) 19 | 20 | # global npm installs are bad, add the local node_modules to the path 21 | os.environ["PATH"] = os.pathsep.join([ 22 | os.environ["PATH"], 23 | os.path.abspath(os.path.join(here, "node_modules", ".bin")) 24 | ]) 25 | 26 | TEST_LOG = ".jupyter.jstest.log" 27 | 28 | 29 | class NBPresentTestController(jstest.JSController): 30 | """ Javascript test subclass that installs widget nbextension in test 31 | environment 32 | """ 33 | def __init__(self, section, *args, **kwargs): 34 | extra_args = kwargs.pop('extra_args', None) 35 | super(NBPresentTestController, self).__init__(section, *args, **kwargs) 36 | self.xunit = True 37 | 38 | test_cases = glob.glob(os.path.join(here, 'js', 'test_notebook_*.js')) 39 | js_test_dir = jstest.get_js_test_dir() 40 | 41 | includes = [ 42 | os.path.join(js_test_dir, 'util.js') 43 | ] + glob.glob(os.path.join(here, 'js', '_*.js')) 44 | 45 | self.cmd = [ 46 | 'casperjs', 'test', 47 | '--includes={}'.format(",".join(includes)), 48 | '--engine={}'.format(self.engine) 49 | ] + test_cases 50 | 51 | if extra_args is not None: 52 | self.cmd = self.cmd + extra_args 53 | 54 | if IS_WIN: 55 | self.cmd[0] = "{}.cmd".format(self.cmd[0]) 56 | 57 | def launch(self, buffer_output=False, capture_output=False): 58 | # print('*** ENV:', self.env) # dbg 59 | # print('*** CMD:', self.cmd) # dbg 60 | env = os.environ.copy() 61 | env.update(self.env) 62 | if buffer_output: 63 | capture_output = True 64 | self.stdout_capturer = c = jstest.StreamCapturer( 65 | echo=not buffer_output) 66 | c.start() 67 | stdout = c.writefd if capture_output else None 68 | # stderr = subprocess.STDOUT if capture_output else None 69 | self.process = subprocess.Popen( 70 | self.cmd, 71 | stderr=subprocess.PIPE, 72 | stdout=stdout, 73 | env=env) 74 | 75 | def wait(self): 76 | self.process.communicate() 77 | self.stdout_capturer.halt() 78 | self.stdout = self.stdout_capturer.get_buffer() 79 | return self.process.returncode 80 | 81 | # copy pasta from... 82 | # https://github.com/jupyter/notebook/blob/master/notebook/jstest.py#L234 83 | def setup(self): 84 | self.ipydir = jstest.TemporaryDirectory() 85 | self.config_dir = jstest.TemporaryDirectory() 86 | self.nbdir = jstest.TemporaryDirectory() 87 | self.home = jstest.TemporaryDirectory() 88 | self.env = { 89 | 'HOME': self.home.name, 90 | 'JUPYTER_CONFIG_DIR': self.config_dir.name, 91 | 'IPYTHONDIR': self.ipydir.name, 92 | } 93 | self.dirs.append(self.ipydir) 94 | self.dirs.append(self.home) 95 | self.dirs.append(self.config_dir) 96 | self.dirs.append(self.nbdir) 97 | os.makedirs(os.path.join(self.nbdir.name, 98 | os.path.join(u'sub dir1', u'sub dir 1a'))) 99 | os.makedirs(os.path.join(self.nbdir.name, 100 | os.path.join(u'sub dir2', u'sub dir 1b'))) 101 | 102 | if self.xunit: 103 | self.add_xunit() 104 | 105 | # If a url was specified, use that for the testing. 106 | if self.url: 107 | try: 108 | alive = jstest.requests.get(self.url).status_code == 200 109 | except: 110 | alive = False 111 | 112 | if alive: 113 | self.cmd.append("--url=%s" % self.url) 114 | else: 115 | raise Exception('Could not reach "%s".' % self.url) 116 | else: 117 | # start the ipython notebook, so we get the port number 118 | self.server_port = 0 119 | self._init_server() 120 | if self.server_port: 121 | self.cmd.append('--url=http://localhost:%i%s' % ( 122 | self.server_port, self.base_url)) 123 | else: 124 | # don't launch tests if the server didn't start 125 | self.cmd = [ 126 | jstest.sys.executable, '-c', 'raise SystemExit(1)'] 127 | 128 | def _init_server(self): 129 | "Start the notebook server in a separate process" 130 | self.server_command = command = [ 131 | jstest.sys.executable, 132 | '-m', 'notebook', 133 | '--no-browser', 134 | '--notebook-dir', self.nbdir.name, 135 | '--NotebookApp.base_url=%s' % self.base_url, 136 | ] 137 | # ipc doesn't work on Windows, and darwin has crazy-long temp paths, 138 | # which run afoul of ipc's maximum path length. 139 | # if jstest.sys.platform.startswith('linux'): 140 | # command.append('--KernelManager.transport=ipc') 141 | self.stream_capturer = c = jstest.StreamCapturer() 142 | c.start() 143 | env = os.environ.copy() 144 | env.update(self.env) 145 | # if self.engine == 'phantomjs': 146 | # env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1' 147 | self.server = subprocess.Popen( 148 | command, 149 | stdout=c.writefd, 150 | stderr=jstest.subprocess.STDOUT, 151 | cwd=self.nbdir.name, 152 | env=env, 153 | ) 154 | with patch.dict('os.environ', {'HOME': self.home.name}): 155 | runtime_dir = jstest.jupyter_runtime_dir() 156 | self.server_info_file = os.path.join( 157 | runtime_dir, 158 | 'nbserver-%i.json' % self.server.pid 159 | ) 160 | self._wait_for_server() 161 | 162 | def cleanup(self): 163 | if hasattr(self, "stream_capturer"): 164 | captured = self.stream_capturer.get_buffer().decode( 165 | 'utf-8', 'replace') 166 | with codecs.open(TEST_LOG, "a+", encoding="utf-8") as fp: 167 | fp.write("-----------------------\n{} results:\n{}\n".format( 168 | self.section, 169 | self.server_command)) 170 | fp.write(captured) 171 | print("-----\nJUPYTER JSTESTLOG") 172 | print(captured) 173 | print("-----\nEND JUPYTER JSTESTLOG") 174 | 175 | super(NBPresentTestController, self).cleanup() 176 | 177 | 178 | def prepare_controllers(options): 179 | """Monkeypatched prepare_controllers for running widget js tests 180 | 181 | instead of notebook js tests 182 | """ 183 | if options.testgroups: 184 | groups = options.testgroups 185 | else: 186 | groups = [''] 187 | return [NBPresentTestController(g, extra_args=options.extra_args) 188 | for g in groups], [] 189 | 190 | 191 | def test_notebook(): 192 | with patch.object(jstest, 'prepare_controllers', prepare_controllers): 193 | jstest.main() 194 | 195 | 196 | if __name__ == '__main__': 197 | test_notebook() 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Nicholas Bollweg", 4 | "url": "https://github.com/bollwyvl" 5 | }, 6 | "bugs": { 7 | "url": "https://github.com/Anaconda-Platform/nbpresent/issues" 8 | }, 9 | "chat": "https://gitter.im/Anaconda-Platform/nbpresent", 10 | "dependencies": { 11 | "baobab": "^2.2.1", 12 | "d3": "~3.5.12", 13 | "html2canvas": "^0.5.0-beta4", 14 | "phantomjs-polyfill": "0.0.1", 15 | "underscore": "^1.8.3", 16 | "uuid": "^2.0.1", 17 | "webfontloader": "^1.6.21" 18 | }, 19 | "description": "This will be the home of the next generation of slide authoring presentation for [Jupyter Notebooks](https://github.com/jupyter/notebook).", 20 | "devDependencies": { 21 | "babel": "^6.1.18", 22 | "babel-eslint": "^4.1.8", 23 | "babel-preset-es2015": "^6.1.18", 24 | "babelify": "^7.2.0", 25 | "browserify": "~11.2.0", 26 | "casperjs": "^1.1.1", 27 | "esdoc": "^0.4.0", 28 | "eslint": "^1.10.3", 29 | "jquery": "^2.2.0", 30 | "less-plugin-autoprefix": "~1.5.1", 31 | "less-plugin-clean-css": "~1.5.1", 32 | "less": "~2.5.3", 33 | "lodash": "^3.10.1", 34 | "phantomjs-prebuilt": "^2.1.7", 35 | "requirejs": "^2.1.22", 36 | "uglifyify": "~3.0.1", 37 | "watch": "~0.16.0" 38 | }, 39 | "discussion": "https://groups.google.com/forum/#!forum/nbpresent", 40 | "eslintConfig": { 41 | "env": { 42 | "browser": true, 43 | "es6": true 44 | }, 45 | "extends": "eslint:recommended", 46 | "parser": "babel-eslint", 47 | "rules": { 48 | "strict": 0 49 | } 50 | }, 51 | "homepage": "https://github.com/Anaconda-Platform/nbpresent", 52 | "license": "BSD-3-Clause", 53 | "main": "index.js", 54 | "name": "nbpresent", 55 | "repository": { 56 | "type": "git", 57 | "url": "git://github.com/Anaconda-Platform/nbpresent.git" 58 | }, 59 | "scripts": { 60 | "all": "npm run dist && npm run lint && npm run test && npm run docs", 61 | "build": "python -m nbpresent.tasks.build", 62 | "build:deps": "python -m nbpresent.tasks.deps", 63 | "build:dev": "python -m nbpresent.tasks.build dev", 64 | "build:index": "python -m nbpresent.tasks.index", 65 | "build:less": "python -m nbpresent.tasks.less", 66 | "build:notebook": "python -m nbpresent.tasks.notebook", 67 | "build:release": "python -m nbpresent.tasks.build release", 68 | "build:requirejs": "python -m nbpresent.tasks.requirejs", 69 | "build:standalone": "python -m nbpresent.tasks.standalone", 70 | "clean": "python -m nbpresent.tasks.clean", 71 | "docs": "npm run docs:clean && npm run docs:esdoc && npm run docs:sphinx && npm run docs:notebooks", 72 | "docs:clean": "rm -rf nbpresent/static/docs", 73 | "docs:esdoc": "esdoc -c .esdoc", 74 | "docs:notebooks": "nbpresent -i notebooks/index.ipynb -o nbpresent/static/index.html", 75 | "docs:sphinx": "sphinx-apidoc -f -o ./sphinx/source/auto nbpresent && cd sphinx && make html && mv -f build/html ../nbpresent/static/docs/sphinx", 76 | "less": "python -m nbpresent.tasks.less", 77 | "lint": "npm run lint:eslint && npm run lint:flake8", 78 | "lint:eslint": "eslint --ext es6 ./src", 79 | "lint:flake8": "flake8 nbpresent setup.py sphinx", 80 | "pkg:conda": "conda build conda.recipe -c javascript -c mutirri -c cpcloud -c anaconda-nb-extensions -c bokeh -c wakari", 81 | "pkg:pypi": "python setup.py register -r pypitest && python setup.py sdist && python setup.py bdist_wheel && python setup.py sdist upload -r pypitest && python setup.py bdist_wheel upload -r pypitest", 82 | "pkg:pypi:release": "python setup.py register -r pypi && python setup.py sdist && python setup.py bdist_wheel && python setup.py sdist bdist_wheel upload -r pypi", 83 | "readme": "jupyter nbconvert notebooks/README.ipynb --to=rst --stdout > README.rst && jupyter nbconvert notebooks/README.ipynb --to=markdown --stdout > README.md", 84 | "test": "python -m nose nbpresent.tests", 85 | "watch": "watch 'npm run build:dev' src", 86 | "watch:test": "watch 'npm run test' ./nbpresent/static/nbpresent/js" 87 | }, 88 | "version": "3.0.2" 89 | } 90 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaconda/nbpresent/6bf2f50b7abd27273dc9e7d469bf1866bafbf5bb/screenshot.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [nosetests] 5 | verbosity=1 6 | detailed-errors=1 7 | with-coverage=1 8 | cover-package=nbpresent 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from setuptools import setup 4 | 5 | 6 | # should be loaded below 7 | __version__ = None 8 | 9 | with open('nbpresent/_version.py') as version: 10 | exec(version.read()) 11 | 12 | with open('./README.rst') as readme: 13 | README = readme.read() 14 | 15 | setup( 16 | name="nbpresent", 17 | version=__version__, 18 | description="Next generation slides from Jupyter Notebooks", 19 | long_description=README, 20 | author="Nicholas Bollweg", 21 | author_email="nbollweg@continuum.io", 22 | license="BSD-3-Clause", 23 | url="https://github.com/Anaconda-Platform/nbpresent", 24 | keywords="ipython jupyter markdown presentation slides revealjs d3", 25 | classifiers=[ 26 | "Development Status :: 4 - Beta", 27 | "Framework :: IPython", 28 | "Programming Language :: Python", 29 | "License :: OSI Approved :: BSD License" 30 | ], 31 | packages=["nbpresent"], 32 | setup_requires=["notebook"], 33 | tests_require=["nose", "requests", "coverage"], 34 | include_package_data=True, 35 | entry_points={ 36 | 'console_scripts': ['nbpresent=nbpresent.export:main'], 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /sphinx/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/nbpresent.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/nbpresent.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/nbpresent" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/nbpresent" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /sphinx/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\nbpresent.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\nbpresent.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /sphinx/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from nbpresent._version import __version__ 3 | 4 | 5 | extensions = [ 6 | 'sphinx.ext.autodoc', 7 | 'sphinx.ext.intersphinx', 8 | 'sphinx.ext.todo', 9 | 'sphinx.ext.coverage', 10 | 'sphinx.ext.viewcode', 11 | ] 12 | 13 | templates_path = ['_templates'] 14 | source_suffix = '.rst' 15 | master_doc = 'index' 16 | project = 'nbpresent' 17 | copyright = '2015, Continuum Analytics' 18 | author = 'Nicholas Bollweg' 19 | version = __version__ 20 | release = __version__ 21 | language = None 22 | exclude_patterns = [] 23 | pygments_style = 'sphinx' 24 | todo_include_todos = True 25 | html_theme = 'alabaster' 26 | html_static_path = ['_static'] 27 | htmlhelp_basename = 'nbpresentdoc' 28 | latex_elements = {} 29 | latex_documents = [ 30 | (master_doc, 'nbpresent.tex', 'nbpresent Documentation', 31 | 'Nicholas Bollweg', 'manual'), 32 | ] 33 | man_pages = [(master_doc, 'nbpresent', 'nbpresent Documentation', [author], 1)] 34 | texinfo_documents = [ 35 | (master_doc, 'nbpresent', 'nbpresent Documentation', author, 'nbpresent', 36 | 'One line description of project.', 'Miscellaneous'), 37 | ] 38 | intersphinx_mapping = {'https://docs.python.org/': None} 39 | -------------------------------------------------------------------------------- /sphinx/source/index.rst: -------------------------------------------------------------------------------- 1 | .. nbpresent documentation master file, created by 2 | sphinx-quickstart on Tue Dec 15 07:43:19 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to nbpresent's documentation! 7 | ===================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | 15 | .. automodule:: nbpresent 16 | :members: 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /src/es6/actions/base.es6: -------------------------------------------------------------------------------- 1 | export const NS = "nbpresent"; 2 | 3 | export class BaseActions { 4 | constructor(keyActions=[]){ 5 | this.keyActions = keyActions; 6 | this.register(); 7 | } 8 | 9 | register(){ 10 | 11 | } 12 | 13 | push(){ 14 | } 15 | 16 | pop(){ 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/es6/actions/notebook.es6: -------------------------------------------------------------------------------- 1 | import Jupyter from "base/js/namespace"; 2 | import {BaseActions, NS} from "./base"; 3 | 4 | export class NotebookActions extends BaseActions{ 5 | register(){ 6 | this._kbm = Jupyter.notebook.keyboard_manager; 7 | 8 | this.keyActions.map(({name, value}) => { 9 | this._kbm.actions.register(value, name, NS); 10 | }); 11 | } 12 | 13 | push(){ 14 | this.keyActions.map(({name, keys=[]}) => { 15 | keys.map((key) => { 16 | this._kbm.command_shortcuts.add_shortcut(key, `${NS}:${name}`); 17 | }) 18 | }); 19 | } 20 | 21 | pop(){ 22 | this.keyActions.map(({keys=[], name}) => { 23 | keys.map((key) => { 24 | if(this._kbm.command_shortcuts.get_shortcut(key) === `${NS}:${name}`){ 25 | this._kbm.command_shortcuts.remove_shortcut(key); 26 | } 27 | }); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/es6/actions/standalone.es6: -------------------------------------------------------------------------------- 1 | import {BaseActions} from "./base"; 2 | 3 | export class StandaloneActions extends BaseActions{ 4 | } 5 | -------------------------------------------------------------------------------- /src/es6/cells/base.es6: -------------------------------------------------------------------------------- 1 | /** @external {Cell} https://github.com/jupyter/notebook/blob/master/notebook/static/notebook/js/cell.js */ 2 | 3 | import {d3, html2canvas} from "nbpresent-deps"; 4 | 5 | import {log} from "../logger"; 6 | import {PART, PART_SELECT} from "../parts"; 7 | 8 | let _THUMBS = {}; 9 | 10 | 11 | export class BaseCellManager { 12 | /** return an id-indexed set of {@link Cell}-like objects: each has the 13 | * a fake jQuery `element` member), as embedded in HTML 14 | * @abstract 15 | * @return {Object} */ 16 | getCells(){ 17 | throw Error("Not Implemented"); 18 | } 19 | 20 | getPart(content, element){ 21 | let cell = this.getCells()[content.cell]; 22 | 23 | if(!cell && !element){ 24 | return null; 25 | } 26 | 27 | let $el = d3.select(element || cell.element[0]), 28 | part = content.part === PART.whole ? 29 | $el : $el.select(PART_SELECT[content.part]); 30 | 31 | return part; 32 | } 33 | 34 | clearThumbnails(contents=[]){ 35 | let keys = Object.keys(_THUMBS); 36 | if(contents.length){ 37 | keys = contents.map((content) => this.contentKey(content)); 38 | } 39 | keys.map((key)=>{ delete _THUMBS[key]; }); 40 | } 41 | 42 | contentKey(content){ 43 | return `${content.cell}-${content.part}`; 44 | } 45 | 46 | thumbnail(content){ 47 | let el = this.getPart(content); 48 | el = el ? el.node() : null; 49 | if(!el){ 50 | return Promise.reject(content); 51 | } 52 | return html2canvas(el) 53 | .then((canvas) => { 54 | return { 55 | canvas, 56 | uri: canvas.toDataURL("image/png"), 57 | width: canvas.width, 58 | height: canvas.height 59 | }; 60 | }, (err) => log.error("thumbnail error", content, err)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/es6/cells/notebook.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | import Jupyter from "base/js/namespace"; 3 | 4 | import {PART_SELECT} from "../parts"; 5 | 6 | import {BaseCellManager} from "./base"; 7 | 8 | 9 | /** Wraps the Jupyter Notebook for working with {@link Cell}s 10 | * @extends {BaseCellManager} */ 11 | export class NotebookCellManager extends BaseCellManager { 12 | /** returns an id-indexed set of {@link Cells} from the main namespace i.e. 13 | * `window.Jupyter` 14 | * @return {Object} */ 15 | getCells(){ 16 | return Jupyter.notebook.get_cells().reduce((memo, cell)=> { 17 | if(cell.metadata.nbpresent){ 18 | memo[cell.metadata.nbpresent.id] = cell; 19 | } 20 | return memo; 21 | }, {}); 22 | } 23 | 24 | cellId(cell){ 25 | try { 26 | return cell.metadata.nbpresent.id; 27 | }catch(err){ 28 | return null; 29 | } 30 | } 31 | 32 | /** 33 | Generate a sparse matrix of: 34 | cell * part * {cell, part, bb} 35 | ] 36 | */ 37 | cellPartGeometry(){ 38 | let mgr = this; 39 | return Jupyter.notebook.get_cells().map((cell)=>{ 40 | let key = this.cellId(cell); 41 | 42 | let parts = d3.entries(PART_SELECT).map((part)=>{ 43 | return mgr.getPart({cell: key, part: part.key}, cell.element[0]) 44 | .reduce(function(memo, data){ 45 | let el = data[0], 46 | bb = el && el.getBoundingClientRect(); 47 | if(bb && bb.height){ 48 | memo.push({cell: {key, value: cell}, part: part.key, bb}); 49 | } 50 | return memo; 51 | }, []); 52 | }); 53 | return parts; 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/es6/cells/overlay.es6: -------------------------------------------------------------------------------- 1 | import {d3, _} from "nbpresent-deps"; 2 | 3 | import {PART} from "../parts"; 4 | 5 | export class LinkOverlay{ 6 | constructor(manager, done){ 7 | this.manager = manager; 8 | this.done = done; 9 | this.init(); 10 | } 11 | 12 | init(){ 13 | this.$body = d3.select("body"); 14 | this.$header = this.$body.select("#header"); 15 | this.$site = this.$body.select("#site"); 16 | this.$ui = this.$body.select("#notebook-container") 17 | .append("div") 18 | .classed({"nbp-link-overlay": 1}); 19 | this.update(); 20 | 21 | _.defer(() => this.$body.classed({"nbp-linking": 1})); 22 | } 23 | 24 | destroy(){ 25 | this.$body.classed({"nbp-linking": 0}); 26 | _.delay(() => this.$ui.remove(), 300); 27 | } 28 | 29 | update(){ 30 | let heightOffset = this.$header.property("clientHeight") - 31 | this.$site.property("scrollTop"), 32 | parts = _.chain(this.manager.cellPartGeometry()) 33 | .flatten() 34 | .filter(({bb}) => bb) 35 | .value(); 36 | 37 | let $part = this.$ui.selectAll(".nbp-part-overlay") 38 | .data(parts); 39 | 40 | $part.enter() 41 | .append("button").classed({"btn nbp-part-overlay": 1}) 42 | .on("click", ({part, cell}) => this.done({part, cell: cell.value})) 43 | .text(({part}) => part); 44 | 45 | $part.classed({ 46 | "nbp-part-overlay-source": ({part}) => part === PART.source, 47 | "nbp-part-overlay-outputs": ({part}) => part === PART.outputs, 48 | "nbp-part-overlay-widgets": ({part}) => part === PART.widgets, 49 | "nbp-part-overlay-whole": ({part}) => part === PART.whole 50 | }); 51 | 52 | $part.style({ 53 | top: ({bb}) => `${bb.top - heightOffset}px`, 54 | height: ({bb}) => `${bb.height - 1}px` 55 | }); 56 | 57 | $part.exit().remove(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/es6/cells/standalone.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | import {BaseCellManager} from "./base"; 3 | 4 | 5 | /** A Cell Manager for static HTML 6 | * @extends {BaseCellManager} */ 7 | export class StandaloneCellManager extends BaseCellManager { 8 | /** return an id-indexed set of {@link Cell}-like objects: each has the 9 | * a fake jQuery `element` member), as embedded in HTML 10 | * @return {Object} */ 11 | getCells(){ 12 | let cells = {}; 13 | 14 | d3.selectAll(".cell").each(function(){ 15 | let el = d3.select(this), 16 | id = el.attr("data-nbp-id"); 17 | 18 | if(id){ 19 | cells[id] = { 20 | element: [el.node()] 21 | } 22 | } 23 | }); 24 | return cells; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/es6/d3.bbox.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Lucas Beyer 2 | // Licensed under the MIT License (MIT) 3 | // Version 1.0 4 | // https://github.com/lucasb-eyer/d3-boundingbox 5 | 6 | // ES6 "translation" by (c) 2015 Nick Bollweg 7 | /* eslint-disable */ 8 | 9 | import {d3} from "nbpresent-deps"; 10 | 11 | export function bbox() { 12 | // All those are initialized to default further down using the setters. 13 | var xextent = null 14 | var yextent = null 15 | var handlesize = null 16 | var dirs = null 17 | var curs = null 18 | var cbs = { 19 | dragstart: null, 20 | dragmove: null, 21 | dragend: null, 22 | resizestart: null, 23 | resizemove: null, 24 | resizeend: null 25 | } 26 | 27 | function my(selection) { 28 | var drag = d3.behavior.drag() 29 | .origin(function(d, i) { return {x: this.getAttribute("x"), y: this.getAttribute("y")}; }) 30 | .on("drag.lbbbox", dragmove) 31 | .on("dragstart.lbbbox", dragstart) 32 | .on("dragend.lbbbox", dragend) 33 | selection.call(drag) 34 | selection.on("mousemove.lbbbox", move) 35 | selection.on("mouseleave.lbbbox", leave) 36 | 37 | return selection 38 | } 39 | 40 | function clamp(x, extent) { return Math.max(extent[0], Math.min(x, extent[1])); } 41 | function inside(x, extent) { return extent[0] < x && x < extent[1]; } 42 | 43 | // Will return w, nw, n, ne, e, se, s, sw for the eight borders, 44 | // M for inside, or "" when current location not in `dirs`. 45 | function whichborder(xy, elem) { 46 | var border = "" 47 | var x = +elem.getAttribute("x") 48 | var y = +elem.getAttribute("y") 49 | var w = +elem.getAttribute("width") 50 | var h = +elem.getAttribute("height") 51 | 52 | if(xy[1] < y + handlesize.n) border += 'n' 53 | else if(xy[1] > y + h - handlesize.s) border += 's' 54 | 55 | if(xy[0] < x + handlesize.w) border += 'w' 56 | else if(xy[0] > x + w - handlesize.e) border += 'e' 57 | 58 | 59 | if(border == "" && (dirs.indexOf("x") > -1 || dirs.indexOf("y") > -1)) 60 | border = "M" 61 | else if(dirs.indexOf(border) == -1) 62 | border = "" 63 | 64 | return border 65 | } 66 | 67 | function move(d, i) { 68 | // Don't do anything if we're currently dragging. 69 | // Otherwise, the cursor might jump horribly! 70 | // Also don't do anything if no cursors. 71 | if(this.__resize_action__ !== undefined || !curs) 72 | return 73 | 74 | var b = whichborder(d3.mouse(this), this) 75 | 76 | var x = dirs.indexOf("x") 77 | var y = dirs.indexOf("y") 78 | // Bwahahahaha this works even when one is at index 0. 79 | if(b == "M" && 1/(x*y) < 0) 80 | document.body.style.cursor = x >= 0 ? curs.x : curs.y 81 | else 82 | document.body.style.cursor = curs[b] || null 83 | } 84 | 85 | function leave(d, i) { 86 | // Only unset cursor if we're not dragging, 87 | // otherwise we get horrible cursor-flipping action. 88 | // Also only unset it if we actually did set it! 89 | if(this.__resize_action__ === undefined && curs) 90 | document.body.style.cursor = null 91 | } 92 | 93 | function dragstart(d, i) { 94 | this.__resize_action__ = whichborder(d3.mouse(this), this) 95 | this.__ow__ = +this.getAttribute("width") 96 | this.__oh__ = +this.getAttribute("height") 97 | 98 | if(this.__resize_action__ == "M") { 99 | if(cbs.dragstart) cbs.dragstart.call(this, d, i) 100 | } else if(this.__resize_action__.length) { 101 | if(cbs.resizestart) cbs.resizestart.call(this, d, i) 102 | } 103 | } 104 | 105 | function dragend(d, i) { 106 | if(this.__resize_action__ == "M") { 107 | if(cbs.dragend) cbs.dragend.call(this, d, i) 108 | } else if(this.__resize_action__.length) { 109 | if(cbs.resizeend) cbs.resizeend.call(this, d, i) 110 | } 111 | 112 | delete this.__resize_action__ 113 | delete this.__ow__ 114 | delete this.__oh__ 115 | 116 | // Still need to unset here, in case the user stop dragging 117 | // while the mouse isn't on the element anymore (e.g. off-limits). 118 | if(curs) 119 | document.body.style.cursor = null 120 | } 121 | 122 | function dragmove(d, i) { 123 | if(this.__resize_action__ == "M") { 124 | if(cbs.dragmove) 125 | if(false === cbs.dragmove.call(this, d, i)) 126 | return 127 | } else if(this.__resize_action__.length) { 128 | if(cbs.resizemove) 129 | if(false === cbs.resizemove.call(this, d, i)) 130 | return 131 | } 132 | 133 | // Potentially dynamically determine the allowed space. 134 | var xext = typeof xextent === "function" ? xextent.call(this, d, i) : xextent 135 | var yext = typeof yextent === "function" ? yextent.call(this, d, i) : yextent 136 | 137 | // Handle moving around first, more easily. 138 | if(this.__resize_action__ == "M") { 139 | if(dirs.indexOf("x") > -1 && d3.event.dx != 0) 140 | // This is so that even moving the mouse super-fast, this still "sticks" to the extent. 141 | this.setAttribute("x", clamp(clamp(d3.event.x, xext) + this.__ow__, xext) - this.__ow__) 142 | if(dirs.indexOf("y") > -1 && d3.event.dy != 0) 143 | this.setAttribute("y", clamp(clamp(d3.event.y, yext) + this.__oh__, yext) - this.__oh__) 144 | // Now check for all possible resizes. 145 | } else { 146 | var x = +this.getAttribute("x") 147 | var y = +this.getAttribute("y") 148 | 149 | // First, check for vertical resizes, 150 | if(/^n/.test(this.__resize_action__)) { 151 | var b = y + +this.getAttribute("height") 152 | var newy = clamp(clamp(d3.event.y, yext), [-Infinity, b-1]) 153 | this.setAttribute("y", newy) 154 | this.setAttribute("height", b - newy) 155 | } else if(/^s/.test(this.__resize_action__)) { 156 | var b = clamp(d3.event.y + this.__oh__, yext) 157 | this.setAttribute("height", clamp(b - y, [1, Infinity])) 158 | } 159 | 160 | // and then for horizontal ones. Note both may happen. 161 | if(/w$/.test(this.__resize_action__)) { 162 | var r = x + +this.getAttribute("width") 163 | var newx = clamp(clamp(d3.event.x, xext), [-Infinity, r-1]) 164 | this.setAttribute("x", newx) 165 | this.setAttribute("width", r - newx) 166 | } else if(/e$/.test(this.__resize_action__)) { 167 | var r = clamp(d3.event.x + this.__ow__, xext) 168 | this.setAttribute("width", clamp(r - x, [1, Infinity])) 169 | } 170 | } 171 | } 172 | 173 | my.xextent = function(_) { 174 | if(!arguments.length) return xextent 175 | xextent = _ !== false ? _ : [-Infinity, +Infinity] 176 | return my 177 | } 178 | my.xextent(false) 179 | 180 | my.yextent = function(_) { 181 | if(!arguments.length) return yextent 182 | yextent = _ !== false ? _ : [-Infinity, +Infinity] 183 | return my 184 | } 185 | my.yextent(false) 186 | 187 | my.handlesize = function(_) { 188 | if(!arguments.length) return handlesize 189 | handlesize = !+_ ? _ : {'w': _,'n': _,'e': _,'s': _} // coolface 190 | return my 191 | } 192 | my.handlesize(3) 193 | 194 | my.cursors = function(_) { 195 | if(!arguments.length) return curs 196 | curs = _ !== true ? _ : { 197 | M: "move", 198 | x: "col-resize", 199 | y: "row-resize", 200 | n: "n-resize", 201 | e: "e-resize", 202 | s: "s-resize", 203 | w: "w-resize", 204 | nw: "nw-resize", 205 | ne: "ne-resize", 206 | se: "se-resize", 207 | sw: "sw-resize" 208 | } 209 | return my 210 | } 211 | my.cursors(true) 212 | 213 | my.directions = function(_) { 214 | if(!arguments.length) return dirs 215 | dirs = _ !== true ? _ : ["n", "e", "s", "w", "nw", "ne", "se", "sw", "x", "y"] 216 | return my 217 | } 218 | my.directions(true) 219 | 220 | my.on = function(name, cb) { 221 | if(cb === undefined) return cbs[name] 222 | cbs[name] = cb 223 | return my 224 | } 225 | 226 | my.infect = function(selection) { 227 | selection.call(my) 228 | return my 229 | } 230 | 231 | my.disinfect = function(selection) { 232 | selection.on(".drag", null) 233 | selection.on(".lbbbox", null) 234 | return my 235 | } 236 | 237 | return my 238 | } 239 | -------------------------------------------------------------------------------- /src/es6/editor.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {PART} from "./parts"; 4 | 5 | import {RegionTree} from "./regiontree"; 6 | import {NotebookCellManager} from "./cells/notebook"; 7 | import {NotebookActions} from "./actions/notebook"; 8 | 9 | import {bbox} from "./d3.bbox"; 10 | 11 | const DIRS = ["left", "right", "up", "down"], 12 | DIR_ATTR = { 13 | left: "x", 14 | right: "x", 15 | up: "y", 16 | down: "y" 17 | }, 18 | DIR_DELTA = { 19 | left: -1, 20 | right: 1, 21 | up: -1, 22 | down: 1 23 | }; 24 | 25 | /** The visual editor overlay for drag-and-drop positioning of slide regions. 26 | */ 27 | export class Editor { 28 | /** Make a new Editor. 29 | * @param {baobab.Cursor} slide - the slide to edit 30 | * @param {object} selectedRegion - the slide id and region id 31 | * @listens {/slides/{slide}} */ 32 | constructor(slide, selectedRegion) { 33 | /** whether this Editor has been killed. 34 | * @type {bool} */ 35 | this.destroyed = false; 36 | 37 | /** cursor pointed at a specific slide. 38 | * @type {baobab.Cursor} */ 39 | this.slide = slide; 40 | 41 | /** the currently selected region 42 | * @type {object} 43 | * @param {baobab.Cursor} the region/slide to edit */ 44 | this.selectedRegion = selectedRegion; 45 | 46 | /** a sub-cursor for just region changes 47 | * @type {baobab.Cursor} */ 48 | this.regions = this.slide.select("regions"); 49 | 50 | /** handles bookkeeping with the browser 51 | * @type {NotebookCellManager} */ 52 | this.cellManager = new NotebookCellManager(); 53 | 54 | // TODO: make these discrete to base unit 55 | /** the relative size to screen width transfer function 56 | * @type {d3.scale} */ 57 | this.x = d3.scale.linear(); 58 | 59 | /** the relative size to screen height transfer function 60 | * @type {d3.scale} */ 61 | this.y = d3.scale.linear(); 62 | 63 | // ready to init 64 | this.initUI(); 65 | 66 | // TODO: move this? 67 | /** sub-ui for editing regions 68 | * @type {RegionTree} */ 69 | this.sidebar = new RegionTree(this.slide, this.selectedRegion); 70 | 71 | // ready to behave 72 | this.initBehavior() 73 | .initActions(); 74 | 75 | // fake data event 76 | this.update(); 77 | 78 | this.watcher = this.slide.tree.watch({ 79 | slide: this.slide, 80 | selectedRegion: this.selectedRegion, 81 | regions: this.regions 82 | }); 83 | 84 | // subscribe to event when cursor changes 85 | this.watcher.on("update", () => this.destroyed || this.update()); 86 | } 87 | 88 | /** Destroy the editor and its children utterly. */ 89 | destroy() { 90 | this.deinitActions(); 91 | this.sidebar.destroy(); 92 | this.$ui.remove(); 93 | this.watcher.release(); 94 | 95 | this.destroyed = true; 96 | } 97 | 98 | 99 | /** Create d3 behaviors. 100 | * @return {Editor} */ 101 | initBehavior(){ 102 | /** @type {d3.bbox} */ 103 | this.bbox = bbox(); 104 | 105 | return this; 106 | } 107 | 108 | /** Create all UI chrome. 109 | * @return {Editor} */ 110 | initUI(){ 111 | this.$body = d3.select("body"); 112 | 113 | /** @type {d3.selection} */ 114 | this.$ui = d3.select("body") 115 | .append("div") 116 | .classed({"nbp-editor": 1}); 117 | 118 | /** @type {d3.selection} */ 119 | this.$bg = this.$ui.append("div") 120 | .classed({slide_bg: 1}); 121 | 122 | /** @type {d3.selection} */ 123 | this.$svg = this.$bg.append("svg"); 124 | 125 | /** @type {d3.selection} */ 126 | this.$defs = this.$svg.append("defs"); 127 | 128 | return this; 129 | } 130 | 131 | 132 | /** 133 | * A stand-in for themed padding 134 | * @return {Number} 135 | */ 136 | padding(){ 137 | return 20; 138 | } 139 | 140 | 141 | /** 142 | * A stand-in for themed aspect ratio 143 | * @return {Number} 144 | */ 145 | aspectRatio(){ 146 | // TODO: put in data 147 | return 16 / 9; 148 | } 149 | 150 | /** 151 | * The "end" event of the d3.bbox control. 152 | * @param {Element} el - the DOM element (usually `this` in d3 callbacks) 153 | * @param {Object} d - the datum for the element 154 | */ 155 | bbEnd(el){ 156 | let $el = d3.select(el), 157 | {region} = this.selectedRegion.get() || {}, 158 | scales = { 159 | x: this.x, 160 | y: this.y, 161 | width: this.x, 162 | height: this.y 163 | }; 164 | 165 | if(!region){ 166 | return; 167 | } 168 | 169 | this.slide.merge(["regions", region, "attrs"], 170 | d3.entries(scales) 171 | .reduce((memo, {key, value}) => { 172 | memo[key] = value.invert($el.attr(key)); 173 | return memo; 174 | }, {})); 175 | } 176 | 177 | /** 178 | * A d3 filter factory that determines whether a region contains a part of a 179 | * part type. 180 | * @param {PART_SELECT} part - the type of part to seek 181 | * @return {bool} 182 | */ 183 | hasContent(part){ 184 | return (d) => (d.value.content || {}).part === part; 185 | } 186 | 187 | /** 188 | * The main tree callback. 189 | */ 190 | update(){ 191 | // TODO: this really needs to be refactored. 192 | let that = this, 193 | uibb = this.$ui.node().getBoundingClientRect(), 194 | width = uibb.width - ((2 * this.padding())), 195 | height = width / this.aspectRatio(), 196 | regions = d3.entries(this.regions.get() || {}), 197 | {region} = this.selectedRegion.get() || {}, 198 | {x, y} = this; 199 | 200 | if(height > uibb.height + 2 * this.padding()){ 201 | height = uibb.height - (2 * this.padding()); 202 | width = height * this.aspectRatio(); 203 | } 204 | 205 | this.x.range([0, width]); 206 | this.y.range([0, height]); 207 | 208 | this.$bg.style({ 209 | left: `${(((uibb.width) - width) / 2)}px`, 210 | top: `${(uibb.height - height) / 2}px`, 211 | width: `${width}px`, 212 | height: `${height}px` 213 | }); 214 | 215 | this.$svg.attr({width, height}); 216 | 217 | regions.sort((a, b) => 218 | (region === a.key) ? 1 : 219 | (region === b.key) ? -1 : 220 | (a.value.attrs.z || 0) - (b.value.attrs.z || 0)); 221 | 222 | let $region = this.$svg.selectAll(".nbp-region") 223 | .data(regions, ({key}) => key) 224 | .order(); 225 | 226 | $region.enter() 227 | .append("g") 228 | .classed({"nbp-region": 1}) 229 | .call(($region) => { 230 | $region.append("rect") 231 | .classed({"nbp-region-bg": 1}) 232 | .each(function(){ 233 | that.bbox.infect(d3.select(this)) 234 | .on("dragend", function(d){ that.bbEnd(this, d) }) 235 | .on("resizeend", function(d){ that.bbEnd(this, d) }); 236 | }); 237 | }) 238 | .on("mousedown", (d) => { 239 | this.selectedRegion.set({slide: this.slide.get("id"), region: d.key}); 240 | }); 241 | 242 | $region 243 | .classed({ 244 | active: ({key}) => key === region, 245 | "nbp-content-source": this.hasContent(PART.source), 246 | "nbp-content-outputs": this.hasContent(PART.outputs), 247 | "nbp-content-widgets": this.hasContent(PART.widgets), 248 | "nbp-content-whole": this.hasContent(PART.whole), 249 | "nbp-content-null": this.hasContent(null) 250 | }) 251 | .select(".nbp-region-bg") 252 | .transition() 253 | .attr({ 254 | width: (d) => x(d.value.attrs.width), 255 | height: (d) => y(d.value.attrs.height), 256 | x: (d) => x(d.value.attrs.x), 257 | y: (d) => y(d.value.attrs.y) 258 | }); 259 | 260 | $region.filter(({value}) => !value.content) 261 | .select(".nbp-region-bg") 262 | .style({fill: null}); 263 | 264 | $region.exit() 265 | .transition() 266 | .style({opacity: 0}) 267 | .remove(); 268 | } 269 | 270 | cycleRegion(delta=1){ 271 | let {region} = this.selectedRegion.get() || {}, 272 | regions = d3.entries(this.regions.get() || {}), 273 | idx = regions.map(({key, value}, i) => key === region ? i : -1) 274 | .filter((i) => i !== -1)[0]; 275 | 276 | idx = idx + delta; 277 | if(idx === -1){ 278 | idx = regions.length - 1; 279 | } 280 | 281 | this.selectedRegion.set(["region"], regions[idx % regions.length].key); 282 | } 283 | 284 | moveRegion(env, direction, amount=0.1){ 285 | let {region} = this.selectedRegion.get() || {}, 286 | attr = DIR_ATTR[direction], 287 | delta = DIR_DELTA[direction]; 288 | 289 | if(region){ 290 | this.regions.set( 291 | [region, "attrs", attr], 292 | this.regions.get([region, "attrs", attr]) + (delta * amount) 293 | ) 294 | } 295 | return this; 296 | } 297 | 298 | initActions(){ 299 | let _actions = [{ 300 | keys: ["shift-tab"], 301 | name: "prev-region", 302 | value: { 303 | icon: "caret-square-o-left", 304 | help: "select previous region", 305 | handler: () => this.cycleRegion() 306 | } 307 | }, 308 | { 309 | name: "next-region", 310 | keys: ["tab"], 311 | value: { 312 | icon: "caret-square-o-right", 313 | help: "select next region", 314 | handler: () => this.cycleRegion(-1) 315 | } 316 | } 317 | ]; 318 | 319 | DIRS.map((dir) => { 320 | _actions.push({ 321 | name: `nudge-region-${dir}`, 322 | keys: [dir], 323 | value: { 324 | icon: `arrow-circle-o-${dir}`, 325 | help: `nudge current region ${dir}`, 326 | handler: (env) => this.moveRegion(env, dir, 0.01) 327 | } 328 | }); 329 | 330 | _actions.push({ 331 | name: `move-region-${dir}`, 332 | keys: [`shift-${dir}`], 333 | value: { 334 | icon: `arrow-circle-o-${dir}`, 335 | help: `move current region ${dir}`, 336 | handler: (env) => this.moveRegion(env, dir, 0.1) 337 | } 338 | }); 339 | }); 340 | 341 | this.actions = new NotebookActions(_actions); 342 | this.actions.push(); 343 | 344 | return this; 345 | } 346 | 347 | deinitActions(){ 348 | this.actions && this.actions.pop(); 349 | return this; 350 | } 351 | 352 | } 353 | -------------------------------------------------------------------------------- /src/es6/help/helper.es6: -------------------------------------------------------------------------------- 1 | import {d3, _} from "nbpresent-deps"; 2 | 3 | import {PKG} from "../package"; 4 | 5 | import {IntroTour} from "./tours/intro"; 6 | import {SlideEditorTour} from "./tours/slide-editor"; 7 | import {SorterTour} from "./tours/sorter"; 8 | 9 | 10 | const TOURS = { 11 | Intro: IntroTour, 12 | Slides: SorterTour, 13 | Editor: SlideEditorTour 14 | }; 15 | 16 | 17 | const COMMUNITY = { 18 | Discuss: ["envelope", PKG.discussion], 19 | Chat: ["comment", PKG.chat], 20 | Contribute: ["code-fork", PKG.homepage], 21 | Bugs: ["bug", PKG.bugs.url] 22 | }; 23 | 24 | 25 | export class Helper { 26 | constructor(tree, {mode}){ 27 | this.tree = tree; 28 | this.mode = mode; 29 | this.initUI(); 30 | _.delay(() => this.$body.classed({"nbp-helping": 1}), 10); 31 | } 32 | 33 | initUI(){ 34 | this.$body = d3.select("body"); 35 | this.$ui = this.$body.append("div") 36 | .classed({"nbp-helper": 1}); 37 | 38 | this.$h2 = this.$ui.append("h2") 39 | .text("nbpresent ") 40 | 41 | this.$h2.append("hr"); 42 | 43 | this.$h2 44 | .append("a") 45 | .classed({"nbp-version": 1}) 46 | .attr({ 47 | href: `${PKG.homepage}#${PKG.version}` 48 | }) 49 | .text(PKG.version); 50 | 51 | this.$h1 = this.$ui.append("h1") 52 | .append("i").classed({"fa fa-gift fa-3x": 1}); 53 | 54 | this.$ui.append("footer") 55 | .classed({"nbp-legal": 1}) 56 | .call((legal) => { 57 | legal.append("span").text("©2016 "); 58 | 59 | legal.append("a") 60 | .attr({href: "https://continuum.io"}) 61 | .text("Continuum Analytics"); 62 | 63 | legal.append("p") 64 | .call((license) => { 65 | license.append("span").text("nbpresent is "); 66 | license.append("a") 67 | .attr({href: `https://spdx.org/licenses/${PKG.license}`}) 68 | .text("free software"); 69 | }); 70 | }); 71 | 72 | this.initTours() 73 | .initCommunity() 74 | .update(); 75 | } 76 | 77 | initCommunity(){ 78 | this.$ui.append("h2").text("Community"); 79 | 80 | this.$ui.selectAll(".nbp-community") 81 | .data(d3.entries(COMMUNITY)) 82 | .enter() 83 | .append("a") 84 | .classed({"btn btn-default nbp-community": 1}) 85 | .attr({ 86 | href: ({value}) => value[1], 87 | target: "_blank" 88 | }) 89 | .call((btn) => { 90 | btn.append("i") 91 | .attr({"class": ({value}) => `fa fa-${value[0]} fa-2x`}); 92 | btn.append("label").text(({key}) => key); 93 | }); 94 | 95 | return this; 96 | } 97 | 98 | initTours(){ 99 | this.$tours = this.$ui.append("div") 100 | .classed({"nbp-tours": 1}); 101 | 102 | this.$tours.append("h2") 103 | .text("Guided Tours"); 104 | 105 | let tour = this.$tours.selectAll(".nbp-tour-launcher") 106 | .data(d3.entries(TOURS)); 107 | 108 | tour.enter() 109 | .append("a") 110 | .classed({"btn btn-default nbp-tour-launcher": 1}) 111 | .call((btn) => ["i", "label"].map((el) => btn.append(el))) 112 | .on("click", ({value}) => this.startTour(value)); 113 | 114 | tour.select("i") 115 | .attr("class", ({value}) => `fa fa-2x fa-${value.icon()}`); 116 | tour.select("label").text(({key}) => ` ${key}`); 117 | 118 | return this; 119 | } 120 | 121 | startTour(Tour){ 122 | let tour = new Tour(this.mode); 123 | tour.init(); 124 | tour.start(true); 125 | 126 | this.mode.mode.set(null); 127 | } 128 | 129 | update(){ 130 | return this; 131 | } 132 | 133 | destroy(){ 134 | this.$body.classed({"nbp-helping": 0}); 135 | _.delay(() => this.$ui.remove(), 500); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/es6/help/tours/base.es6: -------------------------------------------------------------------------------- 1 | import Tour from "bootstraptour"; 2 | 3 | import {d3} from "nbpresent-deps"; 4 | 5 | 6 | export class TourBase { 7 | constructor(mode) { 8 | this.mode = mode; 9 | } 10 | 11 | init(){ 12 | let opts = this.options(); 13 | opts.steps = this.steps(); 14 | this.tour = new Tour(opts); 15 | this.tour.init(); 16 | return this; 17 | } 18 | 19 | static icon(){ 20 | return "question-circle"; 21 | } 22 | 23 | start(){ 24 | this.tour.start(); 25 | return this; 26 | } 27 | 28 | restart(){ 29 | this.tour.restart(); 30 | return this; 31 | } 32 | 33 | end(){ 34 | this.tour.end(); 35 | return this; 36 | } 37 | 38 | options(){ 39 | return { 40 | name: "nbpresent", 41 | duration: 5000, 42 | storage: false, 43 | debug: true, 44 | orphan: true 45 | }; 46 | } 47 | 48 | fakeHover(selector, value){ 49 | this.mode.presenter.speaker.hint(); 50 | d3.selectAll(selector).classed({"nbp-fake-hover": value}); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/es6/help/tours/intro.es6: -------------------------------------------------------------------------------- 1 | //import {$} from "nbpresent-deps"; 2 | 3 | import {ICON} from "../../icons"; 4 | 5 | import {TourBase} from "./base"; 6 | 7 | 8 | export class IntroTour extends TourBase { 9 | static icon(){ 10 | return ICON.intro; 11 | } 12 | 13 | steps(){ 14 | return [{ 15 | element: "#nbp-app-btn", 16 | title: "Thanks for using nbpresent!", 17 | placement: "bottom", 18 | content: "You just started Authoring a presentation with nbpresent" 19 | }, { 20 | element: "#nbp-app", 21 | title: "App Bar", 22 | placement: "left", 23 | content: "When Authoring, you control the content and style of your presentation. It also activates several special editing keyboard shortcuts" 24 | }, { 25 | element: "#nbp-app-btn", 26 | title: "Stop Authoring", 27 | placement: "bottom", 28 | content: "Clicking this again stops Authoring, and removes all keyboard shortcuts" 29 | }, { 30 | element: "#nbp-present-btn", 31 | placement: "left", 32 | title: "Simply Presenting", 33 | content: "If you just want to run your presentation, without any Authoring tools, you can start Presenting directly." 34 | }, { 35 | element: `.nbp-app-bar .fa-${ICON.presenter}`, 36 | placement: "left", 37 | title: "Presenting/Authoring", 38 | content: "Once we've made some slides, we'll start Presenting, where we can use most Notebook functions with the Theme we have defined, as well as customize slides on the fly." 39 | }, { 40 | element: `.nbp-app-bar .fa-${ICON.slides}`, 41 | placement: "left", 42 | title: "Slides", 43 | content: "Slides, made of Regions linked to Cell Parts are the bread and butter of any presentation, and can be imported, created, linked, reordered, and edited here." 44 | }, { 45 | element: `.nbp-app-bar .fa-${ICON.themer}`, 46 | placement: "left", 47 | title: "Theming", 48 | content: "Theming lets you select colors, typography, and backgrounds to make distinctive presentations." 49 | }, { 50 | element: "#save-notbook", 51 | placement: "bottom", 52 | title: "Saving", 53 | content: "Whenever you save your Notebook, all your presentation data will be stored right in the Notebook .ipynb file" 54 | }, { 55 | element: "#file_menu", 56 | placement: "bottom", 57 | title: "Downloading", 58 | content: "After you've made a presentation, you can download it as an HTML page by selecting Download As: Presentation (.html)" 59 | }, { 60 | element: `.nbp-app-bar .fa-${ICON.help}`, 61 | placement: "left", 62 | title: "Help", 63 | content: "Activate Help at any time to try other tours, connect with the nbpresent developers and community, and other information. Thanks again for using nbpresent." 64 | } 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/es6/help/tours/slide-editor.es6: -------------------------------------------------------------------------------- 1 | import {_} from "nbpresent-deps"; 2 | 3 | import {ICON} from "../../icons"; 4 | 5 | import {SORTER} from "../../mode/notebook"; 6 | 7 | import {TourBase} from "./base"; 8 | 9 | 10 | export class SlideEditorTour extends TourBase { 11 | static icon(){ 12 | return ICON.editor; 13 | } 14 | 15 | steps(){ 16 | return [{ 17 | element: `.nbp-app-bar .fa-${ICON.slides}`, 18 | title: "So You Made Some Slides", 19 | content: "Once you've made a few slides, you'll likely need to customize them", 20 | placement: "left", 21 | onShown: () => { 22 | this.mode.mode.set(SORTER); 23 | _.delay(() => this.mode.sorter.appendSlide(), 500); 24 | } 25 | }, { 26 | element: `.nbp-deck-toolbar .fa-${ICON.editor}`, 27 | title: "Editing Slides", 28 | content: "Once you have selected a slide, you can activate the Slide Editor", 29 | placement: "left", 30 | onHide: () => _.defer(() => { 31 | this.mode.sorter.editSlide(); 32 | }) 33 | }, { 34 | element: ".nbp-editor .slide_bg", 35 | placement: "top", 36 | title: "Region Editor", 37 | content: "This is the Region editor. You can click and drag Regions around and resize them." 38 | }, { 39 | element: ".nbp-regiontree", 40 | placement: "right", 41 | title: "Region Tree", 42 | content: "This is the Region tree. It lets you reorder Regions and see the details of how your Regions will show their linked Parts." 43 | }, { 44 | element: `.nbp-regiontree > .nbp-toolbar .fa-${ICON.addRegion}`, 45 | placement: "right", 46 | title: "Add Region", 47 | content: "You can add new regions", 48 | onShow: () => _.defer(() => this.mode.sorter.editor.sidebar.addRegion()) 49 | }, { 50 | element: ".region_attr:first-of-type .attr_name", 51 | placement: "right", 52 | title: "Attribute Editor", 53 | content: "All of the properties of a region can be edited here" 54 | }, { 55 | element: `.nbp-regiontree .btn-toolbar .fa-${ICON.treemap}`, 56 | placement: "right", 57 | title: "Data Layouts", 58 | content: "In addition to manually moving regions around, you can use other Layouts, like this Treemap, which will fill the slide", 59 | onHidden: () => { 60 | this.mode.sorter.editor.sidebar.layout("treemap"); 61 | } 62 | }, { 63 | element: `.nbp-regiontree > .nbp-toolbar .fa-${ICON.addRegion}`, 64 | placement: "right", 65 | title: "More Regions", 66 | content: "More regions will be added with a weight of 1", 67 | onShow: () => _.defer(() => this.mode.sorter.editor.sidebar.addRegion()) 68 | }, { 69 | element: `.region_attr .fa-${ICON.treemap}`, 70 | placement: "right", 71 | title: "Tree Weight", 72 | content: "This new value lets you make a Region bigger or smaller based on relative Weight" 73 | }, { 74 | element: `.nbp-regiontree .btn-toolbar .fa-${ICON.grid}`, 75 | placement: "right", 76 | title: "12 Grid", 77 | content: "The Grid is a comprimise between Free layout and Treemap layout, and rounds all the values to a factor of 12", 78 | onShow: () => this.mode.sorter.editor.sidebar.layout("grid") 79 | }, { 80 | title: "FIN", 81 | content: "Thank you for using nbpresent!" 82 | }] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/es6/help/tours/sorter.es6: -------------------------------------------------------------------------------- 1 | import {d3, $} from "nbpresent-deps"; 2 | 3 | import Jupyter from "base/js/namespace"; 4 | 5 | import {PART} from "../../parts"; 6 | import {ICON} from "../../icons"; 7 | import {SORTER} from "../../mode/notebook"; 8 | 9 | import {TourBase} from "./base"; 10 | 11 | 12 | export class SorterTour extends TourBase { 13 | static icon(){ 14 | return ICON.slides; 15 | } 16 | 17 | steps(){ 18 | return [{ 19 | element: `.nbp-app-bar .fa-${ICON.slides}`, 20 | title: "Slides", 21 | content: "Slides make up a presentation", 22 | placement: "left", 23 | onHide: () => this.mode.mode.set(SORTER) 24 | }, { 25 | element: `.nbp-app-bar .fa-${ICON.slides}`, 26 | title: "Slides", 27 | content: "Clicking Slides toggles the sorter view", 28 | placement: "left" 29 | }, { 30 | element: `.nbp-deck-toolbar .fa-${ICON.addSlide}`, 31 | title: "Deck Toolbar", 32 | content: "Let's create a new slide", 33 | placement: "left", 34 | onHide: () => $(`.nbp-deck-toolbar .fa-${ICON.addSlide}`).click() 35 | }, { 36 | element: ".from_template", 37 | title: "Template Library", 38 | position: "top", 39 | content: "You can pick from some existing templates..." 40 | }, { 41 | element: ".from_slide", 42 | title: "Reuse Slide as Template", 43 | position: "top", 44 | content: "Or copy an existing slide" 45 | }, { 46 | title: "Cells", 47 | position: "top", 48 | content: "Here's are some new cells the Tour will play with", 49 | onShow: () => { 50 | let code = Jupyter.notebook.insert_cell_at_index("code", 0), 51 | markdown = Jupyter.notebook.insert_cell_at_index("markdown", 0); 52 | 53 | markdown.code_mirror.setValue('# Hello, _nbpresent_!'); 54 | code.code_mirror.setValue('import nbpresent\nnbpresent.__version__'); 55 | } 56 | }, { 57 | element: ".nbp-template-library .slide:nth-of-type(1)", 58 | title: "Simple Template", 59 | position: "top", 60 | content: "Let's use this one", 61 | onNext: () => this.mode.sorter.templatePicked( 62 | d3.select(".nbp-template-library .slide:nth-of-type(1)").datum() 63 | ) 64 | }, { 65 | element: ".nbp-slides-wrap .slide:last-child", 66 | placement: "top", 67 | title: "Look at the slide, just look at it", 68 | content: "Here's our new slide" 69 | }, { 70 | element: ".nbp-slides-wrap .slide:last-child .nbp-region", 71 | placement: "top", 72 | title: "Region", 73 | onShow: () => { 74 | let el = d3.select(".nbp-slides-wrap .slide:last-child .nbp-region"); 75 | el.on("click")(el.datum()); 76 | }, 77 | content: "It has one Region" 78 | }, { 79 | element: `.nbp-region-toolbar .fa-${ICON.link}`, 80 | placement: "top", 81 | title: "Linking a Region to a Cell Part", 82 | content: "Each Region can be linked to a single Cell Part", 83 | onHide: () => this.mode.sorter.linkContentOverlay() 84 | }, { 85 | element: `.nbp-region-toolbar .fa-${ICON.link}`, 86 | placement: "top", 87 | title: "Link Overlay", 88 | content: "The Link Overlay shows all of the parts available" 89 | }, { 90 | element: `.nbp-link-overlay .nbp-part-overlay-source:first-of-type`, 91 | placement: "right", 92 | title: "Cell Part: Source", 93 | content: "Source, such as code and Markdown text" 94 | }, { 95 | element: `.nbp-link-overlay .nbp-part-overlay-outputs:first-of-type`, 96 | placement: "right", 97 | title: "Cell Part: Outputs", 98 | content: "Outputs, such as rich figures and script results" 99 | }, { 100 | element: `.nbp-link-overlay .nbp-part-overlay-widgets:first-of-type`, 101 | placement: "bottom", 102 | title: "Cell Part: Widgets", 103 | content: "If you use them, all the Widgets of a cell can be shown together" 104 | }, { 105 | element: `.nbp-link-overlay .nbp-part-overlay-whole:first-of-type`, 106 | placement: "left", 107 | title: "Cell Part: Whole", 108 | content: "Finally, a Whole Cell (including its Source, Widgets, and Outputs) can be linked to a single region" 109 | }, { 110 | element: ".cell.selected", 111 | placement: "left", 112 | title: "Linking an Input Part", 113 | content: "Let's use this cell input", 114 | onHide: () => this.mode.sorter.linkContent(PART.source) 115 | }, { 116 | element: ".nbp-slides-wrap .slide:last-child .nbp-region", 117 | placement: "top", 118 | title: "Part Thumbnail", 119 | content: "A part thumbnail might look a little funny, as it can only be reliably updated when a linked Cell Part is on-screen when you mouse over it, but you should usually be able to get an idea of what you're seeing." 120 | }, { 121 | element: `.nbp-region-toolbar .fa-${ICON.unlink}`, 122 | placement: "top", 123 | title: "Cell Part: Unlinking", 124 | content: "Unlinking removes the connection between a region and a cell part, without deleting either one." 125 | }, { 126 | element: `.nbp-region-toolbar .fa-${ICON.trash}`, 127 | placement: "top", 128 | title: "Region: Trashing", 129 | content: "Trashing a Region permanently deletes it, without affecting any linked Cell Part" 130 | }, { 131 | title: "Achievement Unlocked: Presentation", 132 | content: "We're ready to look at the presentation!" 133 | }, { 134 | element: "#nbp-present-btn", 135 | title: "Great, let's have a look", 136 | placement: "bottom", 137 | content: "Clicking this button brings up the Presenter", 138 | onNext: () => { 139 | this.mode.present(); 140 | this.mode.mode.set(null); 141 | } 142 | }, { 143 | title: "Looks great!", 144 | content: "Your slide is still made up of your notebook" 145 | }, { 146 | element: ".nbp-present", 147 | placement: "top", 148 | title: "It's still a Notebook", 149 | content: "This is still an editable input area" 150 | }, { 151 | element: ".nbp-present", 152 | placement: "bottom", 153 | title: "Part Execution", 154 | content: "Inputs can even be executed with keyboard shortcuts like ctrl+enter" 155 | }, { 156 | element: ".nbp-presenter-toolbar .fa-step-forward", 157 | title: "Go forward", 158 | content: "Click here to go to the next Slide", 159 | placement: "top", 160 | onShown: () => this.fakeHover(".nbp-presenter-toolbar", 1), 161 | onHidden: () => this.fakeHover(".nbp-presenter-toolbar", 0) 162 | }, { 163 | element: ".nbp-presenter-toolbar .fa-step-backward", 164 | title: "Go back", 165 | content: "Clicking here to go back to the previous slide", 166 | placement: "top", 167 | onShown: () => this.fakeHover(".nbp-presenter-toolbar", 1), 168 | onHidden: () => this.fakeHover(".nbp-presenter-toolbar", 0) 169 | }, { 170 | element: ".nbp-presenter-toolbar .fa-fast-backward", 171 | title: "Go back to the beginning", 172 | content: "Clicking here to go back to the first Slide", 173 | placement: "top", 174 | onShown: () => this.fakeHover(".nbp-presenter-toolbar", 1), 175 | onHidden: () => this.fakeHover(".nbp-presenter-toolbar", 0) 176 | }, { 177 | element: ".nbp-presenter-toolbar .fa-book", 178 | title: "My work is done here", 179 | content: "Click here to go back to the Notebook", 180 | placement: "top", 181 | onShown: () => this.fakeHover(".nbp-presenter-toolbar", 1), 182 | onHidden: () => [ 183 | this.fakeHover(".nbp-presenter-toolbar", 0), 184 | this.mode.present(false) 185 | ] 186 | }] 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/es6/icons.es6: -------------------------------------------------------------------------------- 1 | export const ICON = { 2 | showRules: "adjust", 3 | addRegion: "plus-square", 4 | addSlide: "plus-square-o", 5 | addTheme: "plus-circle", 6 | defaultThemeActive: "star", 7 | defaultTheme: "star-o", 8 | editor: "edit", 9 | editRegion: "edit", 10 | grid: "th", 11 | help: "question-circle", 12 | intro: "home", 13 | link: "link", 14 | manual: "arrows", 15 | nbpresent: "gift", 16 | presenter: "youtube-play", 17 | preview: "eye-slash", 18 | slides: "film", 19 | themer: "paint-brush", 20 | trash: "trash", 21 | treemap: "tree", 22 | unlink: "unlink" 23 | }; 24 | -------------------------------------------------------------------------------- /src/es6/layout/grid.es6: -------------------------------------------------------------------------------- 1 | import {d3, _} from "nbpresent-deps"; 2 | 3 | import {ManualLayout} from "./manual"; 4 | 5 | let KEY = "grid", 6 | PAD = "pad", 7 | TOLERANCE = 0.001; 8 | 9 | export class GridLayout extends ManualLayout { 10 | static clsKey(){ 11 | return KEY; 12 | } 13 | 14 | key(){ 15 | return KEY; 16 | } 17 | 18 | attrDefaults() { 19 | var attrs = {}; 20 | attrs[PAD] = 0.01; 21 | return attrs; 22 | } 23 | 24 | nice(memo, opts){ 25 | let [base] = opts, 26 | d = memo._d, 27 | rounded = Math.round(d[base] * 12), 28 | normalized = rounded / 12; 29 | 30 | if(Math.abs(normalized - d[base]) > TOLERANCE){ 31 | memo[base] = normalized; 32 | } 33 | return memo; 34 | } 35 | 36 | init(){ 37 | super.init(); 38 | 39 | let regions = d3.entries(this.slide.value.regions); 40 | 41 | regions.map((d) => { 42 | let path = ["slides", this.slide.key, "regions", d.key, "attrs"]; 43 | 44 | let attrs = _.reduce([ 45 | ["x", 0, 10], 46 | ["y", 0, 10], 47 | ["width", 1, 11], 48 | ["height", 1, 11] 49 | ], this.nice, {_d: d.value.attrs}, this); 50 | 51 | delete attrs._d; 52 | 53 | let needsMerge = !_.isEmpty(attrs) && !_.isEqual( 54 | attrs, 55 | _.pick( 56 | d.value.attrs, 57 | "x", "y", "width", "height", PAD)); 58 | 59 | if(needsMerge){ 60 | this.tree.merge(path, attrs); 61 | } 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/es6/layout/manual.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | let KEY = "manual"; 4 | 5 | export class ManualLayout { 6 | static clsKey(){ 7 | return KEY; 8 | } 9 | 10 | regions() { return d3.entries(this.slide.value.regions); } 11 | key(){ return KEY; } 12 | 13 | attrDefaults() { 14 | return { 15 | x: 0.1, 16 | y: 0.1, 17 | width: 0.8, 18 | height: 0.8 19 | }; 20 | } 21 | 22 | constructor(tree, slide, container){ 23 | this.tree = tree; 24 | this.slide = slide; 25 | this.container = container; 26 | } 27 | 28 | init(){ 29 | let {clientWidth, clientHeight} = this.container; 30 | 31 | this.x = d3.scale.linear() 32 | .range([0, clientWidth]); 33 | this.y = d3.scale.linear() 34 | .range([0, clientHeight]); 35 | 36 | // initialize missing values 37 | this.initAttrs(); 38 | } 39 | 40 | initAttrs(){ 41 | // initialize missing values 42 | let regions = this.regions(); 43 | 44 | d3.entries(this.attrDefaults()) 45 | .map(({key, value})=>{ 46 | regions 47 | .filter((d) => d.value.attrs[key] == null) 48 | .map((d) => { 49 | var attrs = {}; 50 | attrs[key] = value; 51 | this.tree.merge( 52 | ["slides", this.slide.key, "regions", d.key, "attrs"], 53 | attrs 54 | ); 55 | }); 56 | }); 57 | } 58 | 59 | update(region, part){ 60 | let {x, y, width, height, z, pad} = region.value.attrs; 61 | 62 | z = z || 0; 63 | pad = pad || 0; 64 | 65 | part.style({ 66 | height: `${parseInt(this.y(height - (2 * pad)))}px`, 67 | left: `${parseInt(this.x(x + pad))}px`, 68 | top: `${parseInt(this.y(y + pad))}px`, 69 | width: `${parseInt(this.x(width + (2 * pad)))}px` 70 | }); 71 | } 72 | 73 | clean(unpresent){ 74 | unpresent.style({ 75 | left: null, 76 | top: null, 77 | width: null, 78 | height: null 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/es6/layout/treemap.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {ManualLayout} from "./manual"; 4 | 5 | let KEY = "treemap", 6 | WEIGHT = "treemap:weight"; 7 | 8 | export class TreemapLayout extends ManualLayout { 9 | static clsKey(){ 10 | return KEY; 11 | } 12 | 13 | attrDefaults() { 14 | var attrs = {}; 15 | attrs[WEIGHT] = 1.0; 16 | return attrs; 17 | } 18 | 19 | key(){ 20 | return KEY; 21 | } 22 | 23 | init(){ 24 | super.init(); 25 | 26 | let that = this; 27 | 28 | this.treemap = d3.layout.treemap() 29 | .size([100, 100]) 30 | .sticky(true) 31 | .value((d) => { 32 | return (d._value || {}).attrs[WEIGHT] || 1; 33 | }); 34 | 35 | let regions = d3.entries(this.slide.value.regions) 36 | .map((d)=> { 37 | d._value = d.value; 38 | return d; 39 | }); 40 | 41 | this.treemap({ 42 | children: regions 43 | }); 44 | 45 | regions.map((d) => { 46 | let path = ["slides", that.slide.key, "regions", d.key, "attrs"]; 47 | let old = that.tree.get(path), 48 | newValues = { 49 | x: d.x / 100, 50 | y: d.y / 100, 51 | width: d.dx / 100, 52 | height: d.dy / 100 53 | }, 54 | toSet; 55 | 56 | for(var key in newValues){ 57 | if(old[key] !== newValues[key]){ 58 | (toSet ? toSet : toSet = {})[key] = newValues[key]; 59 | } 60 | } 61 | 62 | if(toSet){ 63 | that.tree.merge(path, toSet); 64 | } 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/es6/less.es6: -------------------------------------------------------------------------------- 1 | export const JPY_BRAND = "#f37626", 2 | UI_BG = "#222"; 3 | -------------------------------------------------------------------------------- /src/es6/logger.es6: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | // export let FOO = 1; 4 | 5 | export const ERROR = 0, 6 | WARN = 1, 7 | INFO = 2, 8 | LOG = 3, 9 | DEBUG = 4; 10 | 11 | let _logLevel = DEBUG; 12 | 13 | 14 | export let log = { 15 | error: function(...args){ console.error(...args); }, 16 | warn: function(...args){ (_logLevel >= WARN) && console.warn(...args); }, 17 | info: function(...args){ (_logLevel >= INFO) && console.info(...args); }, 18 | log: function(...args){ (_logLevel >= LOG) && console.log(...args); }, 19 | debug: function(...args){ (_logLevel >= LOG) && console.debug(...args)} 20 | } 21 | 22 | export function setLevel(level){ 23 | _logLevel = level; 24 | } 25 | -------------------------------------------------------------------------------- /src/es6/mini.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {NotebookCellManager} from "./cells/notebook"; 4 | import {PART} from "./parts"; 5 | 6 | // TODO: refactor this 7 | export const SLIDE_WIDTH = 160, 8 | SLIDE_HEIGHT = 90; 9 | 10 | class MiniSlide { 11 | width() { 12 | return SLIDE_WIDTH; 13 | } 14 | 15 | height(){ 16 | return SLIDE_HEIGHT; 17 | } 18 | 19 | constructor(selectedRegion) { 20 | this.selectedRegion = selectedRegion; 21 | this.cellManager = new NotebookCellManager(); 22 | 23 | this._regions = ({value}) => value.regions; 24 | 25 | this.update = this.update.bind(this); 26 | this.clicked = this.clicked.bind(this); 27 | } 28 | 29 | regions(_){ 30 | return arguments.length ? 31 | [this._regions = d3.functor(_), this][1] : 32 | this._regions; 33 | } 34 | 35 | hasContent(part){ 36 | return (d) => (d.region.value.content || {}).part == part; 37 | } 38 | 39 | clicked(d){ 40 | // if called outside the d3 context... 41 | d = d || d3.select(this).datum(); 42 | 43 | if(!(this.selectedRegion)){ 44 | return; 45 | } 46 | 47 | let {slide, region} = this.selectedRegion.get() || {}; 48 | 49 | if(slide === d.slide.key && region === d.region.key){ 50 | return this.selectedRegion.set(null); 51 | } 52 | 53 | this.selectedRegion.set({ 54 | slide: d.slide.key, 55 | region: d.region.key 56 | }); 57 | } 58 | 59 | update($slide) { 60 | let cellManager = this.cellManager; 61 | 62 | $slide.classed({"nbp-mini": 1}); 63 | 64 | let $region = $slide 65 | .selectAll(".nbp-region") 66 | .data((d) => d3.entries(this._regions(d)).map((region) => { 67 | return {slide: d, region}; 68 | })); 69 | 70 | $region.enter() 71 | .append("div") 72 | .classed({"nbp-region": 1}) 73 | .on("click", this.clicked) 74 | .on("mouseover", function({region}){ 75 | if(region.value.content){ 76 | cellManager.thumbnail(region.value.content) 77 | .then(({uri}) => { 78 | d3.select(this).style({"background-image": `url(${uri})`}) 79 | }); 80 | }else{ 81 | d3.select(this).style({"background-image": null}); 82 | } 83 | }); 84 | 85 | $region.exit() 86 | .remove(); 87 | 88 | let {slide, region} = ( 89 | this.selectedRegion && this.selectedRegion.get() 90 | ) || {}; 91 | 92 | $region 93 | .classed({ 94 | active: (d) => { 95 | return d.slide.key === slide && d.region.key === region 96 | }, 97 | "nbp-content-source": this.hasContent(PART.source), 98 | "nbp-content-outputs": this.hasContent(PART.outputs), 99 | "nbp-content-widgets": this.hasContent(PART.widgets), 100 | "nbp-content-whole": this.hasContent(PART.whole), 101 | "nbp-content-null": this.hasContent(null) 102 | }) 103 | // TODO: scale 104 | .style({ 105 | width: (d, i) => `${d.region.value.attrs.width * this.width(d, i)}px`, 106 | height: (d, i) => `${d.region.value.attrs.height * this.height(d, i)}px`, 107 | left: (d, i) => `${d.region.value.attrs.x * this.width(d, i)}px`, 108 | top: (d, i) => `${d.region.value.attrs.y * this.height(d, i)}px` 109 | }); 110 | } 111 | } 112 | 113 | export {MiniSlide}; 114 | -------------------------------------------------------------------------------- /src/es6/mode/base.es6: -------------------------------------------------------------------------------- 1 | import {Tree} from "../tree"; 2 | import {log} from "../logger"; 3 | 4 | /** Make a new Editor. The base app, as created in `index.html` */ 5 | export class BaseMode { 6 | /** Make a new Editor. 7 | * @param {baobab.Cursor} slide - the slide to edit */ 8 | constructor(root){ 9 | this.root = root; 10 | 11 | this.tree = new Tree({ 12 | slides: this.metadata().slides, 13 | themes: this.metadata().themes, 14 | root: this.root 15 | }).tree; 16 | 17 | this.slides = this.tree.select(["slides"]); 18 | 19 | this.themes = this.tree.select(["themes"]); 20 | 21 | this.log = log; 22 | 23 | this.init(); 24 | } 25 | 26 | init(){ 27 | return this; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/es6/mode/notebook.es6: -------------------------------------------------------------------------------- 1 | import Jupyter from "base/js/namespace"; 2 | 3 | import {d3, _, $} from "nbpresent-deps"; 4 | 5 | import {ICON} from "../icons"; 6 | 7 | import {Toolbar} from "../toolbar"; 8 | import {NotebookPresenter} from "../presenter/notebook"; 9 | 10 | import {Sorter} from "../sorter"; 11 | import {ThemeManager} from "../theme/manager"; 12 | import {Helper} from "../help/helper"; 13 | 14 | import {BaseMode} from "./base"; 15 | 16 | import {NotebookActions} from "../actions/notebook"; 17 | 18 | 19 | export const THEMER = "themer", 20 | SORTER = "sorter", 21 | HELPER = "helper", 22 | MODES = [ 23 | THEMER, 24 | SORTER, 25 | HELPER 26 | ]; 27 | 28 | export class NotebookMode extends BaseMode { 29 | 30 | init() { 31 | super.init(); 32 | 33 | this.enabled = this.tree.select(["app", "enabled"]); 34 | this.enabled.on("update", () => this.enabledChanged()); 35 | 36 | this.mode = this.tree.select(["app", "mode"]); 37 | this.mode.on("update", () => this.modeUpdated()); 38 | 39 | let debouncedSave = _.debounce(() => this.metadata(true), 1e3); 40 | 41 | [this.slides, this.themes].map(({on}) => on("update", debouncedSave)); 42 | 43 | this.slides.on("update", () => this.update()); 44 | 45 | this.initActions(); 46 | this.initEvents(); 47 | 48 | this.$body = d3.select("body"); 49 | 50 | return this.initUI(); 51 | } 52 | 53 | initUI(){ 54 | this.$ui = this.$body.append("div") 55 | .classed({"nbp-app": 1}); 56 | 57 | this.appBar = new Toolbar() 58 | .btnClass("btn-default btn-lg") 59 | .btnGroupClass("btn-group-vertical") 60 | .tipOptions({container: "body", placement: "top"}); 61 | 62 | this.$appBar = this.$ui.append("div") 63 | .classed({"nbp-app-bar": 1}) 64 | .datum([ 65 | [{ 66 | icon: `${ICON.presenter} fa-2x`, 67 | label: "Present", 68 | click: () => this.present() 69 | }], 70 | [{ 71 | icon: `${ICON.slides} fa-2x`, 72 | label: "Slides", 73 | click: () => this.mode.set(this.mode.get() === SORTER ? null : SORTER) 74 | }], 75 | [{ 76 | icon: `${ICON.themer} fa-2x`, 77 | label: "Themes", 78 | click: () => this.mode.set(this.mode.get() === THEMER ? null : THEMER) 79 | }], 80 | [{ 81 | icon: `${ICON.help} fa-2x`, 82 | label: "Help", 83 | click: () => this.mode.set(this.mode.get() === HELPER ? null : HELPER) 84 | }] 85 | ]) 86 | .call(this.appBar.update); 87 | 88 | return this.update(); 89 | } 90 | 91 | initEvents(){ 92 | $(Jupyter.notebook.events).on( 93 | "create.Cell", 94 | (evt, {cell}) => this.cellCreated(cell)); 95 | } 96 | 97 | /** Clear cell metadata (specifically on paste) of nbpresent stuff 98 | TODO: revisit when copy and paste might mean something 99 | */ 100 | cellCreated(cell){ 101 | _.defer(() => { 102 | delete cell.metadata.nbpresent; 103 | }); 104 | } 105 | 106 | initActions(){ 107 | this.actions = new NotebookActions([{ 108 | name: "show-sorter", 109 | value: { 110 | icon: `fa-${ICON.nbpresent}`, 111 | help: 'enable nbpresent', 112 | handler: () => this.show() 113 | } 114 | }, { 115 | name: "show-presentation", 116 | keys: ["esc"], 117 | value: { 118 | icon: `fa-${ICON.presenter}`, 119 | help: 'show presentation', 120 | handler: ()=> { 121 | if(this.presenter.presenting.get() && this.mode.get()){ 122 | this.mode.set(null); 123 | }else{ 124 | this.present(); 125 | } 126 | } 127 | } 128 | } 129 | ]); 130 | 131 | return this; 132 | } 133 | 134 | 135 | deinitActions(){ 136 | this.actions && this.actions.pop(); 137 | 138 | return this; 139 | } 140 | 141 | update(){ 142 | this.$body.classed({ 143 | "nbp-no-slides": !(Object.keys(this.slides.get() || {}).length) 144 | }); 145 | 146 | return this; 147 | } 148 | 149 | metadata(update){ 150 | let md = Jupyter.notebook.metadata; 151 | if(update){ 152 | md.nbpresent = { 153 | slides: this.slides.serialize(), 154 | themes: this.themes.serialize() 155 | }; 156 | }else{ 157 | return md.nbpresent || { 158 | slides: {}, 159 | themes: {} 160 | } 161 | } 162 | } 163 | 164 | enabledChanged(){ 165 | let enabled = this.enabled.get(); 166 | 167 | if(enabled){ 168 | this.ensurePresenter(); 169 | } 170 | 171 | this.$body.classed({ 172 | "nbp-app-enabled": enabled 173 | }); 174 | Jupyter.page.show_site(); 175 | 176 | if(enabled){ 177 | this.actions.push(); 178 | }else{ 179 | this.actions && this.actions.pop(); 180 | } 181 | } 182 | 183 | 184 | modeClass(mode){ 185 | return { 186 | themer: ThemeManager, 187 | sorter: Sorter, 188 | helper: Helper 189 | }[mode]; 190 | } 191 | 192 | modeUpdated(){ 193 | const current = this.mode.get(); 194 | 195 | MODES.filter((mode) => (mode !== current) && this[mode]) 196 | .map((mode) => { 197 | this[mode].destroy(); 198 | delete this[mode]; 199 | }); 200 | 201 | if(current && this[current]){ 202 | return; 203 | } 204 | 205 | let ModeClass = this.modeClass(current); 206 | 207 | current && (this[current] = new ModeClass(this.tree, {mode: this})); 208 | } 209 | 210 | 211 | show(){ 212 | let newEnabled = !(this.enabled.get()); 213 | 214 | this.enabled.set(newEnabled); 215 | 216 | if(!newEnabled){ 217 | this.mode.set(null); 218 | } 219 | 220 | return this; 221 | } 222 | 223 | 224 | ensurePresenter(){ 225 | if(!(this.presenter)){ 226 | this.presenter = new NotebookPresenter(this.tree); 227 | } 228 | return this; 229 | } 230 | 231 | present(force){ 232 | this.ensurePresenter(); 233 | 234 | let presenting = arguments.length ? force : 235 | !this.presenter.presenting.get(); 236 | 237 | this.presenter.presenting.set(presenting); 238 | 239 | return this; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/es6/mode/standalone.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {StandalonePresenter} from "../presenter/standalone"; 4 | 5 | import {BaseMode} from "./base"; 6 | 7 | export class StandaloneMode extends BaseMode { 8 | init() { 9 | super.init(); 10 | 11 | this.presenter = new StandalonePresenter(this.tree); 12 | 13 | this.presenter.presenting.set(true); 14 | 15 | return this; 16 | } 17 | 18 | metadata() { 19 | let tree = JSON.parse(d3.select("#nbpresent_tree").text()); 20 | 21 | // TODO: centralize serialized keys 22 | return { 23 | slides: tree.slides || {}, 24 | themes: tree.themes || {}, 25 | root: this.root 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/es6/package.es6: -------------------------------------------------------------------------------- 1 | import pkg from "../../package.json"; 2 | 3 | export const PKG = pkg; 4 | -------------------------------------------------------------------------------- /src/es6/parts.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | const PART = { 4 | source: "source", 5 | outputs: "outputs", 6 | widgets: "widgets", 7 | whole: "whole" 8 | }; 9 | 10 | const PART_SELECT = { 11 | source: ".inner_cell", 12 | outputs: ".output_wrapper", 13 | widgets: ".widget-area .widget-subarea", 14 | whole: null 15 | }; 16 | 17 | let partColor = d3.scale.ordinal() 18 | .domain(d3.values(PART)) 19 | .range(["blue", "red", "purple", "orange"]); 20 | 21 | export {PART, PART_SELECT, partColor}; 22 | -------------------------------------------------------------------------------- /src/es6/presenter/base.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {SpeakerBase} from "../speaker/base"; 4 | 5 | import {ThemeBase} from "../theme/base"; 6 | 7 | import {ManualLayout} from "../layout/manual"; 8 | import {TreemapLayout} from "../layout/treemap"; 9 | import {GridLayout} from "../layout/grid"; 10 | 11 | import {PART, PART_SELECT} from "../parts"; 12 | 13 | 14 | export class Presenter { 15 | constructor(tree) { 16 | this.tree = tree; 17 | 18 | this.slides = tree.select(["sortedSlides"]); 19 | 20 | this.cellManager = this.makeCellManager(); 21 | this.speaker = this.makeSpeaker(this.tree); 22 | 23 | this.initUI(); 24 | 25 | this.themes = this.tree.select(["themes"]); 26 | this.presenting = this.tree.select(["app", "presenting"]); 27 | this.current = this.tree.select(["app", "selectedSlide"]); 28 | 29 | this.presenting.on("update", () => this.present()); 30 | 31 | 32 | [this.slides, this.current, this.themes] 33 | .map(({on})=> on("update", () => this.update())); 34 | } 35 | 36 | makeCellManager() { 37 | throw new Error("Not implemented"); 38 | } 39 | 40 | 41 | makeSpeaker(tree){ 42 | return new SpeakerBase(tree); 43 | } 44 | 45 | initUI(){ 46 | this.$ui = d3.select("body") 47 | .append("div") 48 | .classed({"nbp-presenter": 1}); 49 | 50 | d3.select(window).on("mousemove", ()=> { 51 | this.presenting.get() && this.speaker.hint(); 52 | }); 53 | 54 | this.$style = d3.select("head") 55 | .append("style") 56 | .classed({"nbp-presenter-style": 1}); 57 | 58 | this.$backgrounds = this.$ui.append("div") 59 | .classed({"nbp-presenter-backgrounds": 1}); 60 | } 61 | 62 | initActions(){ 63 | 64 | } 65 | 66 | deinitActions(){ 67 | 68 | } 69 | 70 | /** Decode the slide object through the registry of layout classes. 71 | * @param {Object} slide - the key, value of the current slide 72 | * @return {Class} */ 73 | layoutClass(slide){ 74 | // TODO: refactor this into plugin mechanism 75 | return { 76 | manual: ManualLayout, 77 | treemap: TreemapLayout, 78 | grid: GridLayout 79 | }[slide.value.layout || "manual"]; 80 | } 81 | 82 | /** Initialize the layout 83 | * @param {Object} slide - the immutable data of the current slide 84 | * @return {RegionTree} */ 85 | updateLayout(slide){ 86 | let LayoutClass = this.layoutClass(slide); 87 | 88 | if(this.layout && 89 | this.layout.key() == LayoutClass.clsKey() && 90 | this.layout.slide.key === slide.key 91 | ){ 92 | this.layout.slide = slide; 93 | }else{ 94 | this.layout = new LayoutClass( 95 | this.tree, 96 | slide, 97 | document.documentElement 98 | ); 99 | } 100 | this.layout.init(); 101 | return this; 102 | } 103 | 104 | 105 | themeClass(slide){ // eslint-disable-line no-unused-vars 106 | // TODO: refactor this into plugin mechanism? 107 | return ThemeBase; 108 | } 109 | 110 | updateTheme(slide){ 111 | let ThemeClass = this.themeClass(slide), 112 | themes = this.themes.get(), 113 | themeId = slide.value.theme || themes.default; 114 | 115 | if(themes.theme && !themes.theme[themeId]){ 116 | themeId = Object.keys(themes.theme)[0]; 117 | } 118 | 119 | this.theme = new ThemeClass( 120 | this.themes.select(["theme", themeId || ""]), 121 | slide, 122 | this.$style 123 | ); 124 | 125 | this.theme.init(); 126 | 127 | return this; 128 | } 129 | 130 | present() { 131 | if(!(this.presenting.get())){ 132 | this.deinitActions(); 133 | }else{ 134 | this.initActions(); 135 | } 136 | this.update(); 137 | } 138 | 139 | getCells() { 140 | return this.cellManager.getCells(); 141 | } 142 | 143 | update() { 144 | const presenting = this.presenting.get(); 145 | 146 | let that = this; 147 | 148 | d3.select("body").classed({"nbp-presenting": presenting}); 149 | 150 | let current = this.current.get(), 151 | slide = this.slides.get([{key: current}]); 152 | 153 | // TODO: handle cleanup 154 | // transition = this.layout && this.layout.destroy() 155 | 156 | if(!slide){ 157 | return this.current.set(this.slides.get([0, "key"])); 158 | } 159 | 160 | this.updateLayout(slide) 161 | .updateTheme(slide); 162 | 163 | if(!presenting){ 164 | return this.clean(true); 165 | } 166 | 167 | let cells = this.getCells(); 168 | 169 | d3.selectAll(this.allPartSelect()) 170 | .classed({"nbp-unpresent": 1, "nbp-present": 0}); 171 | 172 | d3.entries(slide.value.regions) 173 | .filter(({value}) => value.content) 174 | .map((region) => { 175 | let {content} = region.value, 176 | cell = cells[content.cell]; 177 | 178 | if(!cell){ 179 | return; 180 | } 181 | 182 | let $el = d3.select(cell.element[0]), 183 | part = content.part === PART.whole ? 184 | $el : 185 | $el.select(PART_SELECT[content.part]); 186 | 187 | part 188 | .classed({ 189 | "nbp-unpresent": 0, 190 | "nbp-present": 1 191 | }) 192 | .each(() => that.theme.update(region, part)) 193 | .each(() => that.layout.update(region, part)); 194 | }); 195 | 196 | this.clean(); 197 | } 198 | 199 | allPartSelect(){ 200 | return d3.entries(PART_SELECT) 201 | .filter(({value}) => value) 202 | .map(({value}) => `.cell ${value}`) 203 | .concat([".cell"]) 204 | .join(", "); 205 | } 206 | 207 | clean(force){ 208 | let that = this, 209 | clean = (that.layout && this.layout.clean) || (() => 0); 210 | 211 | if(force){ 212 | d3.selectAll(this.allPartSelect()) 213 | .classed({"nbp-unpresent": 1, "nbp-present": 0}); 214 | } 215 | 216 | d3.selectAll(".nbp-unpresent") 217 | .call(clean) 218 | .classed({"nbp-unpresent": 0, "nbp-present": 0}); 219 | 220 | return this; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/es6/presenter/notebook.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {NotebookCellManager} from "../cells/notebook"; 4 | 5 | import {NotebookSpeaker} from "../speaker/notebook"; 6 | 7 | import {Presenter} from "./base"; 8 | 9 | import {NotebookActions} from "../actions/notebook"; 10 | 11 | export class NotebookPresenter extends Presenter { 12 | makeCellManager() { 13 | return new NotebookCellManager(); 14 | } 15 | makeSpeaker(tree){ 16 | return new NotebookSpeaker(tree); 17 | } 18 | 19 | initActions(){ 20 | let _actions = [{ 21 | name: "prev-slide", 22 | keys: ["left"], 23 | value: { 24 | icon: 'fa-step-backward', 25 | help: 'previous slide', 26 | handler: () => this.speaker.retreat() 27 | } 28 | }, { 29 | name: "next-slide", 30 | keys: ["right", "space"], 31 | value: { 32 | icon: 'fa-step-forward', 33 | help: 'next slide', 34 | handler: () => this.speaker.advance() 35 | } 36 | } 37 | ]; 38 | 39 | this.actions = new NotebookActions(_actions); 40 | this.actions.push(); 41 | } 42 | 43 | deinitActions(){ 44 | this.actions && this.actions.pop(); 45 | } 46 | 47 | clean(force){ 48 | super.clean(force); 49 | 50 | d3.entries(this.getCells()).map(({value}) => { 51 | value.code_mirror.refresh(); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/es6/presenter/standalone.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | import {StandaloneCellManager} from "../cells/standalone"; 3 | import {Presenter} from "./base"; 4 | 5 | const KEYS = { 6 | ESC: 27, 7 | LEFT: 37, 8 | RIGHT: 39, 9 | SPACE: 32 10 | }; 11 | 12 | export class StandalonePresenter extends Presenter { 13 | makeCellManager() { 14 | return new StandaloneCellManager(); 15 | } 16 | 17 | initActions(){ 18 | d3.select("body") 19 | .on("keydown", ()=>{ 20 | switch(d3.event.keyCode){ 21 | case KEYS.RIGHT: 22 | case KEYS.SPACE: 23 | this.speaker.advance(); 24 | break; 25 | case KEYS.LEFT: 26 | this.speaker.retreat(); 27 | break; 28 | case KEYS.ESC: 29 | this.present(); 30 | break; 31 | } 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/es6/regiontree.es6: -------------------------------------------------------------------------------- 1 | import {d3, uuid} from "nbpresent-deps"; 2 | 3 | import Jupyter from "base/js/namespace"; 4 | 5 | import {ICON} from "./icons"; 6 | import {Toolbar} from "./toolbar"; 7 | import {MiniSlide} from "./mini"; 8 | 9 | 10 | class RegionTree { 11 | constructor(slide, region){ 12 | this.slide = slide; 13 | this.selectedRegion = region; 14 | 15 | this.mini = (new MiniSlide(this.selectedRegion)) 16 | .regions((d) => { 17 | var obj = {}; 18 | obj[d.region.id] = d.region; 19 | return obj; 20 | }); 21 | 22 | this.initUI(); 23 | this.update(); 24 | 25 | this.update = this.update.bind(this); 26 | 27 | this.slide.on("update", this.update); 28 | this.selectedRegion.on("update", this.update); 29 | 30 | this.watcher = this.slide.tree.watch({ 31 | slide: this.slide, 32 | selectedRegion: this.selectedRegion 33 | }); 34 | } 35 | 36 | destroy() { 37 | this.$ui.transition() 38 | .style({opacity: 0}) 39 | .remove(); 40 | this.killed = true; 41 | } 42 | 43 | width() { 44 | return 300; 45 | } 46 | 47 | 48 | /** Set the layout to the given key 49 | * @param {String} layout - the layout to use 50 | * @return {RegionTree} */ 51 | layout(layout){ 52 | this.slide.set("layout", layout); 53 | return this; 54 | } 55 | 56 | initUI(){ 57 | this.$ui = d3.select("body") 58 | .append("div") 59 | .classed({"nbp-regiontree": 1}); 60 | 61 | let toolbar = new Toolbar(); 62 | 63 | this.$toolbar = this.$ui.append("div") 64 | .datum([ 65 | [{ 66 | icon: ICON.addRegion, 67 | click: () => this.addRegion(), 68 | label: "+ Region" 69 | }], 70 | // TODO: make this extensible 71 | [{ 72 | icon: ICON.manual, 73 | click: () => this.layout("manual"), 74 | label: "Free" 75 | }, { 76 | icon: ICON.treemap, 77 | click: () => this.layout("treemap"), 78 | label: "Treemap" 79 | }, { 80 | icon: ICON.grid, 81 | click: () => this.layout("grid"), 82 | label: "Grid" 83 | }] 84 | ]) 85 | .call(toolbar.update); 86 | } 87 | 88 | toggleStyle(style){ 89 | let {region} = this.selectedRegion.get() || {}, 90 | path = ["regions", region, "style", style]; 91 | this.slide.set(path, !(this.slide.get(path))); 92 | } 93 | 94 | addRegion(){ 95 | let id = uuid.v4(); 96 | this.slide.set(["regions", id], { 97 | id, 98 | attrs: { 99 | x: 0.1, 100 | y: 0.1, 101 | width: 0.8, 102 | height: 0.8 103 | } 104 | }); 105 | } 106 | 107 | update(){ 108 | if(this.killed){ 109 | return; 110 | } 111 | 112 | let that = this; 113 | 114 | let regions = d3.entries(this.slide.get("regions") || {}), 115 | $region = this.$ui.selectAll(".region_info") 116 | .data(regions, ({key}) => key), 117 | slide = this.slide.get(); 118 | 119 | $region 120 | .enter() 121 | .append("div") 122 | .classed({region_info: 1}); 123 | 124 | $region.exit() 125 | .transition() 126 | .style({opacity: 0}) 127 | .remove(); 128 | 129 | let $mini = $region 130 | .selectAll(".slide") 131 | .data((d) => [{ 132 | value: slide, 133 | key: slide.id, 134 | region: d.value 135 | }]); 136 | 137 | $mini.enter() 138 | .append("div") 139 | .classed({slide: 1}); 140 | 141 | $mini.exit().transition() 142 | .style({opacity: 0}) 143 | .remove(); 144 | 145 | $mini.call(this.mini.update); 146 | 147 | let layout = this.slide.get(["layout"]); 148 | 149 | let $attr = $region.selectAll(".region_attr") 150 | .data((region) => 151 | d3.entries(region.value.attrs) 152 | .map((attr) => { return {region, attr}; }) 153 | .filter((d)=>{ 154 | if(layout == "treemap"){ 155 | return d.attr.key.indexOf(layout) === 0; 156 | }else{ 157 | return d.attr.key.indexOf("treemap") === -1; 158 | } 159 | }) 160 | ) 161 | 162 | 163 | $attr.exit().remove(); 164 | 165 | $attr.enter() 166 | .append("div") 167 | .classed({ 168 | region_attr: 1, 169 | "input-group": 1, 170 | "input-group-sm": 1 171 | }) 172 | .call(function($attr){ 173 | $attr.append("span").classed({"input-group-btn": 1}) 174 | .append("button").classed({ 175 | btn: 1, 176 | "btn-default": 1, 177 | "btn-xs": 1, 178 | attr_name: 1 179 | }); 180 | 181 | $attr.append("span").classed({"input-group-addon": 1, attr_ns: 1}) 182 | .append("i") 183 | .classed({fa: 1, "fa-fw": 1}); 184 | 185 | $attr.append("input").classed({"form-control": 1}) 186 | .attr({type: "text"}) 187 | .each(function(){ 188 | Jupyter.keyboard_manager.register_events(this); 189 | }) 190 | .on("change", function(d){ 191 | let el = d3.select(this), 192 | val = parseFloat(el.property("value")); 193 | that.slide.set(["regions", d.region.key, d.attr.key], val); 194 | }); 195 | }) 196 | .on("mousedown", function(d){ 197 | that.makeSlider(this, d); 198 | }); 199 | 200 | 201 | $attr.select(".attr_name").text((d) => { 202 | return d.attr.key.indexOf(":") === -1 ? 203 | d.attr.key : 204 | d.attr.key.split(":")[1]; 205 | }); 206 | $attr.select(".attr_ns").each(function(d){ 207 | let hasIcon = d.attr.key.indexOf(":") !== -1; 208 | let el = d3.select(this) 209 | .style({ 210 | display: hasIcon ? null : "none" 211 | }); 212 | 213 | if(!hasIcon){ 214 | return; 215 | } 216 | 217 | // TODO: put this in layout 218 | let icon = { 219 | treemap: "tree", 220 | grid: "calculator" 221 | }[d.attr.key.split(":")[0]]; 222 | 223 | el.select(".fa") 224 | .classed(`fa-${icon}`, 1); 225 | 226 | }) 227 | $attr.select(".form-control").attr({ 228 | value: (d) => { 229 | if(typeof d.attr.value === "boolean"){ 230 | return d.attr.value; 231 | }else if(typeof d.attr.value === "number"){ 232 | return d.attr.value.toFixed(3); 233 | } 234 | } 235 | }); 236 | } 237 | 238 | makeSlider(element) { 239 | let that = this, 240 | x = d3.mouse(element)[0], 241 | el = d3.select(element); 242 | 243 | el.on("mousemove", function(d){ 244 | let x1 = d3.mouse(this)[0], 245 | dx = (x1 - x) / 20, 246 | path = ["regions", d.region.key, "attrs", d.attr.key]; 247 | 248 | x = x1; 249 | 250 | that.slide.set(path, that.slide.get(path) + dx) 251 | }) 252 | .on("mouseup", function(){ 253 | el.on("mousemove", null); 254 | }) 255 | .on("mouseexit", function(){ 256 | el.on("mousemove", null); 257 | }); 258 | } 259 | 260 | } 261 | 262 | export {RegionTree}; 263 | -------------------------------------------------------------------------------- /src/es6/speaker/base.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {Toolbar} from "../toolbar"; 4 | 5 | export class SpeakerBase { 6 | constructor(tree){ 7 | this.tree = tree; 8 | this.current = this.tree.select(["app", "selectedSlide"]); 9 | 10 | this.presenting = this.tree.select(["app", "presenting"]); 11 | this.presenting.on("update", () => this.update()); 12 | 13 | this 14 | .init() 15 | .initUI() 16 | .update(); 17 | } 18 | 19 | init(){ 20 | return this; 21 | } 22 | 23 | initUI(){ 24 | this.$ui = d3.select("body") 25 | .append("div") 26 | .classed({nbpresent_speaker: 1}) 27 | .on("mouseover", ()=> this.focused = true ) 28 | .on("mouseout", ()=> { 29 | this.focused = false; 30 | this.startDecay(); 31 | }); 32 | 33 | this.initToolbar(); 34 | this.startDecay(); 35 | return this; 36 | } 37 | 38 | update(){ 39 | this.$ui.style({display: this.presenting.get() ? null : "none"}); 40 | } 41 | 42 | decay(){ 43 | this.energy = (this.energy || 0) * 0.8; 44 | this.$ui.style({ 45 | opacity: this.focused ? 1 : this.energy 46 | }); 47 | 48 | if(this.focused){ 49 | clearInterval(this.decayInterval); 50 | this.decayInterval = null; 51 | }else if(this.energy <= 0.1){ 52 | clearInterval(this.decayInterval); 53 | this.decayInterval = null; 54 | this.energy = 0; 55 | } 56 | } 57 | 58 | startDecay(){ 59 | this.energy = 0.6; 60 | if(!(this.decayInerval)){ 61 | this.decayInerval = setInterval(()=>this.decay(), 100) 62 | } 63 | } 64 | 65 | hint(){ 66 | this.startDecay(); 67 | } 68 | 69 | toolbarIcons(){ 70 | let that = this; 71 | 72 | return [ 73 | [{ 74 | icon: "fast-backward", 75 | click: () => that.current.set(this.tree.get(["sortedSlides", 0, "key"])), 76 | label: "First" 77 | }], 78 | [{ 79 | icon: "step-backward", 80 | click: () => this.retreat(), 81 | label: "Previous" 82 | }], 83 | [{ 84 | icon: "step-forward", 85 | click: () => this.advance(), 86 | label: "Next" 87 | }] 88 | ]; 89 | } 90 | 91 | retreat(){ 92 | let current = this.tree.get(["slides", this.current.get()]); 93 | this.current.set(current.prev); 94 | } 95 | 96 | advance(){ 97 | let slides = this.tree.get(["slides"]), 98 | current = slides[this.current.get()]; 99 | 100 | d3.entries(slides).map((d)=> { 101 | if(d.value.prev === current.id){ 102 | this.current.set(d.key); 103 | } 104 | }); 105 | 106 | return this.current.get(); 107 | } 108 | 109 | initToolbar(){ 110 | let toolbar = new Toolbar(); 111 | 112 | toolbar 113 | .btnGroupClass("btn-group-vertical") 114 | .btnClass("btn-link btn-lg") 115 | .tipOptions({container: "body", placement: "top"}); 116 | 117 | // TODO: Make this overlay (Jupyter-branded Reveal Compass) 118 | this.$toolbar = this.$ui.append("div") 119 | .classed({"nbp-presenter-toolbar": 1}) 120 | .datum(this.toolbarIcons()) 121 | .call(toolbar.update); 122 | 123 | return this; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/es6/speaker/notebook.es6: -------------------------------------------------------------------------------- 1 | import {SpeakerBase} from "./base"; 2 | 3 | export class NotebookSpeaker extends SpeakerBase { 4 | toolbarIcons(){ 5 | return super.toolbarIcons().concat([ 6 | [{ 7 | icon: "book", 8 | click: () => this.presenting.set(false), 9 | label: "Notebook" 10 | }] 11 | ]); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/es6/templates/_util.es6: -------------------------------------------------------------------------------- 1 | export function regions([x, y, width, height]){ 2 | return {x: x / 10, y: y / 10, width: width / 10, height: height / 10}; 3 | } 4 | 5 | export function slide(dims){ 6 | return { 7 | regions: dims.map(regions) 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/es6/templates/library.es6: -------------------------------------------------------------------------------- 1 | import {d3, uuid, $} from "nbpresent-deps"; 2 | 3 | import {MiniSlide, SLIDE_WIDTH} from "../mini"; 4 | 5 | import {MANUAL_TEMPLATES} from "./manual"; 6 | 7 | const _TEMPLATES = MANUAL_TEMPLATES; 8 | 9 | class TemplateLibrary { 10 | constructor(picked) { 11 | this.picked = picked; 12 | 13 | this.initUI(); 14 | 15 | this.mini = new MiniSlide(); 16 | 17 | this.fakeSlide = this.fakeSlide.bind(this); 18 | this.update = this.update.bind(this); 19 | 20 | this.x = d3.scale.linear() 21 | .domain([0, 1]) 22 | .range([0, SLIDE_WIDTH + 20]); 23 | 24 | this.update(); 25 | } 26 | 27 | destroy() { 28 | this.$ui.transition() 29 | .style({opacity: 0}) 30 | .remove(); 31 | this.killed = true; 32 | } 33 | 34 | fakeSlide(template) { 35 | let id = uuid.v4(); 36 | return { 37 | key: id, 38 | value: { 39 | id, 40 | regions: template.regions.reduce((memo, template) => { 41 | let id = uuid.v4(); 42 | memo[id] = $.extend({}, {id}, {attrs: template}); 43 | return memo; 44 | }, {}) 45 | } 46 | } 47 | } 48 | 49 | initUI(){ 50 | this.$ui = d3.select("body") 51 | .append("div") 52 | .classed({"nbp-template-library": 1}); 53 | 54 | this.$ui.append("button") 55 | .classed({btn: 1, hide_library: 1, "btn-default": 1}) 56 | .on("click", () => this.picked(null)) 57 | .append("i") 58 | .classed({fa: 1, "fa-remove": 1}); 59 | 60 | this.$ui.append("h3") 61 | .classed({from_template: 1}) 62 | .text("Pick template"); 63 | 64 | this.$ui.append("h3") 65 | .classed({from_slide: 1}) 66 | .text("Reuse slide as template"); 67 | 68 | this.$slides = this.$ui.append("div") 69 | .classed({"npb-template-library-slides": 1}); 70 | } 71 | 72 | update(){ 73 | let $slide = this.$slides.selectAll(".slide") 74 | .data(_TEMPLATES.map(this.fakeSlide)); 75 | 76 | $slide.enter() 77 | .append("div") 78 | .classed({slide: 1}) 79 | .call(this.mini.update); 80 | 81 | $slide.style({left: (d, i) => `${this.x(i)}px`}); 82 | 83 | $slide.on("click", (d) => this.picked(d)); 84 | 85 | $slide.exit().remove(); 86 | } 87 | } 88 | 89 | export {TemplateLibrary}; 90 | -------------------------------------------------------------------------------- /src/es6/templates/manual.es6: -------------------------------------------------------------------------------- 1 | import {slide} from "./_util"; 2 | 3 | export const MANUAL_TEMPLATES = [ 4 | [[0, 0, 10, 10]], 5 | [[0.5, 1, 4.5, 8], [5, 1, 4.5, 8]], 6 | [[1, 1, 8, 6], [1, 7, 4, 2], [5, 7, 4, 2]], 7 | [[1, 1, 2, 8], [4, 1, 2, 8], [7, 1, 2, 8]], 8 | [[1, 1, 8, 8], [1, 1, 2, 8], [4, 1, 2, 8], [7, 1, 2, 8]], 9 | [[1, 1, 4, 4], [5, 1, 4, 4], [1, 5, 4, 4], [5, 5, 4, 4]] 10 | ].map(slide, (d) => d / 10); 11 | -------------------------------------------------------------------------------- /src/es6/theme/base.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {loadFonts} from "./fonts"; 4 | 5 | const PRESENT_PREFIX = ".nbp-presenting .nbp-present"; 6 | 7 | export class ThemeBase{ 8 | constructor(theme, slide, style){ 9 | this.theme = theme; 10 | this.rules = theme.select(["rules"]); 11 | this.palette = theme.select(["palette"]); 12 | 13 | this.backgrounds = theme.select(["backgrounds"]); 14 | this.textBase = theme.select(["text-base"]); 15 | this.slide = slide; 16 | this.$style = style; 17 | } 18 | 19 | update(region, part){ // eslint-disable-line no-unused-vars 20 | // TODO: allow per-region (heh, and per-slide) theme customization 21 | } 22 | 23 | init(){ 24 | let palette = this.palette.get() || {}; 25 | 26 | let rules = [{ 27 | key: "", 28 | value: this.textBase.get() || {} 29 | }].concat(d3.entries(this.rules.get() || {})) 30 | .map(({key, value})=>{ 31 | let directives = d3.entries(value).map(({key, value})=>{ 32 | switch(key){ 33 | case "font-size": 34 | value = `${value}rem`; 35 | break; 36 | case "font-family": 37 | loadFonts([value]); 38 | value = `"${value}"`; 39 | break; 40 | case "color": 41 | let {rgb=[0, 0, 0], a=1.0} = palette[value] || {}; 42 | value = `rgba(${rgb}, ${a})`; 43 | break; 44 | } 45 | 46 | return `\t${key}: ${value};`; 47 | }); 48 | 49 | const BLOCKS = d3.range(1, 7).map((i) => `h${i}`) 50 | .concat(["blockquote"]); 51 | 52 | if(BLOCKS.indexOf(key) !== -1){ 53 | directives.push('margin: 0 0 1.3em 0;') 54 | directives.push('padding: 0;'); 55 | } 56 | 57 | return `${PRESENT_PREFIX} ${key}{ 58 | line-height: 1.25; 59 | ${directives.join("\n")} 60 | }`; 61 | }) 62 | .join("\n"); 63 | 64 | this.$style.text(rules); 65 | 66 | let backgrounds = this.backgrounds.get() || {}, 67 | background = d3.select(".nbp-presenter-backgrounds") 68 | .selectAll(".nbp-presenter-background") 69 | .data(d3.entries(backgrounds, ({key}) => key)); 70 | 71 | background.exit().remove(); 72 | 73 | background.enter().append("div").classed({ 74 | "nbp-presenter-background": 1 75 | }); 76 | 77 | background.style({ 78 | "background-image": ({value}) => { 79 | let img = value["background-image"]; 80 | return img && `url(${img})`; 81 | }, 82 | "background-color": ({value}) => { 83 | let {rgb} = palette[value["background-color"]] || {}; 84 | return rgb && `rgb(${rgb})` 85 | }, 86 | "background-position": ({value}) => `${value.x} ${value.y}`, 87 | left: 0, 88 | right: 0, 89 | bottom: 0, 90 | top: 0 91 | }); 92 | } 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/es6/theme/card.es6: -------------------------------------------------------------------------------- 1 | import {d3} from "nbpresent-deps"; 2 | 3 | import {loadFonts} from "./fonts"; 4 | 5 | export class ThemeCard{ 6 | // accepts a d3 selector bound to {key, value} of themes 7 | update(theme){ 8 | theme.classed({"nbp-theme-card": 1}); 9 | 10 | theme.each(function(){ 11 | let el = d3.select(this); 12 | ["palette", "backgrounds", "fonts"].map((bit) => { 13 | if(!el.select(`.nbp-theme-card-${bit}`).node()){ 14 | el.append("div") 15 | .attr("class", `nbp-theme-card-${bit}`); 16 | } 17 | }); 18 | }); 19 | 20 | this.updateBackground(theme) 21 | .updateColor(theme) 22 | .updateFont(theme); 23 | } 24 | 25 | updateBackground(theme){ 26 | let background = theme.select(".nbp-theme-card-backgrounds") 27 | .selectAll(".nbp-theme-card-background") 28 | .data((theme) => { 29 | let backgrounds = d3.entries(theme.value.backgrounds || {}); 30 | 31 | return backgrounds.map(({value}) => { 32 | return { 33 | theme: theme.value, 34 | background: value, 35 | height: 100 / backgrounds.length 36 | }; 37 | }); 38 | }); 39 | 40 | background.enter().append("div") 41 | .classed({"nbp-theme-card-background": 1}); 42 | 43 | background.exit().remove(); 44 | 45 | background.style({ 46 | height: ({height}) => `${height}%`, 47 | "background-color": ({theme, background}) => { 48 | let color = background["background-color"]; 49 | color = color && theme.palette[color]; 50 | if(color){ 51 | return `rgb(${color.rgb})`; 52 | } 53 | }, 54 | "background-image": ({background}) => { 55 | let bg = background["background-image"]; 56 | if(bg){ 57 | return `url(${bg})`; 58 | } 59 | } 60 | }); 61 | 62 | return this; 63 | } 64 | 65 | updateColor(theme){ 66 | let color = theme.select(".nbp-theme-card-palette") 67 | .selectAll(".nbp-theme-card-color") 68 | .data(({value}) => d3.entries(value.palette)); 69 | 70 | color.enter().append("div") 71 | .classed({"nbp-theme-card-color": 1}); 72 | 73 | color.exit().remove(); 74 | 75 | color.style({ 76 | "background-color": ({value}) => { 77 | return `rgba(${value.rgb || [0,0,0]}, ${value.a || 1})`; 78 | } 79 | }); 80 | 81 | return this; 82 | } 83 | 84 | updateFont(theme){ 85 | let rule = theme.select(".nbp-theme-card-fonts") 86 | .selectAll(".nbp-theme-card-font") 87 | .data(({value}) => { 88 | let rules = d3.entries(value.rules), 89 | base = value["text-base"]; 90 | if(base){ 91 | rules = rules.concat([{key: "∅", value: base}]); 92 | } 93 | return rules.filter(({value}) => { 94 | return value["font-family"] || value["font-size"] || value["font-color"]; 95 | }); 96 | }); 97 | 98 | rule.enter().append("div") 99 | .classed({"nbp-theme-card-font": 1}); 100 | 101 | rule.exit().remove(); 102 | 103 | rule.text(({key, value}) => `${key} ${value["font-size"] || ""}`) 104 | .style({ 105 | "font-family": ({value}) => { 106 | let font = value["font-family"]; 107 | font && loadFonts([font]); 108 | return font; 109 | }, 110 | "color": (({value}) => value["color"]) 111 | }); 112 | 113 | return this; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/es6/theme/fonts.es6: -------------------------------------------------------------------------------- 1 | import {WebFont, _} from "nbpresent-deps"; 2 | 3 | 4 | export const SYMB = `h1 h2 h3 h4 h5 h6 h7 5 | ul ol li 6 | blockquote pre code 7 | strong em a i 8 | table thead tbody tfoot th td` 9 | .split(/\s+/); 10 | 11 | export const FONTS_MONO = [ 12 | "Inconsolata", 13 | "Source Code Pro", 14 | "Roboto Mono", 15 | "Droid Sans Mono", 16 | "Ubuntu Mono", 17 | "VT323", 18 | "PT Mono", 19 | "Cousine", 20 | "Oxygen Mono", 21 | "Anonymous Pro", 22 | "Fira Mono", 23 | "Cutive Mono", 24 | "Nova Mono", 25 | "Share Tech Mono" 26 | ]; 27 | 28 | // http://fontpair.co 29 | export const FONTS = [ 30 | "ABeeZee", 31 | "Abel", 32 | "Abril Fatface", 33 | "Alegreya", 34 | "Alfa Slab One", 35 | "Alice", 36 | "Allerta", 37 | "Amaranth", 38 | "Amatic SC", 39 | "Andika", 40 | "Arimo", 41 | "Asap", 42 | "Average", 43 | "Bevan", 44 | "Bitter", 45 | "Bree Serif", 46 | "Cabin", 47 | "Cantata One", 48 | "Cardo", 49 | "Clicker Script", 50 | "Crete Round", 51 | "Crimson Text", 52 | "Dancing Script", 53 | "Didact Gothic", 54 | "Domine", 55 | "Dosis", 56 | "Droid Sans", 57 | "EB Garamond", 58 | "Exo", 59 | "Fanwood Text", 60 | "Fauna One", 61 | "Fjalla One", 62 | "Flamenco", 63 | "Francois One", 64 | "Gentium Book Basic", 65 | "Gudea", 66 | "Hind", 67 | "Imprima", 68 | "Istok Web", 69 | "Josefin Sans", 70 | "Josefin Slab", 71 | "Judson", 72 | "Kameron", 73 | "Kreon", 74 | "Lato", 75 | "Ledger", 76 | "Libre Baskerville", 77 | "Lobster", 78 | "Lora", 79 | "Lustria", 80 | "Medula One", 81 | "Merriweather", 82 | "Montserrat", 83 | "Muli", 84 | "Neuton", 85 | "Nixie One", 86 | "Noto Sans", 87 | "Nunito", 88 | "Old Standard TT", 89 | "Open Sans", 90 | "Oswald", 91 | "Ovo", 92 | "Oxygen", 93 | "PT Sans", 94 | "PT Serif", 95 | "Pacifico", 96 | "Patua One", 97 | "Philosopher", 98 | "Playfair Display", 99 | "Playfair Display SC", 100 | "Pontano Sans", 101 | "Quando", 102 | "Quattrocento", 103 | "Quattrocento Sans", 104 | "Questrial", 105 | "Quicksand", 106 | "Raleway", 107 | "Rancho", 108 | "Roboto", 109 | "Roboto Slab", 110 | "Rokkitt", 111 | "Rufina", 112 | "Sacramento", 113 | "Sansita One", 114 | "Shadows Into Light", 115 | "Signika", 116 | "Sintony", 117 | "Slabo 13px", 118 | "Source Sans Pro", 119 | "Squada One", 120 | "Stint Ultra Expanded", 121 | "Ubuntu", 122 | "Ultra", 123 | "Unica One", 124 | "Vollkorn", 125 | "Walter Turncoat", 126 | "Yeseva One" 127 | ].concat(FONTS_MONO); 128 | 129 | let _fontLoaded = { 130 | Lato: 1, 131 | "google:Lato": 1 132 | }; 133 | 134 | /* TODO: a generalized namespace thingy for fonts, a la JSON-LD: 135 | - google:Lato 136 | - nbp:Lato 137 | */ 138 | export function loadFonts(fonts){ 139 | fonts = _.difference(fonts, _.keys(_fontLoaded)); 140 | if(!fonts.length){ return []; } 141 | WebFont.load({google: {families: fonts }}); 142 | _.extend(_fontLoaded, _.object(fonts, fonts)); 143 | return fonts; 144 | } 145 | 146 | loadFonts(["Lato"]); 147 | -------------------------------------------------------------------------------- /src/es6/theme/manager.es6: -------------------------------------------------------------------------------- 1 | import {d3, uuid, _} from "nbpresent-deps"; 2 | 3 | import {ICON} from "../icons"; 4 | import {Toolbar} from "../toolbar"; 5 | 6 | import {ThemeCard} from "./card"; 7 | 8 | import {PLAIN_THEMES} from "./theme/plain"; 9 | import {REVEAL_THEMES} from "./theme/reveal"; 10 | 11 | 12 | // TODO: make this configurable 13 | const BASE_THEMES = _.extend({}, 14 | PLAIN_THEMES, 15 | REVEAL_THEMES 16 | ); 17 | 18 | export class ThemeManager { 19 | constructor(tree){ 20 | this.tree = tree; 21 | 22 | this.themes = tree.select(["themes", "theme"]); 23 | this.defaultTheme = tree.select(["themes", "default"]); 24 | 25 | let mgrCursor = tree.select(["app", "theme-manager"]); 26 | this.stockThemes = mgrCursor.select(["themes"]); 27 | this.current = mgrCursor.select(["current"]); 28 | 29 | this.card = new ThemeCard(); 30 | 31 | [this.defaultTheme, this.themes, this.stockThemes] 32 | .map(({on}) => on("update", ()=> this.update())); 33 | this.current.on("update", () => [ 34 | this.currentUpdated(), 35 | _.defer(()=> this.update()) 36 | ]); 37 | 38 | this.initUI(); 39 | 40 | _.defer(() => this.$body.classed({"nbp-theming": 1})); 41 | } 42 | 43 | destroy(){ 44 | this.$body.classed({"nbp-theming": 0}); 45 | 46 | _.delay(() => { 47 | this.$ui.remove(); 48 | }, 200); 49 | } 50 | 51 | initUI(){ 52 | this.$body = d3.select("body"); 53 | this.$ui = this.$body.append("div") 54 | .classed({"nbp-theme-manager": 1}); 55 | 56 | this.toolbar = new Toolbar() 57 | .btnGroupClass("btn-group-vertical"); 58 | 59 | this.$toolbar = this.$ui.append("div") 60 | .datum([ 61 | [{ 62 | icon: ICON.trash, 63 | visible: () => this.current.get(), 64 | click: () => this.destroyTheme(this.current.get()), 65 | label: "- Theme" 66 | }] 67 | ]); 68 | 69 | this.$ui.append("div") 70 | .classed({"nbp-theme-previews": 1}) 71 | .append("h2").text("Your Themes"); 72 | 73 | this.$ui.append("div") 74 | .classed({"nbp-theme-previews-canned": 1}) 75 | .append("h2").text("Community Themes"); 76 | 77 | return this.update() 78 | .currentUpdated(); 79 | } 80 | 81 | update(){ 82 | let themes = this.themes.get() || {}, 83 | current = this.current.get(), 84 | stock = _.extend({}, this.stockThemes.get(), BASE_THEMES), 85 | theme = this.$ui.select(".nbp-theme-previews") 86 | .selectAll(".nbp-theme-preview") 87 | .data(d3.entries(themes), ({key}) => key), 88 | canned = this.$ui.select(".nbp-theme-previews-canned") 89 | .selectAll(".nbp-theme-preview-canned") 90 | .data(d3.entries(stock), ({key}) => key), 91 | defaultTheme = this.defaultTheme.get(); 92 | 93 | this.$toolbar.call(this.toolbar.update); 94 | 95 | theme.enter().append("div") 96 | .classed({"nbp-theme-preview": 1}) 97 | .call((theme) => { 98 | theme.append("div") 99 | .classed({"nbp-theme-preview-card": 1}) 100 | .on("click", ({key}) => this.current.set(key)); 101 | 102 | theme.append("a") 103 | .classed({"nbp-default-theme btn": 1}) 104 | .on("click", ({key}) => this.defaultTheme.set(key)) 105 | .append("i") 106 | .classed({"fa fa-2x": 1}); 107 | }); 108 | 109 | theme.exit().remove(); 110 | 111 | theme.classed({ 112 | "nbp-theme-preview-current": ({key}) => key === current 113 | }); 114 | 115 | let _cls = []; 116 | 117 | _cls[`fa-${ICON.defaultTheme}`] = ({key}) => key !== defaultTheme 118 | _cls[`fa-${ICON.defaultThemeActive}`] = ({key}) => key === defaultTheme; 119 | 120 | theme.select(".nbp-default-theme i") 121 | .classed(_cls); 122 | 123 | this.card.update(theme.select(".nbp-theme-preview-card")); 124 | 125 | canned.enter().append("div") 126 | .classed({"nbp-theme-preview-canned": 1}) 127 | .on("click", ({value}) => { 128 | let id = uuid.v4(); 129 | this.themes.set([id], _.extend({}, value, {id})); 130 | this.current.set(id); 131 | }); 132 | 133 | canned.exit().remove(); 134 | 135 | this.card.update(canned); 136 | 137 | return this; 138 | } 139 | 140 | currentUpdated(){ 141 | let current = this.current.get(), 142 | defaultTheme = this.defaultTheme.get(), 143 | themes = this.themes.get(); 144 | 145 | if(current){ 146 | if(!defaultTheme || !themes[defaultTheme]){ 147 | this.defaultTheme.set(current); 148 | } 149 | } 150 | } 151 | 152 | destroyTheme(theme){ 153 | theme = theme || this.current.get(); 154 | if(theme){ 155 | this.themes.unset([theme]); 156 | } 157 | this.current.set(null); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/es6/theme/theme/plain.es6: -------------------------------------------------------------------------------- 1 | export const PLAIN_THEMES = { 2 | "dark": { 3 | "backgrounds": { 4 | "dc7afa04-bf90-40b1-82a5-726e3cff5267": { 5 | "background-color": "31af15d2-7e15-44c5-ab5e-e04b16a89eff", 6 | "id": "dc7afa04-bf90-40b1-82a5-726e3cff5267" 7 | } 8 | }, 9 | "palette": { 10 | "19cc588f-0593-49c9-9f4b-e4d7cc113b1c": { 11 | "id": "19cc588f-0593-49c9-9f4b-e4d7cc113b1c", 12 | "rgb": [ 13 | 252, 14 | 252, 15 | 252 16 | ] 17 | }, 18 | "31af15d2-7e15-44c5-ab5e-e04b16a89eff": { 19 | "id": "31af15d2-7e15-44c5-ab5e-e04b16a89eff", 20 | "rgb": [ 21 | 68, 22 | 68, 23 | 68 24 | ] 25 | }, 26 | "50f92c45-a630-455b-aec3-788680ec7410": { 27 | "id": "50f92c45-a630-455b-aec3-788680ec7410", 28 | "rgb": [ 29 | 197, 30 | 226, 31 | 245 32 | ] 33 | }, 34 | "c5cc3653-2ee1-402a-aba2-7caae1da4f6c": { 35 | "id": "c5cc3653-2ee1-402a-aba2-7caae1da4f6c", 36 | "rgb": [ 37 | 43, 38 | 126, 39 | 184 40 | ] 41 | }, 42 | "efa7f048-9acb-414c-8b04-a26811511a21": { 43 | "id": "efa7f048-9acb-414c-8b04-a26811511a21", 44 | "rgb": [ 45 | 25.118061674008803, 46 | 73.60176211453744, 47 | 107.4819383259912 48 | ] 49 | } 50 | }, 51 | "rules": { 52 | "a": { 53 | "color": "19cc588f-0593-49c9-9f4b-e4d7cc113b1c" 54 | }, 55 | "blockquote": { 56 | "color": "50f92c45-a630-455b-aec3-788680ec7410", 57 | "font-size": 3 58 | }, 59 | "code": { 60 | "font-family": "Anonymous Pro" 61 | }, 62 | "h1": { 63 | "color": "19cc588f-0593-49c9-9f4b-e4d7cc113b1c", 64 | "font-family": "Merriweather", 65 | "font-size": 8 66 | }, 67 | "h2": { 68 | "color": "19cc588f-0593-49c9-9f4b-e4d7cc113b1c", 69 | "font-family": "Merriweather", 70 | "font-size": 6 71 | }, 72 | "h3": { 73 | "color": "50f92c45-a630-455b-aec3-788680ec7410", 74 | "font-family": "Lato", 75 | "font-size": 5.5 76 | }, 77 | "h4": { 78 | "color": "c5cc3653-2ee1-402a-aba2-7caae1da4f6c", 79 | "font-family": "Lato", 80 | "font-size": 5 81 | }, 82 | "h5": { 83 | "font-family": "Lato" 84 | }, 85 | "h6": { 86 | "font-family": "Lato" 87 | }, 88 | "h7": { 89 | "font-family": "Lato" 90 | }, 91 | "li": { 92 | "color": "50f92c45-a630-455b-aec3-788680ec7410", 93 | "font-size": 3.25 94 | }, 95 | "pre": { 96 | "font-family": "Anonymous Pro", 97 | "font-size": 4 98 | } 99 | }, 100 | "text-base": { 101 | "color": "19cc588f-0593-49c9-9f4b-e4d7cc113b1c", 102 | "font-family": "Lato", 103 | "font-size": 4 104 | } 105 | }, 106 | "plain": { 107 | "palette": { 108 | "19cc588f-0593-49c9-9f4b-e4d7cc113b1c": { 109 | "id": "19cc588f-0593-49c9-9f4b-e4d7cc113b1c", 110 | "rgb": [ 111 | 252, 112 | 252, 113 | 252 114 | ] 115 | }, 116 | "31af15d2-7e15-44c5-ab5e-e04b16a89eff": { 117 | "id": "31af15d2-7e15-44c5-ab5e-e04b16a89eff", 118 | "rgb": [ 119 | 68, 120 | 68, 121 | 68 122 | ] 123 | }, 124 | "50f92c45-a630-455b-aec3-788680ec7410": { 125 | "id": "50f92c45-a630-455b-aec3-788680ec7410", 126 | "rgb": [ 127 | 155, 128 | 177, 129 | 192 130 | ] 131 | }, 132 | "c5cc3653-2ee1-402a-aba2-7caae1da4f6c": { 133 | "id": "c5cc3653-2ee1-402a-aba2-7caae1da4f6c", 134 | "rgb": [ 135 | 43, 136 | 126, 137 | 184 138 | ] 139 | }, 140 | "efa7f048-9acb-414c-8b04-a26811511a21": { 141 | "id": "efa7f048-9acb-414c-8b04-a26811511a21", 142 | "rgb": [ 143 | 25.118061674008803, 144 | 73.60176211453744, 145 | 107.4819383259912 146 | ] 147 | } 148 | }, 149 | "rules": { 150 | "blockquote": { 151 | "color": "50f92c45-a630-455b-aec3-788680ec7410" 152 | }, 153 | "code": { 154 | "font-family": "Anonymous Pro" 155 | }, 156 | "h1": { 157 | "color": "c5cc3653-2ee1-402a-aba2-7caae1da4f6c", 158 | "font-family": "Lato", 159 | "font-size": 8 160 | }, 161 | "h2": { 162 | "color": "c5cc3653-2ee1-402a-aba2-7caae1da4f6c", 163 | "font-family": "Lato", 164 | "font-size": 6 165 | }, 166 | "h3": { 167 | "color": "50f92c45-a630-455b-aec3-788680ec7410", 168 | "font-family": "Lato", 169 | "font-size": 5.5 170 | }, 171 | "h4": { 172 | "color": "c5cc3653-2ee1-402a-aba2-7caae1da4f6c", 173 | "font-family": "Lato", 174 | "font-size": 5 175 | }, 176 | "h5": { 177 | "font-family": "Lato" 178 | }, 179 | "h6": { 180 | "font-family": "Lato" 181 | }, 182 | "h7": { 183 | "font-family": "Lato" 184 | }, 185 | "pre": { 186 | "font-family": "Anonymous Pro", 187 | "font-size": 4 188 | } 189 | }, 190 | "text-base": { 191 | "font-family": "Merriweather", 192 | "font-size": 4 193 | } 194 | } 195 | }; 196 | -------------------------------------------------------------------------------- /src/es6/tome/arise.es6: -------------------------------------------------------------------------------- 1 | import {log} from "../logger"; 2 | import {PART} from "../parts"; 3 | import {BaseTome} from "./base"; 4 | 5 | /** import slideshow/RISE presentations */ 6 | export class AriseTome extends BaseTome { 7 | icon(){ 8 | return "cube"; 9 | } 10 | 11 | title(){ 12 | return "RISE/reveal.js"; 13 | } 14 | 15 | label() { 16 | let slides = this.cells() 17 | .map(({metadata}) => (metadata.slideshow || {}).slide_type) 18 | .filter(String) 19 | .filter((type) => ["slide", "subslide"].indexOf(type) !== -1); 20 | return `${slides.length} Slides`; 21 | } 22 | 23 | relevant() { 24 | return this.cells() 25 | .filter(({metadata}) => metadata.slideshow) 26 | .length; 27 | } 28 | 29 | study(){ 30 | let slides = [], 31 | prev; 32 | 33 | this.cells().map((cell, i) => { 34 | prev = slides.slice(-1)[0]; 35 | 36 | let slideType = (cell.metadata.slideshow || {}) .slide_type || 37 | (i ? "-" : "slide"); 38 | 39 | switch(slideType){ 40 | case "subslide": 41 | case "slide": 42 | slides.push(this.wholeCellSlide(cell, prev ? prev.id : null)); 43 | break; 44 | case "fragment": 45 | case "-": 46 | prev && this.fromSlideshowContinuation(cell, prev); 47 | break; 48 | case "notes": 49 | case "skip": 50 | break; 51 | default: 52 | log.debug(slideType, "not implemented yet"); 53 | break; 54 | } 55 | }); 56 | 57 | return slides; 58 | } 59 | 60 | fromSlideshowContinuation(cell, prev){ 61 | let id = this.sorter.nextId(), 62 | cellId = this.sorter.cellId(cell); 63 | prev.regions[id] = { 64 | id, 65 | content: {cell: cellId, part: PART.whole}, 66 | attrs: { 67 | x: 0.1, 68 | y: 0.5, 69 | width: 0.8, 70 | height: 0.4 71 | } 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/es6/tome/base.es6: -------------------------------------------------------------------------------- 1 | import Jupyter from "base/js/namespace"; 2 | 3 | import {PART} from "../parts"; 4 | 5 | 6 | /** The base class for automated slide creators */ 7 | export class BaseTome { 8 | /** construct a new tome, set up for a slide */ 9 | constructor(sorter){ 10 | this.sorter = sorter; 11 | } 12 | 13 | relevant(){ 14 | return true; 15 | } 16 | 17 | study(){ 18 | return []; 19 | } 20 | 21 | execute(){ 22 | this.study().map((d)=> this.sorter.slides.set([d.id], d) ); 23 | } 24 | 25 | cells() { 26 | return Jupyter.notebook.get_cells(); 27 | } 28 | 29 | wholeCellSlide(cell, prev=null){ 30 | let slideId = this.sorter.nextId(), 31 | regionId = this.sorter.nextId(), 32 | cellId = this.sorter.cellId(cell), 33 | regions = {}; 34 | 35 | regions[regionId] = { 36 | id: regionId, 37 | content: { 38 | cell: cellId, 39 | part: PART.whole 40 | }, 41 | attrs: { 42 | x: 0.1, 43 | y: 0.1, 44 | width: 0.8, 45 | height: 0.8 46 | } 47 | }; 48 | 49 | return { 50 | regions: regions, 51 | id: slideId, 52 | prev: prev 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/es6/tome/basic.es6: -------------------------------------------------------------------------------- 1 | import {BaseTome} from "./base"; 2 | 3 | 4 | /** import slideshow/RISE presentations */ 5 | export class BasicTome extends BaseTome { 6 | icon(){ 7 | return "columns fa-rotate-270"; 8 | } 9 | 10 | title(){ 11 | return "Basic"; 12 | } 13 | 14 | label(){ 15 | return `${this.study().length} Slides`; 16 | } 17 | 18 | study(){ 19 | let slides = [], 20 | prev; 21 | 22 | this.cells().map((cell) => { 23 | prev = slides.slice(-1)[0]; 24 | slides.push(this.wholeCellSlide(cell, prev ? prev.id : null)); 25 | }); 26 | 27 | return slides; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/es6/toolbar.es6: -------------------------------------------------------------------------------- 1 | import {d3, $, _} from "nbpresent-deps"; 2 | 3 | let fn = d3.functor; 4 | 5 | export class Toolbar { 6 | constructor(){ 7 | this._btnClass = fn("btn-default"); 8 | this._btnGroupClass = fn("btn-group"); 9 | this._tipOptions = fn({container: "body"}); 10 | 11 | this.update = this.update.bind(this); 12 | } 13 | 14 | btnClass(val){ 15 | return arguments.length ? 16 | [this._btnClass = fn(val), this][1] : 17 | this._btnClass; 18 | } 19 | 20 | tipOptions(val){ 21 | return arguments.length ? 22 | [this._tipOptions = fn(val), this][1] : 23 | this._tipOptions; 24 | } 25 | 26 | btnGroupClass(val){ 27 | return arguments.length ? 28 | [this._btnGroupClass = fn(val), this][1] : 29 | this._btnGroupClass; 30 | } 31 | 32 | update($selection){ 33 | let that = this; 34 | $selection.classed({"btn-toolbar nbp-toolbar": 1}); 35 | 36 | let $group = $selection.selectAll(".btn-toolbar-group") 37 | .data((d) => d) 38 | 39 | $group.enter() 40 | .append("div") 41 | .classed({"btn-toolbar-group": 1}) 42 | .classed(this._btnGroupClass(), 1); 43 | 44 | let $btn = $group.selectAll(".btn") 45 | .data((d) => d); 46 | 47 | $btn.enter() 48 | .append("a") 49 | .classed({btn: 1}) 50 | .call(($btn)=>{ 51 | $btn.append("i"); 52 | $btn.append("label"); 53 | }); 54 | 55 | $btn.attr({ 56 | "class": (d) => `btn ${this._btnClass(d)}`, 57 | title: (d) => d.tip || d.label 58 | }) 59 | .each(function(d){ 60 | if(!$.fn.tooltip){ 61 | return; 62 | } 63 | try { 64 | $(this).tooltip("destroy"); 65 | } catch(err) { 66 | // whatever, jquery 67 | } 68 | d.tip && $(this).tooltip(that._tipOptions(d)); 69 | }) 70 | .on("click", (d) => d.click()); 71 | 72 | // regular icons 73 | $btn.select("i") 74 | .filter(({icon}) => _.isString(icon)) 75 | .attr({ 76 | "class": ({icon}) => `fa fa-fw fa-2x fa-${icon}` 77 | }) 78 | .selectAll("i").remove(); 79 | 80 | // handle stacked icons 81 | let stacked = $btn.select("i") 82 | .filter(({icon}) => !_.isString(icon)) 83 | .attr({"class": "fa-stack"}) 84 | .selectAll("i") 85 | .data((d) => d.icon.map(() => d)); 86 | 87 | stacked.exit().remove(); 88 | 89 | stacked.enter().append("i"); 90 | 91 | stacked.attr({"class": ({icon}, i) => `fa fa-${icon[i]}`}); 92 | 93 | $btn.select("label") 94 | .text(({label}) => label); 95 | 96 | $btn.filter((d) => d.visible && !d.visible()) 97 | .remove(); 98 | 99 | $btn.exit() 100 | .remove(); 101 | 102 | $group.exit().remove(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/es6/tree.es6: -------------------------------------------------------------------------------- 1 | import {Baobab, d3, $} from "nbpresent-deps"; 2 | 3 | export class Tree { 4 | constructor(obj){ 5 | this.tree = new Baobab($.extend({ 6 | slides: {}, 7 | sortedSlides: Baobab.monkey(["slides"], this.sortedSlides), 8 | themes: {} 9 | }, obj)); 10 | } 11 | 12 | sortedSlides(slidesMap){ 13 | let slides = d3.entries(slidesMap); 14 | 15 | slides.sort( 16 | (a, b) => (a.value.prev === null) || (a.key === b.value.prev) ? -1 : 1 17 | ) 18 | 19 | return slides; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/es6/vendor.es6: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | import _bind_polyfill from "phantomjs-polyfill"; 3 | import d3 from "d3"; 4 | import uuid from "uuid"; 5 | import Baobab from "baobab"; 6 | import $ from "jquery"; 7 | import _ from "underscore"; 8 | import WebFont from "webfontloader"; 9 | import html2canvas from "html2canvas/dist/html2canvas.min"; 10 | 11 | export { 12 | _, 13 | $, 14 | Baobab, 15 | d3, 16 | html2canvas, 17 | uuid, 18 | WebFont 19 | }; 20 | -------------------------------------------------------------------------------- /src/js/build.js: -------------------------------------------------------------------------------- 1 | ({ 2 | baseUrl: ".", 3 | paths: { 4 | "nbpresent-deps": "../../nbpresent/static/nbpresent/js/nbpresent.deps.min", 5 | "nbpresent-standalone": "../../nbpresent/static/nbpresent/js/nbpresent.standalone.min", 6 | "jquery" : "../../node_modules/jquery/dist/jquery", 7 | "underscore" : "../../node_modules/underscore/underscore" 8 | }, 9 | name: "main", 10 | out: "../../nbpresent/static/nbpresent/js/nbpresent.static.min.js", 11 | preserveLicenseComments: true 12 | }) 13 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | /* global requirejs define */ 2 | define( 3 | ["require", "jquery", "base/js/namespace"], 4 | function(require, $, Jupyter){ 5 | // here's your namespace global 6 | var nbpresent = window.nbpresent = {loading: true}; 7 | 8 | var initializedResolver, 9 | initializedPromise = new Promise(function(resolve, reject){ 10 | initializedResolver = resolve; 11 | }); 12 | 13 | var $dlMenu = $("#download_html").parent(); 14 | 15 | var formats = [ 16 | {key: "nbpresent", label: "Presentation (.html)"}, 17 | {key: "nbpresent_pdf", label: "Presentation (.pdf)"} 18 | ]; 19 | 20 | function load_ipython_extension() { 21 | initNbpresent(); 22 | } 23 | 24 | function initialized(){ 25 | return initializedPromise; 26 | } 27 | 28 | function initNbpresent(){ 29 | requirejs.config({ 30 | paths: { 31 | "nbpresent-deps": require.toUrl("./nbpresent.deps.min.js"), 32 | "nbpresent-notebook": require.toUrl("./nbpresent.notebook.min.js") 33 | } 34 | }); 35 | 36 | var modulePath = requirejs.toUrl("nbpresent-notebook").split("?")[0] 37 | .split("/") 38 | .slice(0, -2) 39 | .join("/"); 40 | 41 | initStylesheet(modulePath); 42 | 43 | 44 | requirejs(["nbpresent-deps"], function(){ 45 | Jupyter.page.show_site(); 46 | requirejs(["nbpresent-notebook"], function(mode){ 47 | setTimeout(function(){ 48 | nbpresent.mode = new mode.NotebookMode( 49 | require.toUrl(".").split("?")[0] 50 | ); 51 | 52 | initializedResolver(nbpresent.mode); 53 | 54 | initToolbar(); 55 | initMenu(); 56 | }, 1000); 57 | }); 58 | }); 59 | } 60 | 61 | function show(){ 62 | nbpresent.mode.show(); 63 | } 64 | 65 | function present(){ 66 | nbpresent.mode.present(); 67 | } 68 | 69 | function nbconvert(key){ 70 | Jupyter.menubar._nbconvert(key, true); 71 | } 72 | 73 | function initStylesheet(modulePath){ 74 | var id = "nbp-css", 75 | $head = $("head"), 76 | cssPath = modulePath + "/css/nbpresent.min.css"; 77 | 78 | $head.find("link#" + id).length || $("", { 79 | id: id, 80 | rel: "stylesheet", 81 | href: cssPath, 82 | }).appendTo($head); 83 | } 84 | 85 | // set up the UI before doing anything else to avoid UI delay 86 | function initToolbar(){ 87 | $("#view_menu").append( 88 | $("
  • ").append( 89 | $("").text("Toggle Presentation").on("click", show))); 90 | 91 | // TODO: make this one button! 92 | Jupyter.toolbar.add_buttons_group([ 93 | { 94 | label: "Edit Presentation", 95 | icon: "fa-gift", 96 | callback: show, 97 | id: "nbp-app-btn" 98 | }, 99 | { 100 | label: "Show Presentation", 101 | icon: "fa-youtube-play", 102 | callback: present, 103 | id: "nbp-present-btn" 104 | } 105 | ]); 106 | } 107 | 108 | function initMenu(){ 109 | $.get(Jupyter.notebook.base_url + "api/nbconvert", formatsLoaded); 110 | } 111 | 112 | function dlMenuItem(format){ 113 | $("
  • ") 114 | .append( 115 | $("", {"class": "download_" + format.key}) 116 | .text(format.label) 117 | .on("click", function(){ nbconvert(format.key); }) 118 | ) 119 | .appendTo($dlMenu) 120 | } 121 | 122 | function formatsLoaded(available){ 123 | // update the download chrome 124 | formats.map(function(format){ 125 | available[format.key] && dlMenuItem(format); 126 | }); 127 | } 128 | 129 | return { 130 | load_ipython_extension: load_ipython_extension, 131 | initialized: initialized 132 | }; 133 | }); 134 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | require( 2 | 3 | ["jquery", "nbpresent-deps", "nbpresent-standalone"], 4 | 5 | function(jquery, deps, mode){ 6 | $(function(){ 7 | window.nbpresent = new mode.StandaloneMode("./"); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/less/app.less: -------------------------------------------------------------------------------- 1 | .nbp-app { 2 | .nbp-ui(); 3 | 4 | z-index: @z-app-bar; 5 | position: fixed; 6 | top: 0; 7 | bottom: 0; 8 | right: 0; 9 | width: @app-bar-width; 10 | 11 | background-color: @ui-bg; 12 | 13 | margin-right: -@app-bar-width; 14 | transition: margin-right @tx-dur @tx-fn; 15 | 16 | .btn-toolbar { 17 | margin: 0; 18 | right: 0; 19 | } 20 | 21 | .nbp-deck-toolbar { 22 | position: absolute; 23 | bottom: 0; 24 | } 25 | } 26 | 27 | body.nbp-no-slides { 28 | .nbp-app-bar { 29 | .btn[title="Present"] { 30 | opacity: 0.33; 31 | &:hover { 32 | opacity: 1.0; 33 | } 34 | } 35 | } 36 | } 37 | 38 | body:not(.nbp-app-enabled) { 39 | #site { 40 | transition: width @tx-dur @tx-fn; 41 | } 42 | 43 | #maintoolbar{ 44 | padding-right: 0; 45 | transition: padding-right @tx-dur @tx-fn; 46 | } 47 | } 48 | 49 | 50 | body.nbp-app-enabled { 51 | #site { 52 | margin-right: @app-bar-width; 53 | width: auto; 54 | transition: margin-right @tx-dur @tx-fn; 55 | } 56 | 57 | #maintoolbar{ 58 | padding-right: @app-bar-width; 59 | transition: padding-right @tx-dur @tx-fn; 60 | } 61 | 62 | #nbp-app-btn{ 63 | color: @jpy-brand; 64 | box-shadow: @z-shadow; 65 | background-color: @ui-bg; 66 | transition: margin-right @tx-dur @tx-fn, box-shadow @tx-dur @tx-fn; 67 | } 68 | 69 | .nbp-app { 70 | opacity: 1; 71 | margin-right: 0; 72 | transition: margin-right @tx-dur @tx-fn, opacity @tx-dur @tx-fn; 73 | 74 | box-shadow: @z-shadow; 75 | } 76 | 77 | .tour-nbpresent { 78 | z-index: @z-tour; 79 | } 80 | 81 | &.nbp-presenting{ 82 | .nbp-app { 83 | opacity: 0; 84 | transition: opacity @tx-dur @tx-fn; 85 | } 86 | &.nbp-sorting, &.nbp-theming { 87 | .nbp-app { 88 | opacity: 0.5; 89 | transition: opacity @tx-dur @tx-fn; 90 | } 91 | } 92 | .nbp-app:hover { 93 | opacity: 1.0; 94 | transition: opacity @tx-dur @tx-fn; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/less/editor.less: -------------------------------------------------------------------------------- 1 | .nbp-editor { 2 | .nbp-ui(); 3 | position: fixed; 4 | bottom: @sorter-height; 5 | right: @app-bar-width; 6 | left: @editor-sidebar-width; 7 | top: 0; 8 | background-color: rgba(50, 50, 50, 0.5); 9 | z-index: @z-editor; 10 | opacity: 0.9; 11 | 12 | .slide_bg { 13 | background-color: #fff; 14 | position: absolute; 15 | } 16 | 17 | .nbp-region { 18 | .nbp-region-bg { 19 | fill: transparent; 20 | opacity: 0.50; 21 | stroke: grey; 22 | stroke-width: 2px; 23 | stroke-dasharray: 5,5; 24 | } 25 | 26 | &.nbp-content-source .nbp-region-bg{ 27 | stroke: @part-source-color; 28 | stroke-dasharray: none; 29 | } 30 | 31 | &.nbp-content-outputs .nbp-region-bg{ 32 | stroke: @part-outputs-color; 33 | stroke-dasharray: none; 34 | } 35 | 36 | &.nbp-content-widgets .nbp-region-bg{ 37 | stroke: @part-widgets-color; 38 | stroke-dasharray: none; 39 | } 40 | 41 | &.nbp-content-whole .nbp-region-bg{ 42 | stroke: @part-whole-color; 43 | stroke-dasharray: none; 44 | } 45 | 46 | 47 | &.active .nbp-region-bg{ 48 | stroke: @jpy-brand; 49 | opacity: 0.9; 50 | stroke-width: 10px; 51 | stroke-dasharray: none; 52 | } 53 | 54 | } 55 | 56 | } 57 | 58 | // TODO: move this? 59 | .nbp-regiontree { 60 | .nbp-ui(); 61 | 62 | z-index: @z-regiontree; 63 | position: absolute; 64 | width: @editor-sidebar-width; 65 | bottom: @sorter-height; 66 | left: 0; 67 | top: 0; 68 | background-color: @ui-bg; 69 | overflow-y: auto; 70 | overflow-x: hidden; 71 | padding-top: @btn-titled-height; 72 | opacity: 0.9; 73 | 74 | .nbp-toolbar { 75 | position: fixed; 76 | top: 0; 77 | z-index: @z-regiontree + 1; 78 | width: @editor-sidebar-width - 10; 79 | background-color: @ui-bg; 80 | box-shadow: @z-shadow; 81 | } 82 | 83 | .region_info { 84 | clear: both; 85 | float: left; 86 | margin: 10px 0; 87 | 88 | .slide { 89 | float: left; 90 | } 91 | 92 | .region_attr { 93 | float: right; 94 | clear: right; 95 | width: 140px; 96 | text-align: right; 97 | background-color: transparent; 98 | border: none; 99 | color: @jpy-brand; 100 | height: 24px; 101 | padding: 2px; 102 | 103 | &.input-group-sm > .input-group-addon.attr_ns { 104 | background-color: transparent; 105 | border: none; 106 | color: @jpy-brand; 107 | padding: 0; 108 | height: 20px; 109 | } 110 | 111 | .attr_name{ 112 | padding: 0 5px; 113 | height: 20px; 114 | background-color: @jpy-brand; 115 | color: black; 116 | border-radius: 2px; 117 | user-select: none; 118 | cursor: ew-resize; 119 | border: none; 120 | } 121 | 122 | input { 123 | height: 20px; 124 | text-align: right; 125 | background-color: transparent; 126 | border: none; 127 | color: white; 128 | font-weight: bold; 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/less/help.less: -------------------------------------------------------------------------------- 1 | body.nbp-helping { 2 | .nbp-helper{ 3 | margin-right: 0; 4 | transition: margin-right @tx-dur @tx-fn; 5 | } 6 | } 7 | 8 | .nbp-helper { 9 | .nbp-ui(); 10 | 11 | z-index: @z-help; 12 | 13 | position: fixed; 14 | 15 | right: @app-bar-width; 16 | width: @sorter-slide-width; 17 | top: 0; 18 | bottom: 0; 19 | 20 | margin-right: -@sorter-slide-width; 21 | transition: margin-right @tx-dur @tx-fn; 22 | 23 | background-color: @ui-bg; 24 | 25 | hr { 26 | margin: 0 20px; 27 | padding: 0; 28 | border-top: solid 1px fade(@jpy-brand, 50%); 29 | } 30 | 31 | h1, h2, h3 { 32 | color: @jpy-brand; 33 | text-align: center; 34 | } 35 | 36 | .nbp-version { 37 | font-size: 12px; 38 | } 39 | 40 | h2{ 41 | font-size: 18px; 42 | } 43 | 44 | a:not(.btn) { 45 | color: @jpy-brand; 46 | text-decoration: none; 47 | border-bottom: solid 3px fade(@jpy-brand, 20%); 48 | transition: border-bottom @tx-dur @tx-fn; 49 | &:hover { 50 | border-bottom: solid 1px fade(@jpy-brand, 75%); 51 | transition: border-bottom @tx-dur @tx-fn; 52 | } 53 | } 54 | 55 | .nbp-community, .nbp-tour-launcher { 56 | width: 50%; 57 | } 58 | 59 | .nbp-legal { 60 | bottom: 0; 61 | position: absolute; 62 | padding: 10px; 63 | color: @jpy-brand; 64 | font-size: 11px; 65 | text-align: center; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/less/index.less: -------------------------------------------------------------------------------- 1 | @import "mixins"; 2 | @import "app"; 3 | @import "help"; 4 | @import "toolbar"; 5 | @import "editor"; 6 | @import "sorter"; 7 | @import "presenter"; 8 | @import "speaker"; 9 | @import "mini"; 10 | @import "layouts"; 11 | @import "tour"; 12 | @import "link-overlay"; 13 | @import "theme-ui"; 14 | @import "theme-card"; 15 | @import "variables"; 16 | -------------------------------------------------------------------------------- /src/less/layouts.less: -------------------------------------------------------------------------------- 1 | .nbp-template-library { 2 | .nbp-ui(); 3 | 4 | z-index: @z-layouts; 5 | position: fixed; 6 | bottom: @sorter-height; 7 | left: 0; 8 | right: @app-bar-width; 9 | height: @sorter-height; 10 | 11 | background-color: @ui-bg; 12 | opacity: 0.75; 13 | 14 | .npb-template-library-slides { 15 | overflow-x: auto; 16 | overflow-y: hidden; 17 | position: relative; 18 | width: 100%; 19 | height: 170px; 20 | margin-top: 42px; 21 | } 22 | 23 | 24 | .hide_library { 25 | position: absolute; 26 | left: 50%; 27 | bottom: 0; 28 | background-color: @ui-bg; 29 | color: @jpy-brand; 30 | } 31 | 32 | .slide { 33 | opacity: 80%; 34 | position: absolute; 35 | } 36 | 37 | h3 { 38 | color: @jpy-brand; 39 | position: absolute; 40 | margin: 0; 41 | padding: 10px; 42 | &.from_template{ 43 | top: 5px; 44 | } 45 | &.from_slide{ 46 | bottom: 0; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/less/link-overlay.less: -------------------------------------------------------------------------------- 1 | body.nbp-linking { 2 | .nbp-link-overlay .nbp-part-overlay{ 3 | opacity: 1; 4 | transition: opacity @tx-dur @tx-fn; 5 | 6 | &:hover{ 7 | opacity: 0.5; 8 | transition: opacity @tx-dur @tx-fn, width @tx-dur @tx-fn; 9 | width: 100%; 10 | } 11 | } 12 | 13 | .cell { 14 | opacity: 0.5; 15 | } 16 | } 17 | 18 | .nbp-link-overlay { 19 | .nbp-ui(); 20 | 21 | z-index: @z-link-overlay; 22 | top: 0; 23 | right: 0; 24 | bottom: 0; 25 | left: 0; 26 | position: absolute; 27 | 28 | .nbp-part-overlay { 29 | position: absolute; 30 | width: @sorter-slide-height; 31 | 32 | background-color: @ui-bg; 33 | color: white; 34 | text-align: left; 35 | 36 | opacity: 0; 37 | transition: opacity @tx-dur @tx-fn, width @tx-dur @tx-fn; 38 | 39 | &.nbp-part-overlay-source { 40 | border-right: solid 3px @part-source-color; 41 | } 42 | 43 | &.nbp-part-overlay-outputs { 44 | border-right: solid 3px @part-outputs-color; 45 | } 46 | 47 | &.nbp-part-overlay-widgets { 48 | border-right: solid 3px @part-widgets-color; 49 | } 50 | 51 | &.nbp-part-overlay-whole { 52 | border-left: solid 3px @part-whole-color; 53 | right: 0; 54 | text-align: right; 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/less/mini.less: -------------------------------------------------------------------------------- 1 | .slide.nbp-mini { 2 | position: relative; 3 | width: @sorter-slide-width; 4 | height: @sorter-slide-height; 5 | border: solid 1px #eee; 6 | border-radius: 2px; 7 | background-color: #fff; 8 | overflow: hidden; 9 | box-shadow: @z-shadow; 10 | 11 | &.active { 12 | border: solid 3px @jpy-brand; 13 | } 14 | 15 | &.dragging { 16 | box-shadow: @z-shadow; 17 | z-index: 9999; 18 | opacity: 0.9; 19 | } 20 | 21 | .nbp-region { 22 | position: absolute; 23 | 24 | background-color: rgba(255,255,255,0.5); 25 | background-repeat: no-repeat; 26 | border: dashed 3px rgba(128,128,128,0.5); 27 | border-radius: 2px; 28 | 29 | &.nbp-content-source, 30 | &.nbp-content-outputs, 31 | &.nbp-content-widgets, 32 | &.nbp-content-whole { 33 | border: solid 1px black; 34 | } 35 | 36 | &.nbp-content-source { 37 | background-color: fadeOut(@part-source-color, 75%); 38 | } 39 | 40 | &.nbp-content-outputs { 41 | background-color: fadeOut(@part-outputs-color, 75%); 42 | } 43 | 44 | &.nbp-content-widgets { 45 | background-color: fadeOut(@part-widgets-color, 75%); 46 | } 47 | 48 | &.nbp-content-whole { 49 | background-color: fadeOut(@part-whole-color, 75%); 50 | } 51 | 52 | &.active { 53 | border: solid 3px @jpy-brand; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/less/mixins.less: -------------------------------------------------------------------------------- 1 | .nbp-ui { 2 | font-family: Lato, sans-serif; 3 | 4 | .btn { 5 | color: @jpy-brand; 6 | background-color: transparent; 7 | border: none; 8 | font-size: 16px; 9 | opacity: 0.75; 10 | 11 | &:hover { 12 | opacity: 1; 13 | } 14 | 15 | &:active:focus, &:active:hover{ 16 | background-color: @jpy-brand; 17 | color: @ui-bg; 18 | } 19 | 20 | label { 21 | font-size: 12px; 22 | text-align: center; 23 | display: block; 24 | break: both; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/less/presenter.less: -------------------------------------------------------------------------------- 1 | body:not(.nbp-presenting) .nbp-presenter{ 2 | display: none; 3 | } 4 | 5 | body.nbp-presenting { 6 | overflow: hidden; 7 | 8 | #notebook-container { 9 | position:absolute; 10 | left: -9999px 11 | } 12 | 13 | .nbp-presenter { 14 | .nbp-ui(); 15 | z-index: @z-present; 16 | background-color: white; 17 | position: fixed; 18 | top: 0; 19 | left: 0; 20 | bottom: 0; 21 | right: 0; 22 | 23 | .nbp-presenter-background { 24 | position: fixed; 25 | max-width: 100vw; 26 | background-repeat: no-repeat; 27 | } 28 | } 29 | 30 | #notebook .cell .nbp-present .text_cell_render { 31 | border: none; 32 | margin: 0; 33 | padding: 0; 34 | } 35 | 36 | 37 | .cell.nbp-present.selected{ 38 | border: none; 39 | background: none; 40 | } 41 | 42 | .cell.nbp-present, 43 | .cell .nbp-present{ 44 | z-index: @z-presented; 45 | position: fixed; 46 | display: block; 47 | 48 | .prompt, .ctb_hideshow { 49 | display: none; 50 | } 51 | 52 | .input_area, &.input_area { 53 | height: 100%; 54 | 55 | .CodeMirror { 56 | max-height: 100%; 57 | height: 100%; 58 | } 59 | } 60 | 61 | .output { 62 | height: 100%; 63 | .output_area { 64 | height: 100%; 65 | .output_subarea { 66 | height: 100%; 67 | max-width: 100%; 68 | 69 | &.rendered_html.output_result{ 70 | margin: 0; 71 | padding: 0; 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | 79 | .cell div.nbp-unpresent{ 80 | display: none; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/less/sorter.less: -------------------------------------------------------------------------------- 1 | body.nbp-sorting { 2 | #notebook_panel { 3 | margin-bottom: @sorter-height; 4 | } 5 | 6 | .nbp-sorter-drawer { 7 | padding-top: 0; 8 | transition: padding-top @tx-dur @tx-fn; 9 | } 10 | } 11 | 12 | body.nbp-presenting{ 13 | .nbp-sorter-drawer { 14 | opacity: 0.75; 15 | } 16 | } 17 | 18 | body.nbp-presenting.nbp-sorting{ 19 | .nbp-present { 20 | box-shadow: @z-shadow; 21 | transition: box-shadow @tx-dur @tx-fn; 22 | } 23 | } 24 | 25 | body.nbp-presenting:not(.nbp-sorting){ 26 | .nbp-present { 27 | box-shadow: none; 28 | transition: box-shadow @tx-dur @tx-fn; 29 | } 30 | } 31 | 32 | .nbp-sorter-drawer { 33 | position: fixed; 34 | left: 0; 35 | bottom: 0; 36 | right: @app-bar-width; 37 | height: @sorter-height; 38 | 39 | z-index: @z-sorter; 40 | padding-top: @sorter-height; 41 | transition: padding-top @tx-dur @tx-fn; 42 | 43 | .nbp-sorter{ 44 | .nbp-ui(); 45 | 46 | position: absolute; 47 | width: 100%; 48 | height: @sorter-height; 49 | 50 | color: @jpy-brand; 51 | box-shadow: @z-shadow; 52 | background-color: @ui-bg; 53 | opacity: 0.9; 54 | 55 | h2 { 56 | font-size: 18px; 57 | margin: 0; 58 | padding: 10px; 59 | } 60 | 61 | .nbp-sorter-label { 62 | position: absolute; 63 | left: 0; 64 | bottom: 0; 65 | } 66 | 67 | .nbp-sorter-empty { 68 | display: none; 69 | 70 | a, .btn { 71 | color: @jpy-brand; 72 | background-color: transparent; 73 | } 74 | 75 | .btn { 76 | border: solid 1px @jpy-brand; 77 | } 78 | } 79 | 80 | &.empty .nbp-sorter-empty { 81 | display: block; 82 | position: absolute; 83 | top: 0; 84 | text-align: center; 85 | padding: 10px; 86 | 87 | .nbp-empty-intro { 88 | width: @sorter-slide-width * 1.6; 89 | padding: 0px 30px 0 0; 90 | } 91 | 92 | i { 93 | vertical-align: middle; 94 | } 95 | 96 | .nbp-tomes { 97 | position: absolute; 98 | left: @sorter-slide-width * 1.6; 99 | top: 0; 100 | right: 0; 101 | display: flex; 102 | 103 | .nbp-tome { 104 | flex: 1; 105 | text-align: center; 106 | padding: 5px; 107 | } 108 | } 109 | } 110 | 111 | .nbp-region-toolbar{ 112 | position: absolute; 113 | display: none; 114 | top: 0; 115 | left: 50%; 116 | 117 | opacity: 0; 118 | 119 | text-align: center; 120 | } 121 | 122 | .nbp-slides-wrap { 123 | overflow-x: auto; 124 | overflow-y: hidden; 125 | position: relative; 126 | width: 100%; 127 | padding-top: @btn-titled-height; 128 | height: @sorter-height; 129 | 130 | .empty { 131 | position: absolute; 132 | } 133 | 134 | .slide.nbp-mini { 135 | position: absolute; 136 | .nbp-region { 137 | background-size: cover; 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | .nbp-slide-theme-picker{ 145 | .nbp-ui(); 146 | 147 | background-color: @ui-bg; 148 | position: fixed; 149 | bottom: @sorter-height; 150 | right: @app-bar-width; 151 | z-index: @z-sorter; 152 | top: 0; 153 | overflow-y: auto; 154 | padding-left: 5px; 155 | } 156 | -------------------------------------------------------------------------------- /src/less/speaker.less: -------------------------------------------------------------------------------- 1 | body.nbp-presenting { 2 | .nbpresent_speaker{ 3 | .nbp-ui(); 4 | 5 | z-index: @z-speaker; 6 | display: block; 7 | position: fixed; 8 | bottom: 10px; 9 | right: @app-bar-width; 10 | 11 | .nbp-presenter-toolbar { 12 | transition: all 0.2s; 13 | float: right; 14 | 15 | &:hover, &.fake_hover{ 16 | opacity: 1; 17 | transition: all 0.2s; 18 | } 19 | 20 | a { 21 | color: @jpy-brand; 22 | } 23 | } 24 | } 25 | } 26 | 27 | body:not(.nbp-presenting) .nbpresent_speaker{ 28 | display: none; 29 | } 30 | -------------------------------------------------------------------------------- /src/less/theme-card.less: -------------------------------------------------------------------------------- 1 | .nbp-theme-card { 2 | .nbp-ui(); 3 | 4 | width: @theme-card-height; 5 | height: @theme-card-height; 6 | background-color: white; 7 | box-shadow: z-shadow; 8 | border-radius: 2px; 9 | border: solid 1px @ui-bg; 10 | margin: 5px 5px 0 0; 11 | position: relative; 12 | 13 | .nbp-theme-card-backgrounds { 14 | position:absolute; 15 | top: @theme-card-swatch-size; 16 | right: 0; 17 | bottom: 0; 18 | 19 | .nbp-theme-card-background{ 20 | width: @theme-card-swatch-size; 21 | background-size: cover; 22 | background-position: center; 23 | } 24 | } 25 | 26 | .nbp-theme-card-fonts { 27 | position: absolute; 28 | top: @theme-card-swatch-size + 2; 29 | bottom: 0; 30 | left: 4px; 31 | right: @theme-card-swatch-size; 32 | overflow-y: auto; 33 | padding-bottom: 4px; 34 | } 35 | 36 | .nbp-theme-card-palette { 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | right: 0; 41 | overflow-x: auto; 42 | .nbp-theme-card-color { 43 | width: @theme-card-swatch-size; 44 | height: @theme-card-swatch-size; 45 | float: left; 46 | } 47 | } 48 | 49 | .nbp-theme-card-font { 50 | font-size: 12px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/less/theme-ui.less: -------------------------------------------------------------------------------- 1 | body:not(.nbp-theming) { 2 | .nbp-theme-manager { 3 | margin-top: -@theme-mgr-height; 4 | transition: margin-top @tx-dur @tx-fn; 5 | } 6 | } 7 | 8 | .nbp-theme-manager { 9 | .nbp-ui(); 10 | 11 | position: fixed; 12 | height: @theme-mgr-height; 13 | z-index: @z-theme-mgr; 14 | right: @app-bar-width; 15 | left: 0; 16 | bottom: 0; 17 | top: 0; 18 | background-color: @ui-bg-alpha; 19 | 20 | margin-top: 0; 21 | transition: margin-top @tx-dur @tx-fn; 22 | 23 | .nbp-toolbar{ 24 | float: left; 25 | } 26 | 27 | h2 { 28 | margin: 0; 29 | padding: 0; 30 | color: @jpy-brand; 31 | text-align: center; 32 | font-size: 18px; 33 | } 34 | 35 | .nbp-theme-previews { 36 | position: absolute; 37 | left: @app-bar-width; 38 | height: @theme-mgr-height; 39 | right: 250px; 40 | overflow-y: auto; 41 | overflow-x: hidden; 42 | 43 | .nbp-theme-preview { 44 | float: left; 45 | position: relative; 46 | 47 | .nbp-default-theme { 48 | position: absolute; 49 | bottom: 4px; 50 | right: 4px; 51 | } 52 | 53 | &.nbp-theme-preview-current .nbp-theme-preview-card{ 54 | border: solid 3px @jpy-brand; 55 | } 56 | } 57 | } 58 | 59 | .nbp-theme-previews-canned{ 60 | position: absolute; 61 | right: 0; 62 | height: @theme-mgr-height; 63 | width: 220px; 64 | overflow-y: auto; 65 | overflow-x: hidden; 66 | 67 | .nbp-theme-preview-canned { 68 | float: right; 69 | opacity: 0.5; 70 | &:hover { 71 | opacity: 1.0; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/less/toolbar.less: -------------------------------------------------------------------------------- 1 | .nbp-toolbar { 2 | .btn-group { 3 | box-shadow: @z-shadow; 4 | 5 | .btn { 6 | &:last-child{ 7 | border-right: none; 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/less/tour.less: -------------------------------------------------------------------------------- 1 | .tour-nbpresent { 2 | .nbp-ui(); 3 | 4 | font-size: 18px; 5 | padding: 0px; 6 | box-shadow: @z-shadow; 7 | max-width: 200px; 8 | min-width: 400px; 9 | width: 25vw; 10 | 11 | .popover-title { 12 | font-size: @tour-padding; 13 | padding: @tour-padding; 14 | } 15 | 16 | .popover-content { 17 | padding: @tour-padding; 18 | } 19 | 20 | .popover-navigation{ 21 | background-color: @ui-bg; 22 | } 23 | 24 | .popover-navigation .btn { 25 | color: @jpy-brand; 26 | font-size: 14px; 27 | background-color: @ui-bg; 28 | border: none; 29 | 30 | &:hover{ 31 | color: @ui-bg; 32 | background-color: @jpy-brand; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/less/variables.less: -------------------------------------------------------------------------------- 1 | // z-stack 2 | @z-present: 999; 3 | @z-presented: 1000; 4 | 5 | @z-speaker: 9001; 6 | @z-help: 9001; 7 | 8 | @z-app-bar: 9002; 9 | @z-editor: 9002; 10 | @z-regiontree: 9002; 11 | @z-sorter: 9002; 12 | @z-layouts: 9002; 13 | @z-link-overlay: 9002; 14 | 15 | @z-theme-mgr: 9003; 16 | @z-theme-editor: 9002; 17 | 18 | @z-tour: 9999; 19 | 20 | // colors 21 | @ui-bg: #222; 22 | @ui-bg-alpha: rgba(50, 50, 50, 0.95); 23 | @jpy-brand: #f37626; 24 | @active: #337ab7; 25 | @part-source-color: #303F9F; 26 | @part-outputs-color: #D84315; 27 | @part-widgets-color: average(@part-source-color, @part-outputs-color); 28 | @part-whole-color: @jpy-brand; 29 | 30 | 31 | // misc 32 | @btn-titled-height: 64px; 33 | 34 | // app 35 | @app-bar-width: 78px; 36 | 37 | // theme 38 | @theme-card-height: @sorter-slide-height; 39 | @theme-mgr-height: @theme-card-height + 48; 40 | @theme-swatch-size: 24px; 41 | @theme-card-swatch-size: 15px; 42 | 43 | // sorter 44 | @sorter-slide-width: 160px; 45 | @sorter-slide-height: 90px; 46 | @sorter-height: @sorter-slide-height + 100; 47 | 48 | // editor 49 | @editor-sidebar-width: 320px; 50 | 51 | // pretties 52 | @z-shadow: 0px 0px 12px 1px rgba(87, 87, 87, 0.2); 53 | 54 | // tour 55 | @tour-padding: 24px; 56 | 57 | // transitions 58 | @tx-dur: 0.2s; 59 | @tx-fn: ease; 60 | --------------------------------------------------------------------------------