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 0000000..5b4a3f8 Binary files /dev/null and b/nbextensions/config/config_menu/icon.png differ 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( + $('