From e3f2afa46024468c25827ca4ee1bda285a0d49a7 Mon Sep 17 00:00:00 2001 From: Juergen Hasch Date: Fri, 4 Sep 2015 20:26:43 +0200 Subject: [PATCH] Reorg 2nd batch --- .gitignore | 18 +- bld.bat | 2 + build.sh | 1 + conda-recipe/build.sh | 1 - conda-recipe/install.py | 81 --- extensions/nbextensions.py | 118 ++--- extensions/{embed.py => post_htmlembed.py} | 99 ++-- .../{codefolding.py => pre_codefolding.py} | 0 ...{pymdpreprocessor.py => pre_pymarkdown.py} | 0 conda-recipe/meta.yaml => meta.yaml | 17 +- .../config/config_menu/config_menu.yaml | 6 + nbextensions/config/config_menu/icon.png | Bin 0 -> 33794 bytes nbextensions/config/config_menu/main.js | 29 ++ nbextensions/config/main.css | 170 +++++++ nbextensions/config/main.js | 475 ++++++++++++++++++ nbextensions/config/readme.md | 65 +++ nbextensions/config/render.js | 34 ++ .../publishing/printview/printview-button.png | Bin 18982 -> 0 bytes .../usability/aspell/ipy-aspell-server.py | 71 +++ .../chrome-clipboard/chrome-clipboard.yaml | 4 +- .../usability/chrome-clipboard/main.js | 233 +++++---- .../usability/chrome-clipboard/readme.md | 36 ++ .../usability/codefolding/codefolding.yaml | 4 +- .../codefolding_firstline_folded.png | Bin 6159 -> 0 bytes .../codefolding_firstline_unfolded.png | Bin 7418 -> 0 bytes .../codefolding_indent_folded_1.png | Bin 7837 -> 0 bytes .../codefolding_indent_folded_2.png | Bin 4135 -> 0 bytes .../codefolding_indent_unfolded.png | Bin 12170 -> 0 bytes nbextensions/usability/codefolding/main.js | 52 +- nbextensions/usability/codefolding/readme.md | 53 ++ nbextensions/usability/dragdrop/dragdrop.yaml | 4 +- nbextensions/usability/dragdrop/embed.py | 53 -- nbextensions/usability/dragdrop/main.js | 301 +++++------ nbextensions/usability/dragdrop/readme.md | 25 + .../usability/execute_time/ExecuteTime.js | 20 +- .../usability/execute_time/ExecuteTime.yaml | 4 +- .../execute_time/execution-timings-box.png | Bin 64712 -> 0 bytes .../execute_time/execution-timings-menu.png | Bin 71705 -> 0 bytes nbextensions/usability/execute_time/readme.md | 28 ++ nbextensions/usability/exercise/exercise.yaml | 7 + nbextensions/usability/exercise/icon.png | Bin 0 -> 15750 bytes nbextensions/usability/exercise/main.js | 95 ++-- .../usability/hide_input/hide_input.tpl | 20 + .../hide_input/hide_input_all_hide.png | Bin 29979 -> 0 bytes .../hide_input/hide_input_all_show.png | Bin 77728 -> 0 bytes .../hide_input/ipython_nbconvert_config.py | 7 + .../usability/limit_output/limit-output.yaml | 11 +- nbextensions/usability/limit_output/readme.md | 11 + .../usability/navigation-hotkeys/hotkeys.yaml | 4 +- .../usability/navigation-hotkeys/main.js | 430 ++++++++-------- .../usability/navigation-hotkeys/readme.md | 24 + .../usability/python-markdown/main.js | 4 - .../python-markdown/python-markdown-post.png | Bin 37725 -> 0 bytes .../python-markdown/python-markdown-pre.png | Bin 19639 -> 0 bytes .../python-markdown/python-markdown.yaml | 2 +- .../usability/python-markdown/readme.md | 48 ++ nbextensions/usability/rubberband/readme.md | 31 ++ .../usability/rubberband/rubberband.yaml | 2 +- nbextensions/usability/ruler/readme.md | 1 + nbextensions/usability/ruler/ruler.yaml | 9 +- nbextensions/usability/runtools/readme.md | 50 ++ nbextensions/usability/runtools/runtools.yaml | 2 +- .../usability/runtools/toggle_codecells.png | Bin 15839 -> 0 bytes .../usability/search-replace/readme.md | 8 + .../search-replace/search-replace.yaml | 4 +- templates/nbextensions.html | 18 +- templates/rendermd.html | 2 +- tests/test-postprocessors.py | 0 68 files changed, 1971 insertions(+), 823 deletions(-) create mode 100644 bld.bat create mode 100644 build.sh delete mode 100755 conda-recipe/build.sh delete mode 100755 conda-recipe/install.py rename extensions/{embed.py => post_htmlembed.py} (75%) mode change 100755 => 100644 rename extensions/{codefolding.py => pre_codefolding.py} (100%) rename extensions/{pymdpreprocessor.py => pre_pymarkdown.py} (100%) rename conda-recipe/meta.yaml => meta.yaml (78%) mode change 100755 => 100644 create mode 100644 nbextensions/config/config_menu/config_menu.yaml create mode 100644 nbextensions/config/config_menu/icon.png create mode 100644 nbextensions/config/config_menu/main.js create mode 100644 nbextensions/config/main.css create mode 100644 nbextensions/config/main.js create mode 100644 nbextensions/config/readme.md create mode 100644 nbextensions/config/render.js delete mode 100644 nbextensions/publishing/printview/printview-button.png create mode 100644 nbextensions/usability/aspell/ipy-aspell-server.py create mode 100644 nbextensions/usability/chrome-clipboard/readme.md delete mode 100644 nbextensions/usability/codefolding/codefolding_firstline_folded.png delete mode 100644 nbextensions/usability/codefolding/codefolding_firstline_unfolded.png delete mode 100644 nbextensions/usability/codefolding/codefolding_indent_folded_1.png delete mode 100644 nbextensions/usability/codefolding/codefolding_indent_folded_2.png delete mode 100644 nbextensions/usability/codefolding/codefolding_indent_unfolded.png create mode 100644 nbextensions/usability/codefolding/readme.md delete mode 100644 nbextensions/usability/dragdrop/embed.py create mode 100644 nbextensions/usability/dragdrop/readme.md delete mode 100644 nbextensions/usability/execute_time/execution-timings-box.png delete mode 100644 nbextensions/usability/execute_time/execution-timings-menu.png create mode 100644 nbextensions/usability/execute_time/readme.md create mode 100644 nbextensions/usability/exercise/exercise.yaml create mode 100644 nbextensions/usability/exercise/icon.png create mode 100644 nbextensions/usability/hide_input/hide_input.tpl delete mode 100644 nbextensions/usability/hide_input/hide_input_all_hide.png delete mode 100644 nbextensions/usability/hide_input/hide_input_all_show.png create mode 100644 nbextensions/usability/hide_input/ipython_nbconvert_config.py create mode 100644 nbextensions/usability/limit_output/readme.md create mode 100644 nbextensions/usability/navigation-hotkeys/readme.md delete mode 100644 nbextensions/usability/python-markdown/python-markdown-post.png delete mode 100644 nbextensions/usability/python-markdown/python-markdown-pre.png create mode 100644 nbextensions/usability/python-markdown/readme.md create mode 100644 nbextensions/usability/rubberband/readme.md create mode 100644 nbextensions/usability/ruler/readme.md create mode 100644 nbextensions/usability/runtools/readme.md delete mode 100644 nbextensions/usability/runtools/toggle_codecells.png create mode 100644 nbextensions/usability/search-replace/readme.md create mode 100644 tests/test-postprocessors.py diff --git a/.gitignore b/.gitignore index 1611f4e..276a34c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,16 @@ -custom.js -custom.css +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Sphinx documentation +docs/_build/ + +.idea diff --git a/bld.bat b/bld.bat new file mode 100644 index 0000000..c40a9bb --- /dev/null +++ b/bld.bat @@ -0,0 +1,2 @@ +"%PYTHON%" setup.py install +if errorlevel 1 exit 1 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..434a40f --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +python install.py \ No newline at end of file diff --git a/conda-recipe/build.sh b/conda-recipe/build.sh deleted file mode 100755 index 8a1fa0c..0000000 --- a/conda-recipe/build.sh +++ /dev/null @@ -1 +0,0 @@ -ipython install.py \ No newline at end of file diff --git a/conda-recipe/install.py b/conda-recipe/install.py deleted file mode 100755 index d26fe53..0000000 --- a/conda-recipe/install.py +++ /dev/null @@ -1,81 +0,0 @@ -# Install notebook extensions - -from jupyter_core.paths import jupyter_config_dir, jupyter_data_dir, jupyter_runtime_dir -from traitlets.config.loader import Config, JSONFileConfigLoader -import IPython.extensions -import os -import sys -import logging -import json - -# http://stackoverflow.com/questions/12683834/how-to-copy-directory-recursively-in-python-and-overwrite-all -def recursive_overwrite(src, dest, ignore=None): - if os.path.isdir(src): - if not os.path.isdir(dest): - os.makedirs(dest) - files = os.listdir(src) - if ignore is not None: - ignored = ignore(src, files) - else: - ignored = set() - for f in files: - if f not in ignored: - recursive_overwrite(os.path.join(src, f), - os.path.join(dest, f), - ignore) - else: - shutil.copyfile(src, dest) - -# -# Install files -# - -# copy extensions to IPython extensions directory -extensions = os.path.dirname(IPython.extensions.__file__) -src = os.path.join('src','extensions') -print("Install extensions to %s" % extensions) -recursive_overwrite(src, extensions) - -# Install templates -templates = os.path.join(jupyter_data_dir(), 'templates') -src = os.path.join('src','templates') -print("Install templates to %s" % templates) -recursive_overwrite(src, templates) - -# Install nbextensions -nbextensions = os.path.join(jupyter_data_dir(), 'nbextensions') -src = os.path.join('src','nbextensions') -print("Install notebook extensions to %s" % nbextensions) -recursive_overwrite(src, nbextensions) - -# -# Update nbconvert configuration -# -fname = os.path.join(jupyter_config_dir(), 'jupyter_nbconvert_config.json') -cl = JSONFileConfigLoader(fname) -config = cl.load_config() -newconfig=Config() -# Set template path, pre- and postprocessors of notebook extensions -newconfig.Exporter.template_path = [os.path.join(jupyter_data_dir(),'templates') ] -newconfig.Exporter.preprocessors = ["codefolding.CodeFoldingPreprocessor", "pymdpreprocessor.PyMarkdownPreprocessor" ] -newconfig.NbConvertApp.postprocessor_class = 'embed.EmbedPostProcessor' -config.merge(newconfig) -config.version = 1 -s=json.dumps(config, indent=2, separators=(',', ': '), sort_keys=True) -with open(fname, 'w') as f: - f.write(s) - -# -# Update notebook configuration -# -fname = os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json') -cl = JSONFileConfigLoader(fname, log=log) -config = cl.load_config() -newconfig=Config() -# Add server extension of /nbextension/ configuration tool -newconfig.NotebookApp.server_extensions = [ "nbextensions" ] -config.merge(newconfig) -config.version = 1 -s=json.dumps(config, indent=2, separators=(',', ': '), sort_keys=True) -with open(fname, 'w') as f: - f.write(s) diff --git a/extensions/nbextensions.py b/extensions/nbextensions.py index 6573edf..a8176d9 100644 --- a/extensions/nbextensions.py +++ b/extensions/nbextensions.py @@ -8,93 +8,95 @@ from notebook.base.handlers import IPythonHandler, json_errors from notebook.nbextensions import _get_nbext_dir as get_nbext_dir from tornado import web from itertools import chain -import os +import os.path import yaml +from yaml.scanner import ScannerError import json jupyterdir = jupyter_data_dir() -nbextensions = (get_nbext_dir(), os.path.join(jupyterdir,'nbextensions')) +nbextension_dirs = (get_nbext_dir(), os.path.join(jupyterdir, 'nbextensions')) exclude = [ 'mathjax' ] + class NBExtensionHandler(IPythonHandler): """Render the notebook extension configuration interface.""" + @web.authenticated def get(self): yaml_list = [] # Traverse through nbextension subdirectories to find all yaml files - for root, dirs, files in chain.from_iterable(os.walk(root) for root in nbextensions): + for root, dirs, files in chain.from_iterable( + os.walk(nb_ext_dir) for nb_ext_dir in nbextension_dirs): + # filter to exclude directories dirs[:] = [d for d in dirs if d not in exclude] - for f in files: - if f.endswith('.yaml'): - yaml_list.append([ root, f] ) + + for filename in files: + if filename.endswith('.yaml'): + yaml_list.append((root, filename)) + # Build a list of extensions from YAML file description # containing at least the following entries: - # Type - identifier - # Name - unique name of the extension - # Description - short explanation of the extension - # Main - main file that is loaded, typically 'main.js' + # Type - identifier + # Compatibility - compatible notebook version, e.g. '4.x' + # Name - unique name of the extension + # Description - short explanation of the extension + # Main - main file that is loaded, typically 'main.js' # extension_list = [] - for y in yaml_list: - stream = open(os.path.join(y[0],y[1]), 'r') - extension = yaml.load(stream) - if all (k in extension for k in ('Type', 'Compatibility', 'Name', 'Main', 'Description')): - if not extension['Type'].strip().startswith('IPython Notebook Extension'): + required_keys = ( + 'Type', 'Compatibility', 'Name', 'Main', 'Description') + + for ext_dir, yaml_filename in sorted(yaml_list): + with open(os.path.join(ext_dir, yaml_filename), 'r') as stream: + try: + extension = yaml.load(stream) + except ScannerError: + self.log.warning( + 'failed to load yaml file %r', yaml_filename) continue - if not extension['Compatibility'].strip().startswith(notebook.__version__[0:2]): - continue - # generate URL to extension - idx=y[0].find('nbextensions') - url = y[0][idx::].replace('\\', '/') - extension['url'] = url - # replace single quote with HTML representation - for key in extension: - if isinstance(extension[key], str): - extension[key] = extension[key].replace("'","'") - extension_list.append(extension) - self.log.info("Found extension %s" % extension['Name']) - stream.close() - json_list = json.dumps(extension_list) - # find where the Javascript code and the readme file are - config_js = None - config_md = None - for root, dirs, files in chain.from_iterable(os.walk(root) for root in nbextensions): - dirs[:] = [d for d in dirs if d not in exclude] - for f in files: - if root.endswith('nbconfig') and f =='main.js': - config_js = os.path.join(root, f) - if root.endswith('nbconfig') and f =='readme.md': - config_md = os.path.join(root, f) - if config_js is None: raise FileNotFoundError('Could not find nbconfig Javascript') - idx_js=config_js.find('nbextensions') - idx_md=config_js.find('nbextensions') - self.write(self.render_template('nbextensions.html', + + if any(key not in extension for key in required_keys): + continue + if not extension['Type'].strip().startswith( + 'IPython Notebook Extension'): + continue + compat = extension['Compatibility'].strip() + if not compat.startswith( + notebook.__version__[:2]): + pass + # continue + + # generate URL to extension's main js file + idx = ext_dir.find('nbextensions') + url = ext_dir[idx::].replace('\\', '/') + extension['url'] = url + + # replace single quote with HTML representation + for key in extension: + if isinstance(extension[key], str): + extension[key] = extension[key].replace("'","'") + + extension_list.append(extension) + self.log.info( + "Found {} extension {}".format(compat, extension['Name'])) + + extension_list_json = json.dumps(extension_list) + self.write(self.render_template( + 'nbextensions.html', base_url = self.base_url, - extension_list = json_list, - page_title="Notebook Extension Configuration", - config_js = self.base_url + config_js[idx_js::].replace('\\', '/'), - config_md = self.base_url + 'rendermd/' + config_md[idx_md::].replace('\\', '/') - ) - ) + extension_list = extension_list_json, + page_title="Notebook Extension Configuration" + )) class RenderExtensionHandler(IPythonHandler): """Render given markdown file""" @web.authenticated def get(self, path): - render_js = None - for root, dirs, files in chain.from_iterable(os.walk(root) for root in nbextensions): - dirs[:] = [d for d in dirs if d not in exclude] - for f in files: - if root.endswith('nbconfig') and f =='render.js': - render_js = os.path.join(root, f) - if render_js is None: raise FileNotFoundError('Could not find nbconfig Javascript') - idx_js=render_js.find('nbextensions') self.write(self.render_template('rendermd.html', base_url = self.base_url, render_url = path, page_title = path, - render_js = self.base_url + render_js[idx_js::].replace('\\', '/'), ) ) diff --git a/extensions/embed.py b/extensions/post_htmlembed.py old mode 100755 new mode 100644 similarity index 75% rename from extensions/embed.py rename to extensions/post_htmlembed.py index ac16f17..3bc8246 --- a/extensions/embed.py +++ b/extensions/post_htmlembed.py @@ -1,48 +1,51 @@ -# -*- coding: utf-8 -*- -"""PostProcessor for embedding markdown images in HTML files.""" -from __future__ import print_function - -import os -import re -import base64 -import requests - -from traitlets import Bool, Unicode, Int -from nbconvert.postprocessors.base import PostProcessorBase - - -class EmbedPostProcessor(PostProcessorBase): - """ Post processor designed to embed images in markdown cells as base64 encoded blob in HTML file """ - - def replfunc(self, match): - """ replace source url or file link with base64 encoded blob """ - url = match.group(1) - imgformat = url.split('.')[-1] - if url.startswith('http'): - data = request.get(url) - elif url.startswith('data'): - img = '' - return img - else: - with open(url, 'rb') as f: - data = f.read() - - self.log.info("embedding url: %s, format: %s" % (url, imgformat)) - b64_data=base64.b64encode(data).decode("utf-8") - if imgformat == "svg": - img = '' - elif imgformat == "pdf": - img = '' - else: - img = '' - return img - - def postprocess(self, input): - regex = re.compile('') - ext = input.split('.')[-1] - output=input[0:-(len(ext)+1)] + '-embedded.' + ext - with open(input) as fin, open(output,'w') as fout: - for line in fin: - fout.write(regex.sub(self.replfunc,line)) - fin.close() - fout.close() +# -*- coding: utf-8 -*- +"""PostProcessor for embedding markdown images in HTML files.""" +from __future__ import print_function + +import os +import re +import base64 +import requests + +from traitlets import Bool, Unicode, Int +from nbconvert.postprocessors.base import PostProcessorBase + + +class EmbedPostProcessor(PostProcessorBase): + """ Post processor designed to embed images in markdown cells as base64 encoded blob in HTML file """ + + def replfunc(self, match): + """ replace source url or file link with base64 encoded blob """ + url = match.group(1) + imgformat = url.split('.')[-1] + if url.startswith('http'): + data = request.get(url) + elif url.startswith('data'): + img = '' + return img + else: + with open(url, 'rb') as f: + data = f.read() + + self.log.info("embedding url: %s, format: %s" % (url, imgformat)) + b64_data=base64.b64encode(data).decode("utf-8") + if imgformat == "svg": + img = '' + elif imgformat == "pdf": + img = '' + else: + img = '' + return img + + def postprocess(self, input): + #print(self.__dir__()) + print(self.config) + if self.config.export_format == "html": + regex = re.compile('') + ext = input.split('.')[-1] + output=input[0:-(len(ext)+1)] + '-embedded.' + ext + with open(input) as fin, open(output,'w') as fout: + for line in fin: + fout.write(regex.sub(self.replfunc,line)) + fin.close() + fout.close() diff --git a/extensions/codefolding.py b/extensions/pre_codefolding.py similarity index 100% rename from extensions/codefolding.py rename to extensions/pre_codefolding.py diff --git a/extensions/pymdpreprocessor.py b/extensions/pre_pymarkdown.py similarity index 100% rename from extensions/pymdpreprocessor.py rename to extensions/pre_pymarkdown.py diff --git a/conda-recipe/meta.yaml b/meta.yaml old mode 100755 new mode 100644 similarity index 78% rename from conda-recipe/meta.yaml rename to meta.yaml index fb80415..5b54fb5 --- a/conda-recipe/meta.yaml +++ b/meta.yaml @@ -1,36 +1,37 @@ package: name: nbextensions - version: !!str 0.3 + version: "4.0" source: - path: ./src + git_url: https://github.com/ipython-contrib/IPython-notebook-extensions + git_tag: "4.0" build: - script: ipython install.py + script: python install.py requirements: build: - - python + - ipython >=4 - jupyter-client - jupyter-core - jupyter-notebook - nbconvert - nbformat + - python - traitlets - - ipython >=4 run: - - python + - ipython >=4 - jupyter-client - jupyter-core - jupyter-notebook - nbconvert - nbformat + - python - traitlets - - ipython >=4 about: - home: https://github.com/ipython-contrib/IPython-notebook-extensions/wiki + home: https://github.com/ipython-contrib/IPython-notebook-extensions license: Modified BSD License summary: 'Notebook extensions for the IPython notebook' diff --git a/nbextensions/config/config_menu/config_menu.yaml b/nbextensions/config/config_menu/config_menu.yaml new file mode 100644 index 0000000..573abf6 --- /dev/null +++ b/nbextensions/config/config_menu/config_menu.yaml @@ -0,0 +1,6 @@ +Type: IPython Notebook Extension +Name: NbExtensions menu item +Description: Add an edit-menu item to open the NbExtensions config page +Main: main.js +Compatibility: 4.x +Icon: icon.png diff --git a/nbextensions/config/config_menu/icon.png b/nbextensions/config/config_menu/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5b4a3f889b1802f704d40f261570ed3f2822a0be GIT binary patch literal 33794 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�Q1K+MO-3c;D1MHui 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 I(lq#g0FNQ6^Z)<= literal 0 HcmV?d00001 diff --git a/nbextensions/config/config_menu/main.js b/nbextensions/config/config_menu/main.js new file mode 100644 index 0000000..d2d6b06 --- /dev/null +++ b/nbextensions/config/config_menu/main.js @@ -0,0 +1,29 @@ +// extension by jcb91 +// Tiny extension to add an edit-menu item to open the NbExtensions config page + +define(["jquery"], function ($) { + "use strict"; + + var load_ipython_extension = function () { + var menu_item = $('
  • ').append( + $('', { + 'target' : '_blank', + 'title' : 'Opens in a new window', + 'href' : '/nbextensions/', + }) + .append(' ') + .append($('', {'class' : 'fa fa-cogs menu-icon pull-right'})) + .append($('').html('nbextension config')) + ); + + var edit_menu = $('#edit_menu'); + edit_menu.append($('
  • ').addClass('divider')); + edit_menu.append(menu_item); + }; + + // export the extension so it can be loaded correctly + var extension = { + load_ipython_extension : load_ipython_extension + }; + return extension; +}); diff --git a/nbextensions/config/main.css b/nbextensions/config/main.css new file mode 100644 index 0000000..1a2b071 --- /dev/null +++ b/nbextensions/config/main.css @@ -0,0 +1,170 @@ + +#nbext-container { + padding: 15px; + padding-top: 0; + -moz-box-shadow: 0 0 12px 1px rgba(87, 87, 87, 0.2); + -webkit-box-shadow: 0 0 12px 1px rgba(87, 87, 87, 0.2); + box-shadow: 0 0 12px 1px rgba(87, 87, 87, 0.2); +} + +.nbext-page-title-wrap { + margin-top: 6px; +} + +.nbext-page-title { + height: 1em; + line-height: 1em; + padding: 3px; + margin-left: 16px; + border: none; + font-size: 146.5%; + border-radius: 2px; +} + +.nbext-showhide-incompat { + margin-top: 1em; + margin-bottom: 0.5em; + float: left; + clear: both; +} + + +.nbext-row { + padding: 1em; + display: flex; + flex-direction: row; + align-items: center; + +} + +.nbext-row + .nbext-row { + border-top: 1px solid #AAA; +} + +.nbext-title { + margin-top: 0; +} + +.nbext-compat-true { + color: green; +} + +.nbext-compat-false { + color: red; +} + +.nbext-activate-btns button { + border-radius: 0; +} + +.nbext-activate-btns button:first-child { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.nbext-activate-btns button:last-child { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.nbext-activate-btns button[disabled] { + color: #cccccc; +} + +.nbext-icon { + margin-left: 8px; + text-align: center; +} + +.nbext-icon img { + max-height: 120px; + max-width: 100%; +} + +.nbext-params { + margin-top: 8px; +} + +.nbext-param { + display: block; + width: 100%; + padding: 8px; + border: 1px solid #cccccc; + border-radius: 0; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} + +.nbext-param:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.nbext-param:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.nbext-param + .nbext-param { + border-top: none; +} + +.nbext-param-name { + font-size: 120%; +} + +.nbext-list-wrap { + height: auto; +} + +.nbext-list { + margin-bottom: 0; + list-style-type: none; + -moz-padding-start: 0; + -webkit-padding-start: 0; +} + +.nbext-list-element, +.nbext-list-element-placeholder { + margin-bottom: 2px; +} + +.nbext-list-element-placeholder { + height: 32px; + width: 100%; + border: 1px solid #fcefa1; + background-color: #fbf9ee; + color: #363636; +} + +.nbext-list-element:only-child > .handle { + display: none +} + +.nbext-list-element:only-child > .handle + .form-control { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +a.input-group-addon.nbext-list-btn-add { + border-radius: 2px; + border: 1px solid #cccccc; +} + +/*.nbext-list-element input { + flex: 1; +} +*/ +/*.nbext-list-element a { + flex: 0; +} +*/ +.nbext-list-element:first-child .nbext-list-el-btn-up, +.nbext-list-element:last-child .nbext-list-el-btn-down { + display: none; +} diff --git a/nbextensions/config/main.js b/nbextensions/config/main.js new file mode 100644 index 0000000..9545b43 --- /dev/null +++ b/nbextensions/config/main.js @@ -0,0 +1,475 @@ +// Copyright (c) IPython-Contrib Team. +// Distributed under the terms of the Modified BSD License. + +// Show notebook extension configuration + +require([ + 'jqueryui', + 'require', + 'base/js/namespace', + 'base/js/page', + 'base/js/utils', + 'services/config', + "base/js/events" +], function( + $, + require, + IPython, + page, + utils, + configmod, + events +){ + "use strict"; + + var nbext_config_page = new page.Page(); + var base_url = utils.get_body_data('baseUrl'); + // get list of extensions from body data supplied by the python backend + var extension_list = $('body').data('extension-list'); + //sort them alphabetically + extension_list.sort(function (a, b) { + var an = a.Name.toLowerCase(); + var bn = b.Name.toLowerCase(); + if (an < bn) + return -1; + if (an > bn) + return 1; + return 0; + }); + + /** + * create config var from json config file on server. + * we still need to call config.load later to actually fetch it though! + */ + var config = new configmod.ConfigSection('notebook', {base_url: base_url}); + + // the prefix added to all parameter input id's + var param_id_prefix = 'input_'; + + /** + * A standardized way to get an element-style id from an extension name + */ + var ext_name_to_id = function(ext_name) { + /** + * The HTML 4.01 spec states that ID tokens must + * begin with a letter ([A-Za-z]) + * which may be followed by any number of + * letters, digits, hyphens, underscores, colons, and periods + */ + return 'nbext-ext-' + ext_name.replace(/[^A-Za-z0-9-_:.]/g, ''); + }; + + /** + * Compute the url of an extension's main javascript file + */ + var get_ext_url = function(ext) { + var url = base_url + ext.url + '/' + ext.Main; + url = url.split('.js')[0]; + url = url.split('nbextensions/')[1]; + return url; + }; + + /** + * Update server's json config file to reflect changed activate state + */ + var set_config_active = function(ext_id, state) { + state = state === true; + for(var i=0; i < extension_list.length; i++) { + var ext = extension_list[i]; + var ext_name = ext['Name']; + if (ext_name_to_id(ext_name) == ext_id) { + console.log( + "Turning extension", ext_name, state ? ' on' : ' off'); + var to_load = {}; + var ext_url = get_ext_url(ext); + to_load[ext_url] = (state ? true : null); + config.update({"load_extensions": to_load}); + } + } + }; + + /** + * Update buttons to reflect changed activate state + */ + var set_buttons_active = function(ext_id, state) { + state = (state === true); + $('#' + ext_id + (state ? '-on' : '-off')) + .prop('disabled', true) + .removeClass('btn-primary').addClass('btn-default'); + $('#' + ext_id + (state ? '-off' : '-on')) + .prop('disabled', false) + .removeClass('btn-default').addClass('btn-primary'); + }; + + /** + * Handle button click event to activate/deactivate extension + */ + var handle_buttons_click = function(evt) { + var ext_id = this.id.replace(/-on|-off/, ''); + var state = (this.id.search(/-on/) >= 0) ? true : false; + set_buttons_active(ext_id, state); + set_config_active(ext_id, state); + }; + + /* + * Get the useful value (dependent on element type) from an input element + */ + var get_input_value = function(input) { + input = $(input); + var input_type = input.data('param_type'); + + switch (input_type) { + case 'list': + var val=[]; + input.find('.nbext-list-element').children().not('a').each( + function () { + // "this" is the current child element of input in the loop + val.push(get_input_value(this)); + } + ); + return val; + case 'checkbox': + return input.prop('checked') ? true : false; + default: + return input.val(); + } + }; + + /* + * Set the useful value (dependent on element type) from a js value + */ + var set_input_value = function(input, new_value) { + input = $(input); + var input_type = input.data('param_type'); + switch (input_type) { + case 'list': + var ul = input.children('ul'); + ul.empty(); + var list_element_param = input.data('list_element_param'); + for (var ii=0; ii < new_value.length; ii++) { + var list_element_input = build_param_input(list_element_param); + set_input_value(list_element_input, new_value[ii]); + ul.append(wrap_list_input(list_element_input)); + } + break; + case 'checkbox': + input.prop('checked', new_value ? true : false); + break; + default: + input.val(new_value); + } + }; + + /** + * handle form input for extension parameters, updating parameters in + * server's json config file + */ + var handle_input = function(evt) { + var input = $(evt.target); + + // list elements should alter their parent + if (input.parent().hasClass('nbext-list-element')) { + input = input.parent().parent().parent(); + } + // get param name by cutting off prefix + var configkey = input.attr('id').substring(param_id_prefix.length); + var configval = get_input_value(input); + console.log(configkey, '->', configval); + var c = {}; + c[configkey] = configval; + config.update(c); + return configval; + }; + + var wrap_list_input = function(list_input) { + var btn_remove = $('', {'class': 'btn btn-default input-group-addon nbext-list-el-btn-remove'}); + btn_remove.append($('', {'class': 'fa fa-fw fa-trash'})); + btn_remove.on('click', function () { + var list_el = $(this).closest('li'); + var list_input = list_el.closest('.nbext-list-wrap'); + list_el.remove(); + list_input.change(); // trigger change event + }); + + return $('
  • ', {'class' : 'nbext-list-element input-group'}).append( + $('').append( + $('') + ), + [list_input, btn_remove]); + // , [btn_up, btn_down, btn_remove]); + }; + + + /** + * Build and return an element used to edit a parameter + */ + var build_param_input = function(param) { + var input_type = (param.input_type || 'text').toLowerCase(); + var input; + + switch (input_type) { + case 'list': + input = $('
    ', {'class' : 'nbext-list-wrap'}); + input.append( + $('