├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── DEPLOY.md ├── LICENSE.txt ├── README.md ├── install.json ├── jupyter-wysiwyg ├── __init__.py └── _version.py ├── package.json ├── pyproject.toml ├── setup.py ├── src ├── editor.ts ├── factory.ts ├── index.ts ├── plugin.ts └── version.ts ├── style ├── index.css └── index.js ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | **/*.d.ts 5 | tests 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | // 'plugin:prettier/recommended' 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: 'tsconfig.json', 11 | sourceType: 'module' 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | rules: { 15 | 'prettier/prettier': 0, 16 | '@typescript-eslint/naming-convention': [ 17 | 'error', 18 | { 19 | 'selector': 'interface', 20 | 'format': ['PascalCase'], 21 | 'custom': { 22 | 'regex': '^I[A-Z]', 23 | 'match': true 24 | } 25 | } 26 | ], 27 | '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], 28 | '@typescript-eslint/no-explicit-any': 'off', 29 | '@typescript-eslint/no-namespace': 'off', 30 | '@typescript-eslint/no-use-before-define': 'off', 31 | '@typescript-eslint/quotes': [ 32 | 'error', 33 | 'single', 34 | { avoidEscape: true, allowTemplateLiterals: false } 35 | ], 36 | curly: ['error', 'all'], 37 | eqeqeq: 'error', 38 | 'prefer-arrow-callback': 'error' 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | *.tsbuildinfo 7 | jupyter-wysiwyg/labextension 8 | 9 | # Created by https://www.gitignore.io/api/python 10 | # Edit at https://www.gitignore.io/?templates=python 11 | 12 | ### Python ### 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | pip-wheel-metadata/ 36 | share/python-wheels/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | .spyproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # Mr Developer 94 | .mr.developer.cfg 95 | .project 96 | .pydevproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | .dmypy.json 104 | dmypy.json 105 | 106 | # Pyre type checker 107 | .pyre/ 108 | 109 | # End of https://www.gitignore.io/api/python 110 | 111 | # OSX files 112 | .DS_Store 113 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tests/ 4 | .jshintrc 5 | # Ignore any build output from python: 6 | dist/*.tar.gz 7 | dist/*.wheel 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | dist: trusty 4 | addons: 5 | firefox: latest 6 | cache: 7 | directories: 8 | - $HOME/.npm 9 | before_install: 10 | - wget https://github.com/mozilla/geckodriver/releases/download/v0.11.1/geckodriver-v0.11.1-linux64.tar.gz 11 | - mkdir geckodriver 12 | - tar -xzf geckodriver-v0.11.1-linux64.tar.gz -C geckodriver 13 | - export PATH=$PATH:$PWD/geckodriver 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start || true 16 | install: 17 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 18 | - bash miniconda.sh -b -p $HOME/miniconda 19 | - export PATH="$HOME/miniconda/bin:$PATH" 20 | - hash -r 21 | - conda config --set always_yes yes --set changeps1 no 22 | - conda update -q conda 23 | - conda info -a 24 | - conda install nodejs notebook 25 | - pip install selenium cookiecutter 26 | - pip install --pre jupyterlab 27 | script: 28 | - cd jupyter_wysiwyg 29 | - jupyter labextension install . 30 | - jupyter lab clean 31 | - jupyter labextension link . 32 | - python -m jupyterlab.selenium_check -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # First Time 2 | 3 | 1. Make sure you have twine and build installed: 4 | > pip install twine build 5 | 2. Make sure you have added your [PyPI credentials](https://docs.python.org/3.3/distutils/packageindex.html#pypirc) to `~/.pypirc` 6 | 3. Make sure you have anaconda-client installed: 7 | > conda install anaconda-client 8 | 4. Log into Anaconda Cloud 9 | > anaconda login 10 | 11 | # How to Deploy to PyPi Test 12 | 13 | 1. Make sure that the version number is updated in `package.json`. 14 | 2. Navigate to the directory where the repository was checked out: 15 | > cd jupyter-wysiwyg 16 | 3. Remove any residual build artifacts from the last time jupyter-wysiwyg was built. This step is not necessary the first time the package is built. 17 | > rm dist/\*.tar.gz; rm dist/\*.whl 18 | 4. Build the sdist and wheel artifacts. 19 | > python -m build . 20 | 5. Upload the files by running: 21 | > twine upload -r pypitest dist/\*.tar.gz; twine upload -r pypitest dist/\*.whl 22 | 6. If the upload fails go to [https://testpypi.python.org/pypi](https://testpypi.python.org/pypi) and manually upload dist/jupyter-wysiwyg-*.tar.gz. 23 | 7. Test the deploy by uninstalling and reinstalling the package: 24 | > pip uninstall jupyter-wysiwyg; 25 | > pip install -i https://test.pypi.org/simple/ jupyter-wysiwyg 26 | 27 | # How to Deploy to Production PyPi 28 | 29 | 1. First deploy to test and ensure everything is working correctly (see above). 30 | 2. Make sure that the version number is updated in `package.json`. 31 | 3. Navigate to the directory where the repository was checked out: 32 | > cd jupyter-wysiwyg 33 | 4. Remove any residual build artifacts from the last time jupyter-wysiwyg was built. This step is not necessary the first time the package is built. 34 | > rm dist/\*.tar.gz; rm dist/\*.whl 35 | 5. Build the sdist and wheel artifacts. 36 | > python -m build . 37 | 6. Upload the files by running: 38 | > twine upload dist/\*.tar.gz; twine upload dist/\*.whl 39 | 7. If the upload fails go to [https://testpypi.python.org/pypi](https://testpypi.python.org/pypi) and manually upload dist/jupyter-wysiwyg-*.tar.gz. 40 | 8. Test the deploy by uninstalling and reinstalling the package: 41 | > pip uninstall jupyter-wysiwyg; 42 | > pip install jupyter-wysiwyg 43 | 44 | # How to Deploy to Conda 45 | 46 | 1. Deploy to Production PyPi 47 | 2. Navigate to Anaconda directory 48 | > cd /anaconda3 49 | 3. Activate a clean environment. 50 | > conda activate clean 51 | 4. Run the following, removing the existing directory if necessary: 52 | > conda skeleton pypi jupyter-wysiwyg --version XXX 53 | 5. Build the package: 54 | > conda build jupyter-wysiwyg 55 | 6. Converting this package to builds for other operating systems can be done as shown below. You will need to upload each 56 | built version using a separate upload command. 57 | > conda convert --platform all /anaconda3/conda-bld/osx-64/jupyter-wysiwyg-XXX-py37_0.tar.bz2 -o conda-bld/ 58 | 7. Upload the newly built package: 59 | > anaconda upload /anaconda3/conda-bld/*/jupyter-wysiwyg-XXX-py37_0.tar.bz2 -u g2nb 60 | 8. Log into the [Anaconda website](https://anaconda.org/) to make sure everything is good. 61 | 62 | # How to deploy to NPM 63 | 64 | 1. Make sure that the version number is updated in `package.json`. 65 | 2. Navigate to the directory where the repository was checked out: 66 | > cd jupyter-wysiwyg 67 | 3. Log in to NPM is necessary 68 | > npm login 69 | 4. Build and upload the package. Be prepared to enter a two-factor authentication code when prompted. 70 | > npm publish -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2015-2021, Regents of the University of California & Broad Institute 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rich Text HTML/Markdown Editor for JupyterLab 2 | 3 | This is an nbextension that enables WYSIWYG editing functionality for HTML/Markdown cells in Jupyter. 4 | 5 | ## Requirements 6 | 7 | * JupyterLab >= 3.0 8 | * ipywidgets >= 7.0.0 9 | 10 | ## Installing 11 | 12 | This extension can be installed through PIP. 13 | 14 | ```bash 15 | pip install jupyter-wysiwyg 16 | ``` 17 | 18 | ***OR*** 19 | 20 | ```bash 21 | # Clone the jupyter-wysiwyg repository 22 | git clone https://github.com/g2nb/jupyter-wysiwyg.git 23 | cd jupyter-wysiwyg 24 | 25 | # Install the jupyter-wysiwyg JupyterLab prototype 26 | pip install -e . 27 | jupyter labextension develop . --overwrite 28 | ``` 29 | 30 | ## Development 31 | 32 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in 33 | the extension's source and automatically rebuild the extension. To develop, run each of the following commands in a 34 | separate terminal. 35 | 36 | ```bash 37 | jlpm run watch 38 | jupyter lab 39 | ``` 40 | 41 | The `jlpm` command is JupyterLab's pinned version of [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You 42 | may use `yarn` or `npm` in lieu of `jlpm`. 43 | 44 | With the watch command running, every saved change will immediately be built locally and available in your running 45 | JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the 46 | extension to be rebuilt). 47 | 48 | By default, the `jlpm run build` command generates the source maps for this extension to make it easier to debug using 49 | the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: 50 | 51 | ```bash 52 | jupyter lab build --minimize=False 53 | ``` 54 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyter-wysiwyg", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyter-wysiwyg" 5 | } 6 | -------------------------------------------------------------------------------- /jupyter-wysiwyg/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from ._version import __version__ 5 | 6 | HERE = Path(__file__).parent.resolve() 7 | 8 | with (HERE / "labextension" / "package.json").open() as fid: 9 | data = json.load(fid) 10 | 11 | def _jupyter_labextension_paths(): 12 | return [{ 13 | "src": "labextension", 14 | "dest": data["name"] 15 | }] 16 | 17 | -------------------------------------------------------------------------------- /jupyter-wysiwyg/_version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Regents of the University of California & the Broad Institute. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import json 5 | from pathlib import Path 6 | 7 | __all__ = ["__version__"] 8 | 9 | 10 | def _fetch_version(): 11 | HERE = Path(__file__).parent.resolve() 12 | 13 | for settings in HERE.rglob("package.json"): 14 | try: 15 | with settings.open() as f: 16 | return json.load(f)["version"] 17 | except FileNotFoundError: 18 | pass 19 | 20 | raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}") 21 | 22 | 23 | __version__ = _fetch_version() 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@g2nb/jupyter-wysiwyg", 3 | "version": "23.03.0", 4 | "description": "WYSIWYG editing functionality for markdown/HTML cells in Jupyter", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension", 9 | "widgets" 10 | ], 11 | "files": [ 12 | "lib/**/*.{js,css}", 13 | "style/*.{js,css,png,svg}", 14 | "dist/*.{js,css}", 15 | "style/index.js" 16 | ], 17 | "homepage": "https://github.com/g2nb/jupyter-wysiwyg", 18 | "bugs": { 19 | "url": "https://github.com/g2nb/jupyter-wysiwyg/issues" 20 | }, 21 | "license": "BSD-3-Clause", 22 | "author": { 23 | "name": "Thorin Tabor", 24 | "email": "tmtabor@cloud.ucsd.edu" 25 | }, 26 | "main": "lib/index.js", 27 | "types": "./lib/index.d.ts", 28 | "style": "style/index.css", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/g2nb/jupyter-wysiwyg" 32 | }, 33 | "scripts": { 34 | "build": "jlpm run build:lib && jlpm run build:labextension:dev", 35 | "build:all": "jlpm run build:lib && jlpm run build:labextension", 36 | "build:labextension": "jupyter labextension build .", 37 | "build:labextension:dev": "jupyter labextension build --development True .", 38 | "build:lib": "tsc", 39 | "clean": "jlpm run clean:lib", 40 | "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", 41 | "clean:labextension": "rimraf jupyter-wysiwyg/labextension", 42 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 43 | "eslint": "eslint . --ext .ts,.tsx --fix", 44 | "eslint:check": "eslint . --ext .ts,.tsx", 45 | "install:extension": "jlpm run build", 46 | "prepack": "jlpm run build:lib", 47 | "prepare": "jlpm run clean && jlpm run build:all", 48 | "test": "jlpm run test:firefox", 49 | "test:chrome": "karma start --browsers=Chrome tests/karma.conf.js", 50 | "test:debug": "karma start --browsers=Chrome --singleRun=false --debug=true tests/karma.conf.js", 51 | "test:firefox": "karma start --browsers=Firefox tests/karma.conf.js", 52 | "test:ie": "karma start --browsers=IE tests/karma.conf.js", 53 | "watch": "run-p watch:src watch:labextension", 54 | "watch:labextension": "jupyter labextension watch .", 55 | "watch:lib": "tsc -w", 56 | "watch:src": "tsc -w" 57 | }, 58 | "dependencies": { 59 | "@jupyter-widgets/base": "^4.0.0", 60 | "@jupyterlab/application": "^3.5.3", 61 | "@jupyterlab/mainmenu": "^3.5.3", 62 | "@jupyterlab/notebook": "^3.5.3", 63 | "tinymce": "^5.9.2" 64 | }, 65 | "devDependencies": { 66 | "@jupyterlab/apputils": "^3.5.3", 67 | "@jupyterlab/builder": "^3.5.3", 68 | "@jupyterlab/codeeditor": "3.5.3", 69 | "@jupyterlab/ui-components": "^3.5.3", 70 | "@lumino/application": "^1.31.3", 71 | "@lumino/widgets": "^1.37.1", 72 | "@types/backbone": "^1.4.4", 73 | "@types/node": "^14.14.27", 74 | "@typescript-eslint/eslint-plugin": "^4.8.1", 75 | "@typescript-eslint/parser": "^4.8.1", 76 | "backbone": "^1.2.3", 77 | "css-loader": "^5.0.2", 78 | "eslint": "^7.14.0", 79 | "expect.js": "^0.3.1", 80 | "file-loader": "^6.2.0", 81 | "fs-extra": "^9.1.0", 82 | "karma": "^6.1.1", 83 | "karma-typescript": "^5.3.0", 84 | "mkdirp": "^1.0.4", 85 | "mocha": "^8.3.0", 86 | "npm-run-all": "^4.1.5", 87 | "rimraf": "^3.0.2", 88 | "source-map-loader": "^2.0.1", 89 | "style-loader": "^2.0.0", 90 | "ts-loader": "^8.0.17", 91 | "typescript": "~4.1.3", 92 | "webpack": "^5.21.2", 93 | "webpack-cli": "^4.5.0" 94 | }, 95 | "jupyterlab": { 96 | "extension": "lib/plugin", 97 | "sharedPackages": { 98 | "@jupyter-widgets/base": { 99 | "bundled": false, 100 | "singleton": true 101 | } 102 | }, 103 | "discovery": { 104 | "kernel": [ 105 | { 106 | "kernel_spec": { 107 | "language": "^python" 108 | }, 109 | "base": { 110 | "name": "jupyter-wysiwyg" 111 | }, 112 | "managers": [ 113 | "pip", 114 | "conda" 115 | ] 116 | } 117 | ] 118 | }, 119 | "outputDir": "jupyter-wysiwyg/labextension" 120 | }, 121 | "styleModule": "style/index.js" 122 | } 123 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling", 4 | "jupyterlab>=3.4", 5 | ] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "jupyter-wysiwyg" 10 | description = "WYSIWYG editing functionality for markdown/HTML cells in Jupyter" 11 | readme = "README.md" 12 | requires-python = ">=3.6" 13 | authors = [ 14 | { name = "Thorin Tabor", email = "tmtabor@cloud.ucsd.edu" }, 15 | ] 16 | keywords = [ 17 | "Jupyter", 18 | "JupyterLab", 19 | "JupyterLab3", 20 | ] 21 | classifiers = [ 22 | "Framework :: Jupyter", 23 | "Intended Audience :: Developers", 24 | "Intended Audience :: Science/Research", 25 | "License :: OSI Approved :: BSD License", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | ] 31 | dependencies = [ 32 | "jupyterlab>=3.4", 33 | ] 34 | version = "23.03.0" 35 | 36 | [project.license] 37 | file = "LICENSE.txt" 38 | 39 | [project.urls] 40 | Homepage = "https://github.com/g2nb/jupyter-wysiwyg" 41 | 42 | [tool.hatch.build] 43 | artifacts = [ 44 | "jupyter-wysiwyg/labextension", 45 | ] 46 | 47 | [tool.hatch.build.targets.wheel.shared-data] 48 | "jupyter-wysiwyg/labextension/static" = "share/jupyter/labextensions/@g2nb/jupyter-wysiwyg/static" 49 | "install.json" = "share/jupyter/labextensions/@g2nb/jupyter-wysiwyg/install.json" 50 | "jupyter-wysiwyg/labextension/package.json" = "share/jupyter/labextensions/@g2nb/jupyter-wysiwyg/package.json" 51 | 52 | [tool.hatch.build.targets.sdist] 53 | exclude = [ 54 | ".github", 55 | ] 56 | 57 | [tool.hatch.build.hooks.jupyter-builder] 58 | dependencies = [ 59 | "hatch-jupyter-builder>=0.8.2", 60 | ] 61 | build-function = "hatch_jupyter_builder.npm_builder" 62 | ensured-targets = [ 63 | "jupyter-wysiwyg/labextension/static/style.js", 64 | "jupyter-wysiwyg/labextension/package.json", 65 | ] 66 | skip-if-exists = [ 67 | "jupyter-wysiwyg/labextension/static/style.js", 68 | ] 69 | 70 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 71 | build_dir = "jupyter-wysiwyg/labextension" 72 | source_dir = "src" 73 | build_cmd = "install:extension" 74 | npm = [ 75 | "jlpm", 76 | ] 77 | 78 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 79 | build_cmd = "build:all" 80 | npm = [ 81 | "jlpm", 82 | ] 83 | 84 | [tool.tbump] 85 | github_url = "https://github.com/g2nb/jupyter-wysiwyg" 86 | 87 | [tool.tbump.version] 88 | current = "23.03.0" 89 | regex = ''' 90 | (?P\d+) 91 | \. 92 | (?P\d+) 93 | \. 94 | (?P\d+) 95 | (?P
((a|b|rc)\d+))?
 96 |   (\.
 97 |     (?Pdev\d*)
 98 |   )?
 99 |   '''
100 | 
101 | [tool.tbump.git]
102 | message_template = "Bump to {new_version}"
103 | tag_template = "v{new_version}"
104 | 
105 | [[tool.tbump.file]]
106 | src = "pyproject.toml"
107 | version_template = "version = \"{major}.{minor}.{patch}\""
108 | 
109 | [[tool.tbump.file]]
110 | src = "package.json"
111 | version_template = "\"version\": \"{major}.{minor}.{patch}\""
112 | 


--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # setup.py shim for use with applications that require it.
2 | __import__("setuptools").setup()
3 | 


--------------------------------------------------------------------------------
/src/editor.ts:
--------------------------------------------------------------------------------
  1 | import { CodeEditor } from "@jupyterlab/codeeditor";
  2 | import { IMarkdownCellModel, MarkdownCell } from "@jupyterlab/cells";
  3 | import { UUID } from "@lumino/coreutils";
  4 | import { Signal } from "@lumino/signaling";
  5 | import { IDisposable, DisposableDelegate } from "@lumino/disposable";
  6 | import { ArrayExt } from '@lumino/algorithm';
  7 | import { JSONValue } from "@lumino/coreutils/src/json";
  8 | import { EditorWidget } from "./factory";
  9 | 
 10 | // Import TinyMCE
 11 | import TinyMCE from "tinymce";
 12 | import 'tinymce/icons/default';                 // Default icons are required for TinyMCE 5.3 or above
 13 | import 'tinymce/themes/silver';                 // A theme is also required
 14 | import 'tinymce/skins/ui/oxide/skin.css';       // Import the skin
 15 | import 'tinymce/plugins/advlist';               // Import plugins
 16 | import 'tinymce/plugins/code';
 17 | import 'tinymce/plugins/emoticons';
 18 | import 'tinymce/plugins/emoticons/js/emojis';
 19 | import 'tinymce/plugins/link';
 20 | import 'tinymce/plugins/lists';
 21 | import 'tinymce/plugins/table';
 22 | // import contentUiCss from 'tinymce/skins/ui/oxide/content.css'; // Import content CSS
 23 | // import contentCss from 'tinymce/skins/content/default/content.css';
 24 | 
 25 | export class TinyMCEEditor implements CodeEditor.IEditor {
 26 |     constructor(options: TinyMCEEditor.IOptions, markdownModel: IMarkdownCellModel) {
 27 |         this.host = options.host;
 28 |         this.host.classList.add("jp-RenderedHTMLCommon");
 29 |         this.host.classList.add('jp-TinyMCE');
 30 |         this.host.addEventListener('focus', this.blur, true);
 31 |         this.host.addEventListener('blur', this.focus, true);
 32 |         this.host.addEventListener('scroll', this.scroll, true);
 33 | 
 34 |         this._uuid = options.uuid || UUID.uuid4();
 35 | 
 36 |         this._model = options.model;
 37 |         this.is_markdown = (markdownModel.metadata.get("markdownMode") as boolean);
 38 |         if (!!this.is_markdown) this.is_markdown = false;
 39 |         this._view = new TinyMCEView(this.host, this.model);
 40 |     }
 41 | 
 42 |     static DEFAULT_NUMBER: number = 0;
 43 |     private _model: CodeEditor.IModel;
 44 |     private _uuid = '';
 45 |     private _is_disposed = false;
 46 |     private _keydownHandlers = new Array();
 47 |     private _selection_style: CodeEditor.ISelectionStyle;
 48 |     private _view: TinyMCEView;
 49 |     public is_markdown = false;
 50 |     readonly charWidth: number;
 51 |     readonly edgeRequested = new Signal(this);
 52 |     readonly host: HTMLElement;
 53 |     readonly isDisposed: boolean;
 54 |     readonly lineHeight: number;
 55 | 
 56 |     // Getters
 57 |     get view() { return this._view; }
 58 |     get uuid() { return this._uuid; }
 59 |     get is_disposed() { return this._is_disposed; }
 60 |     get model() { return this._model; }
 61 |     get lineCount() { return TinyMCEEditor.DEFAULT_NUMBER; }
 62 |     get selectionStyle() { return this._selection_style; }
 63 |     get doc() { return new DummyDoc(); }
 64 | 
 65 |     // Setters
 66 |     set uuid(value) { this._uuid = value; }
 67 |     set selectionStyle(value) { this._selection_style = value; }
 68 | 
 69 |     blur(): void { if (this._view) this._view.blur(); }
 70 |     focus(): void { if (this._view) this._view.focus(); }
 71 | 
 72 |     addKeydownHandler(handler: (instance: CodeEditor.IEditor, event: KeyboardEvent) => boolean): IDisposable {
 73 |         this._keydownHandlers.push(handler);
 74 |         return new DisposableDelegate(() => {
 75 |             ArrayExt.removeAllWhere(this._keydownHandlers, val => val === handler);
 76 |         });
 77 |     }
 78 | 
 79 |     dispose(): void {
 80 |         if (this._is_disposed) return;
 81 |         this._is_disposed = true;
 82 |         this.host.removeEventListener('focus', this.focus, true);
 83 |         this.host.removeEventListener('focus', this.blur, true);
 84 |         this.host.removeEventListener('focus', this.scroll, true);
 85 |         Signal.clearData(this);
 86 |     }
 87 | 
 88 |     // Called when a markdown cell is either first rendered or toggled into editor mode
 89 |     refresh(): void {
 90 |         const active_cell = EditorWidget.instance().tracker.activeCell;
 91 |         if (active_cell instanceof MarkdownCell && !active_cell.rendered && EditorWidget.instance().no_side_button()) {
 92 |             active_cell.editor.focus();
 93 |             EditorWidget.instance().render_side_button();
 94 |         }
 95 |     }
 96 | 
 97 |     // This is a dummy implementation that prevents an error in the console
 98 |     getCursorPosition(): CodeEditor.IPosition {
 99 |         return new class implements CodeEditor.IPosition {
100 |             [key: string]: JSONValue;
101 |             line: 0;
102 |             column: 0;
103 |         }();
104 |     }
105 | 
106 |     // This is a dummy implementation that prevents an error in the console
107 |     getCursor(): CodeEditor.IPosition {
108 |         return this.getCursorPosition();
109 |     }
110 | 
111 |     // Empty stubs necessary to implement CodeEditor.IEditor, full integration may require implementing these methods
112 |     clearHistory(): void {}
113 |     scroll(): void {}
114 |     getCoordinateForPosition(position: CodeEditor.IPosition): CodeEditor.ICoordinate { return undefined; }
115 |     getLine(line: number): string | undefined { return undefined; }
116 |     getOffsetAt(position: CodeEditor.IPosition): number { return 0; }
117 |     getOption(option: K): CodeEditor.IConfig[K] { return undefined; }
118 |     getPositionAt(offset: number): CodeEditor.IPosition | undefined { return undefined; }
119 |     getPositionForCoordinate(coordinate: CodeEditor.ICoordinate): CodeEditor.IPosition | null { return undefined; }
120 |     getSelection(): CodeEditor.IRange { return undefined; }
121 |     getSelections(): CodeEditor.IRange[] { return []; }
122 |     getTokenForPosition(position: CodeEditor.IPosition): CodeEditor.IToken { return undefined; }
123 |     getTokens(): CodeEditor.IToken[] { return []; }
124 |     hasFocus(): boolean { return false; }
125 |     newIndentedLine(): void {}
126 |     operation(func: Function): void {}
127 |     redo(): void {}
128 |     removeOverlay(overlay: any): void {}
129 |     resizeToFit(): void {}
130 |     revealPosition(position: CodeEditor.IPosition): void {}
131 |     revealSelection(selection: CodeEditor.IRange): void {}
132 |     setCursorPosition(position: CodeEditor.IPosition): void {}
133 |     setOption(option: K, value: CodeEditor.IConfig[K]): void {}
134 |     setOptions(options: Partial): void {}
135 |     setSelection(selection: CodeEditor.IRange): void {}
136 |     setSelections(selections: CodeEditor.IRange[]): void {}
137 |     setSize(size: CodeEditor.IDimension | null): void {}
138 |     undo(): void {}
139 |     firstLine() { return ''; }
140 |     lastLine() { return ''; }
141 | }
142 | 
143 | /**
144 |  * Dummy implementation prevents errors in search functionality
145 |  */
146 | class DummyDoc {
147 |     sliceString(from: any, to: any) { return ''; }
148 |     toString() { return ''; }
149 |     get length() { return 0; }
150 |     lineAt(index: any) { return ''; }
151 |     line(index: any) { return ''; }
152 |     firstLine() { return ''; }
153 |     lastLine() { return ''; }
154 |     getSelection() { return ''; }
155 |     getRange(start: any, end: any) { return ''; }
156 |     removeOverlay(overlay: any): void {}
157 | }
158 | 
159 | export namespace TinyMCEEditor {
160 |     export interface IOptions extends CodeEditor.IOptions { config?: Partial; }
161 |     export interface IConfig extends CodeEditor.IConfig {}
162 | }
163 | 
164 | export class TinyMCEView {
165 |     constructor(host: HTMLElement, model: CodeEditor.IModel) {
166 |         // Create the wrapper for TinyMCE
167 |         const wrapper = document.createElement("div");
168 |         host.appendChild(wrapper);
169 | 
170 |         // Wait for cell initialization before initializing editor
171 |         setTimeout(() => {
172 |             // Special case to remove anchor links before loading
173 |             const render_node = host?.parentElement?.querySelector('.jp-MarkdownOutput');
174 |             if (render_node) render_node.querySelectorAll('.jp-InternalAnchorLink').forEach(e => e.remove());
175 |             wrapper.innerHTML = render_node?.innerHTML || model.value.text;
176 | 
177 |             try {
178 |                 TinyMCE.init({
179 |                     target: wrapper,
180 |                     skin: false,
181 |                     content_css: false,
182 |                     // content_style: contentUiCss.toString() + '\n' + contentCss.toString(),
183 |                     branding: false,
184 |                     contextmenu: false,
185 |                     elementpath: false,
186 |                     menubar: false,
187 |                     height: 300,
188 |                     resize: false,
189 |                     plugins: 'emoticons lists link code',
190 |                     toolbar: 'styleselect fontsizeselect | bold italic underline strikethrough | subscript superscript | link forecolor backcolor emoticons | bullist numlist outdent indent blockquote | code',
191 |                     init_instance_callback: (editor: any) => editor.on('Change', () => model.value.text = editor.getContent())
192 |                 }).then(editor => {
193 |                     if (!editor.length) return; // If no valid editors, do nothing
194 |                     editor[0].on("focus", () => {
195 |                         const index = this.get_cell_index(model);
196 |                         if (index !== null)
197 |                             EditorWidget.instance().tracker.currentWidget.content.activeCellIndex = index;
198 |                     });
199 |                 });
200 |             }
201 |             catch (e) {
202 |                 console.log("TinyMCE threw an error: " + e);
203 |             }
204 |         }, 500);
205 |     }
206 | 
207 |     blur() {}
208 | 
209 |     focus() {}
210 | 
211 |     get_cell_index(model: CodeEditor.IModel) {
212 |         const id = model.modelDB.basePath;
213 |         const all_cells = EditorWidget.instance().tracker.currentWidget.content.widgets;
214 |         for (let i = 0; i < all_cells.length; i++) {
215 |             const cell = all_cells[i];
216 |             const cell_id = cell.model.modelDB.basePath;
217 |             if (id === cell_id) return i;
218 |         }
219 |         return null;
220 |     }
221 | }
222 | 


--------------------------------------------------------------------------------
/src/factory.ts:
--------------------------------------------------------------------------------
  1 | import {INotebookTracker, NotebookActions, NotebookPanel, StaticNotebook} from "@jupyterlab/notebook";
  2 | import { Cell, MarkdownCell } from "@jupyterlab/cells";
  3 | import { CodeEditor } from "@jupyterlab/codeeditor";
  4 | import { Token } from "@lumino/coreutils";
  5 | import { Widget } from "@lumino/widgets";
  6 | import { TinyMCEEditor } from "./editor";
  7 | import { runIcon } from "@jupyterlab/ui-components";
  8 | 
  9 | export class EditorContentFactory extends NotebookPanel.ContentFactory implements IEditorContentFactory {
 10 |     constructor(options?: Cell.ContentFactory.IOptions | undefined) {
 11 |         super(options);
 12 |     }
 13 | 
 14 |     /**
 15 |      * Create a markdown cell with the WYSIWYG editor rather than CodeMirror
 16 |      *
 17 |      * @param options
 18 |      * @param parent
 19 |      */
 20 |     createMarkdownCell(options: MarkdownCell.IOptions, parent: StaticNotebook): MarkdownCell {
 21 |         const model = options.model;
 22 |         options.contentFactory = new EditorContentFactory({
 23 |             editorFactory: (options: CodeEditor.IOptions) => {
 24 |                 return new TinyMCEEditor(options, model);
 25 |             }
 26 |         } as any);
 27 | 
 28 |         return new MarkdownCell(options).initializeState();
 29 |       }
 30 | }
 31 | 
 32 | export const IEditorContentFactory = new Token("jupyter-wysiwyg");
 33 | 
 34 | export interface IEditorContentFactory extends NotebookPanel.IContentFactory {}
 35 | 
 36 | export class EditorWidget extends Widget {
 37 |     constructor() {
 38 |         super();
 39 |     }
 40 | 
 41 |     static _singleton: EditorWidget;
 42 |     private _tracker: INotebookTracker;
 43 |     private _previous_cell: Cell;
 44 | 
 45 |     static instance() {
 46 |         // Instantiate if necessary
 47 |         if (!EditorWidget._singleton) EditorWidget._singleton = new EditorWidget();
 48 |         return EditorWidget._singleton;
 49 |     }
 50 | 
 51 |     get tracker(): INotebookTracker {
 52 |         return this._tracker;
 53 |     }
 54 | 
 55 |     set tracker(tracker: INotebookTracker) {
 56 |         this._tracker = tracker;
 57 |     }
 58 | 
 59 |     get previous_cell(): Cell {
 60 |         return this._previous_cell;
 61 |     }
 62 | 
 63 |     set previous_cell(_previous_cell: Cell) {
 64 |         this._previous_cell = _previous_cell;
 65 |     }
 66 | 
 67 |     no_side_button(): boolean {
 68 |         return !this.sidebar(this._tracker.activeCell.node)?.querySelector('.jp-RenderButton');
 69 |     }
 70 | 
 71 |     render_side_button() {
 72 |         const sidebar = this.sidebar(this._tracker.activeCell.node);
 73 |         const run_button = this.run_button(this._tracker.activeCell, this._tracker.currentWidget);
 74 |         sidebar.append(run_button);
 75 |     }
 76 | 
 77 |     remove_side_button() {
 78 |         if (this._previous_cell) {
 79 |             const sidebar = this.sidebar(this._previous_cell.node);
 80 |             sidebar.querySelector(".jp-RenderButton")?.remove();
 81 |         }
 82 |         this._previous_cell = this._tracker.activeCell;
 83 |     }
 84 | 
 85 |     sidebar(node: HTMLElement) {
 86 |         return node.closest('.jp-Cell')?.querySelector('.jp-InputArea-prompt');
 87 |     }
 88 | 
 89 |     run_button(cell: Cell, panel: NotebookPanel) {
 90 |         const button = document.createElement("button");
 91 |         button.classList.add("jp-ToolbarButtonComponent", "jp-Button", "jp-RenderButton");
 92 |         button.setAttribute("title", "Render this cell");
 93 |         button.innerHTML = runIcon.svgstr;
 94 |         button.addEventListener("click", () => {
 95 |             panel.content.select(cell);
 96 |             setTimeout(() => void NotebookActions.runAndAdvance(panel.content, panel.sessionContext), 200);
 97 |         });
 98 |         return button;
 99 |     }
100 | }


--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Regents of the University of California & the Broad Institute
2 | // Distributed under the terms of the Modified BSD License.
3 | 
4 | export * from './version';
5 | export * from './factory';
6 | export * from './editor';


--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
 1 | import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application';
 2 | import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
 3 | import { MarkdownCell } from '@jupyterlab/cells';
 4 | import { IStateDB } from '@jupyterlab/statedb';
 5 | import { EditorContentFactory, IEditorContentFactory, EditorWidget } from './factory';
 6 | import "../style/index.css";
 7 | 
 8 | /**
 9 |  * The jupyter-wysiwyg plugins
10 |  */
11 | const wysiwyg_plugin: JupyterFrontEndPlugin = {
12 |     id: "@g2nb/jupyter-wysiwyg:plugin",
13 |     requires: [INotebookTracker, IStateDB],
14 |     activate: activate_editor,
15 |     autoStart: true
16 | };
17 | 
18 | const add_wysiwyg: JupyterFrontEndPlugin = {
19 |     id: "@g2nb/jupyter-wysiwyg:add-wysiwyg",
20 |     provides: NotebookPanel.IContentFactory,
21 |     activate: override_editor,
22 |     autoStart: true
23 | };
24 | 
25 | function activate_editor(app: JupyterFrontEnd, tracker: INotebookTracker, state: IStateDB) {
26 |     console.log('jupyter-wysiwyg plugin activated!');
27 |     EditorWidget.instance().tracker = tracker;
28 | 
29 |     // When the current notebook is changed
30 |     tracker.currentChanged.connect(() => {
31 |         if (!tracker.currentWidget) return; // If no current notebook, do nothing
32 | 
33 |         // When the cell is changed
34 |         tracker.activeCellChanged.connect(() => {
35 |             const active_cell = tracker.activeCell;
36 |             if (active_cell instanceof MarkdownCell && !active_cell.rendered && EditorWidget.instance().no_side_button()) {
37 |                 active_cell.editor.focus();
38 |                 EditorWidget.instance().render_side_button();
39 |                 EditorWidget.instance().remove_side_button();
40 |             }
41 |             else {
42 |                 EditorWidget.instance().remove_side_button();
43 |             }
44 |         });
45 |     });
46 | }
47 | 
48 | function override_editor(app: JupyterFrontEnd):EditorContentFactory {
49 |     console.log('jupyter-wysiwyg override activated!');
50 |     return new EditorContentFactory();
51 | }
52 | 
53 | export default [wysiwyg_plugin, add_wysiwyg];


--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------
 1 | const data = require('../package.json');
 2 | 
 3 | /**
 4 |  * The html widget manager assumes that this is the same as the npm package
 5 |  * version number.
 6 |  */
 7 | export const MODULE_VERSION = data.version;
 8 | 
 9 | /*
10 |  * The current package name.
11 |  */
12 | export const MODULE_NAME = data.name;
13 | 


--------------------------------------------------------------------------------
/style/index.css:
--------------------------------------------------------------------------------
 1 | .jp-TinyMCE {
 2 |     padding-right: 0;
 3 | }
 4 | 
 5 | .jp-TinyMCE > .tox-tinymce {
 6 |     margin-bottom: 0;
 7 | }
 8 | 
 9 | .jp-RenderButton {
10 |     cursor: pointer;
11 | }
12 | 
13 | .jp-RenderButton:hover {
14 |     background-color: var(--jp-layout-color2);
15 | }


--------------------------------------------------------------------------------
/style/index.js:
--------------------------------------------------------------------------------
1 | import './index.css';
2 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "allowSyntheticDefaultImports": true,
 4 |     "composite": true,
 5 |     "declaration": true,
 6 |     "esModuleInterop": true,
 7 |     "incremental": true,
 8 |     "jsx": "react",
 9 |     "module": "esnext",
10 |     "moduleResolution": "node",
11 |     "noEmitOnError": true,
12 |     "noImplicitAny": true,
13 |     "noUnusedLocals": true,
14 |     "preserveWatchOutput": true,
15 |     "resolveJsonModule": true,
16 |     "outDir": "lib",
17 |     "rootDir": "src",
18 |     "skipLibCheck": true,
19 |     "strict": true,
20 |     "strictNullChecks": false,
21 |     "target": "es2017",
22 |     "types": ["node"]
23 |   },
24 |   "include": ["src/*"]
25 | }
26 | 


--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
 1 | const path = require('path');
 2 | const webpack = require('webpack');
 3 | const version = require('./package.json').version;
 4 | 
 5 | // Custom webpack rules
 6 | const rules = [
 7 |   { test: /\.ts$/, loader: 'ts-loader' },
 8 |   { test: /\.js$/, loader: 'source-map-loader' },
 9 |   { test: /\.css$/i, use: ['style-loader', 'css-loader'] },
10 |   { test: /\.(png|svg|jpg)$/i, use: ['file-loader'] }
11 | ];
12 | 
13 | // Packages that shouldn't be bundled but loaded at runtime
14 | const externals = [];
15 | 
16 | const resolve = {
17 |   // Add '.ts' and '.tsx' as resolvable extensions.
18 |   extensions: [".webpack.js", ".web.js", ".ts", ".js", ".css"]
19 | };
20 | 
21 | module.exports = [
22 |   /**
23 |    * Documentation widget bundle
24 |    *
25 |    * This bundle is used to embed widgets in the package documentation.
26 |    */
27 |   {
28 |     entry: './src/index.ts',
29 |     target: 'web',
30 |     output: {
31 |       filename: 'embed-bundle.js',
32 |       path: path.resolve(__dirname, 'docs', 'source', '_static'),
33 |       library: "@g2nb/jupyter-wysiwyg",
34 |       libraryTarget: 'amd',
35 |       // TODO: Replace after release to unpkg.org
36 |       publicPath: '' // 'https://unpkg.com/@g2nb/jupyter-wysiwyg@' + version + '/dist/'
37 |     },
38 |     module: {
39 |       rules: rules
40 |     },
41 |     devtool: 'source-map',
42 |     externals,
43 |     resolve,
44 |     plugins: [
45 |         new webpack.ProvidePlugin({
46 |             process: 'process/browser',
47 |         }),
48 |     ],
49 |   }
50 | 
51 | ];
52 | 


--------------------------------------------------------------------------------