├── .gitattributes ├── README.md ├── .readthedocs.yml ├── docs ├── convert_json_to_toml.py ├── conf.py ├── index.md ├── example_md.md └── example_rst.rst ├── tox.ini ├── .pre-commit-config.yaml ├── pyproject.toml ├── LICENSE ├── tests └── test_basic.py ├── .gitignore └── src └── sphinx_pyscript.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sphinx-pyscript 2 | 3 | This is a Sphinx extension that allows you to use [PyScript](https://docs.pyscript.net) in your documentation. 4 | 5 | See the documentation for details. 6 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.8" 7 | 8 | python: 9 | install: 10 | - method: pip 11 | path: . 12 | extra_requirements: 13 | - docs 14 | 15 | sphinx: 16 | builder: html 17 | fail_on_warning: true 18 | -------------------------------------------------------------------------------- /docs/convert_json_to_toml.py: -------------------------------------------------------------------------------- 1 | from js import document 2 | 3 | input_textarea = document.querySelector("form textarea#input_text") 4 | output_textarea = document.querySelector("form textarea#output_text") 5 | 6 | 7 | def do_convert(event): 8 | result = event.srcElement.value.replace("a", "b") 9 | output_textarea.value = result 10 | 11 | 12 | input_textarea.oninput = do_convert 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py38 3 | 4 | [testenv] 5 | usedevelop = true 6 | 7 | [testenv:py{38,39,310,311}] 8 | extras = 9 | testing 10 | commands = pytest {posargs} 11 | 12 | [testenv:docs] 13 | extras = 14 | docs 15 | whitelist_externals = 16 | rm 17 | echo 18 | commands = 19 | rm -rf docs/_build/html 20 | sphinx-build -nW --keep-going -b html docs/ docs/_build/html 21 | commands_post = echo "open file://{toxinidir}/docs/_build/html/index.html" 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.5.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | 11 | - repo: https://github.com/psf/black 12 | rev: 23.12.1 13 | hooks: 14 | - id: black 15 | 16 | - repo: https://github.com/pycqa/isort 17 | rev: 5.13.2 18 | hooks: 19 | - id: isort 20 | 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: v0.1.11 23 | hooks: 24 | - id: ruff 25 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from sphinx_pyscript import __version__ 4 | 5 | # -- Project information ----------------------------------------------------- 6 | 7 | project = "Sphinx PyScript" 8 | version = __version__ 9 | copyright = f"{date.today().year}, Chris Sewell" 10 | author = "Chris Sewell" 11 | 12 | # -- General configuration --------------------------------------------------- 13 | 14 | extensions = [ 15 | "myst_parser", 16 | "sphinx_pyscript", 17 | ] 18 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 19 | myst_enable_extensions = ["deflist"] 20 | 21 | # -- HTML output ------------------------------------------------- 22 | 23 | html_theme = "furo" 24 | html_theme_options = { 25 | "source_repository": "https://github.com/chrisjsewell/sphinx-pyscript/", 26 | "source_branch": "main", 27 | "source_directory": "docs/", 28 | } 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "sphinx_pyscript" 7 | authors = [{name = "Chris Sewell", email = "chrisj_sewell@hotmail.com"}] 8 | readme = "README.md" 9 | license = {file = "LICENSE"} 10 | classifiers = [ 11 | "Development Status :: 4 - Beta", 12 | "Framework :: Sphinx :: Extension", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Programming Language :: Python :: 3", 16 | ] 17 | keywords = ["sphinx", "pyscript"] 18 | dynamic = ["version", "description"] 19 | requires-python = ">=3.7" 20 | dependencies = [ 21 | "sphinx>4", 22 | "pyyaml", 23 | ] 24 | 25 | [project.urls] 26 | Home = "https://github.com/chrisjsewell/sphinx-pyscript" 27 | Documentation = "https://sphinx-pyscript.readthedocs.io" 28 | 29 | [project.optional-dependencies] 30 | testing = [ 31 | "pytest", 32 | "sphinx-pytest", 33 | ] 34 | docs = [ 35 | "myst-parser", 36 | "furo", 37 | ] 38 | 39 | [tool.isort] 40 | profile = "black" 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chris Sewell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | from sphinx_pytest.plugin import CreateDoctree 2 | 3 | 4 | def test_basic(sphinx_doctree: CreateDoctree): 5 | sphinx_doctree.set_conf({"extensions": ["sphinx_pyscript"]}) 6 | sphinx_doctree.buildername = "html" 7 | result = sphinx_doctree( 8 | """ 9 | Test 10 | ---- 11 | 12 | .. py-config:: 13 | 14 | splashscreen: 15 | autoclose: true 16 | 17 | .. py-repl:: 18 | :output: replOutput 19 | 20 | .. py-terminal:: 21 | 22 | .. py-script:: 23 | 24 | print("Hello World") 25 | 26 | """ 27 | ) 28 | assert ( 29 | [li.rstrip() for li in result.pformat().strip().splitlines()] 30 | == """ 31 | 32 |
33 | 34 | Test 35 | <raw format="html" xml:space="preserve"> 36 | <py-repl output="replOutput"> 37 | 38 | </py-repl> 39 | <raw format="html" xml:space="preserve"> 40 | <py-terminal></py-terminal> 41 | <raw format="html" xml:space="preserve"> 42 | <py-script> 43 | print("Hello World") 44 | </py-script> 45 | <raw format="html" xml:space="preserve"> 46 | <py-config type="json"> 47 | { 48 | "splashscreen": { 49 | "autoclose": true 50 | } 51 | } 52 | </py-config> 53 | """.strip().splitlines() 54 | ) 55 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Sphinx PyScript 2 | 3 | [![GitHub Repo stars](https://img.shields.io/github/stars/chrisjsewell/sphinx-pyscript?label=Like%20and%20Share%21&style=social)](https://github.com/chrisjsewell/sphinx-pyscript) 4 | 5 | This is a Sphinx extension that allows you to use [PyScript](https://docs.pyscript.net) in your documentation. 6 | 7 | ```{toctree} 8 | :hidden: 9 | example_md 10 | example_rst 11 | ``` 12 | 13 | ## Installation 14 | 15 | Install with pip: 16 | 17 | ```bash 18 | pip install sphinx-pyscript 19 | ``` 20 | 21 | ## Usage 22 | 23 | Add the extension to your `conf.py`: 24 | 25 | ```python 26 | extensions = [ 27 | "sphinx_pyscript", 28 | ] 29 | ``` 30 | 31 | To load PyScript on a page, either use the `py-config` directive to load the [config](https://docs.pyscript.net/latest/reference/elements/py-config.html#) in YAML format: 32 | 33 | ```restructuredtext 34 | .. py-config:: 35 | 36 | splashscreen: 37 | autoclose: true 38 | packages: 39 | - matplotlib 40 | ``` 41 | 42 | or with MyST, use the top-matter: 43 | 44 | ```yaml 45 | --- 46 | py-config: 47 | splashscreen: 48 | autoclose: true 49 | packages: 50 | - matplotlib 51 | --- 52 | ``` 53 | 54 | See the examples for more details. 55 | 56 | ## Configuration 57 | 58 | The extension has the following configuration options: 59 | 60 | pyscript_js 61 | : The URL for the PyScript JavaScript file 62 | 63 | pyscript_css 64 | : The URL for the PyScript CSS file 65 | -------------------------------------------------------------------------------- /docs/example_md.md: -------------------------------------------------------------------------------- 1 | --- 2 | py-config: 3 | splashscreen: 4 | autoclose: true 5 | packages: 6 | - matplotlib 7 | --- 8 | 9 | # Example with MyST 10 | 11 | ## `py-repl` and `py-terminal` 12 | 13 | We can create a REPL which will output to a `div` and print `stdout` to a terminal with: 14 | 15 | ````md 16 | ```{py-repl} 17 | :output: replOutput 18 | 19 | print("hallo world") 20 | import matplotlib.pyplot as plt 21 | plt.plot([1, 2, 3]) 22 | plt.gcf() 23 | ``` 24 | 25 | <div id="replOutput"></div> 26 | 27 | ```{py-terminal} 28 | ``` 29 | ```` 30 | 31 | Press `shift+enter` to run the code. 32 | 33 | ```{py-repl} 34 | :output: replOutput 35 | 36 | print("hallo world") 37 | import matplotlib.pyplot as plt 38 | plt.plot([1, 2, 3]) 39 | plt.gcf() 40 | ``` 41 | 42 | <div id="replOutput"></div> 43 | 44 | ```{py-terminal} 45 | ``` 46 | 47 | ## `py-script` application 48 | 49 | Here is a simple application to replace "a" with "b", using the `py-script` directive: 50 | 51 | ````md 52 | ```{py-script} 53 | :file: convert_json_to_toml.py 54 | ``` 55 | 56 | <form method="post"> 57 | <label for="input_text" style="display: block">Input</label> 58 | <textarea id="input_text" name="input_text" style="width: 90%">a</textarea> 59 | <label for="output_text" style="display: block">Output</label> 60 | <textarea id="output_text" name="output_text" readonly="true" style="width: 90%">b</textarea> 61 | </form> 62 | ```` 63 | 64 | with the following code: 65 | 66 | ```{literalinclude} convert_json_to_toml.py 67 | :language: python 68 | ``` 69 | 70 | ```{py-script} 71 | :file: convert_json_to_toml.py 72 | ``` 73 | 74 | <form method="post"> 75 | <label for="input_text" style="display: block">Input</label> 76 | <textarea id="input_text" name="input_text" style="width: 90%">a</textarea> 77 | <label for="output_text" style="display: block">Output</label> 78 | <textarea id="output_text" name="output_text" readonly="true" style="width: 90%">b</textarea> 79 | </form> 80 | -------------------------------------------------------------------------------- /docs/example_rst.rst: -------------------------------------------------------------------------------- 1 | .. py-config:: 2 | 3 | splashscreen: 4 | autoclose: true 5 | packages: 6 | - matplotlib 7 | 8 | Example with RST 9 | ================ 10 | 11 | `py-repl` and `py-terminal` 12 | ---------------------------- 13 | 14 | We can create a REPL which will output to a `div` and print `stdout` to a terminal with: 15 | 16 | .. code-block:: restructuredtext 17 | 18 | .. py-repl:: 19 | :output: replOutput 20 | 21 | print("hallo world") 22 | import matplotlib.pyplot as plt 23 | plt.plot([1, 2, 3]) 24 | plt.gcf() 25 | 26 | .. raw:: html 27 | 28 | <div id="replOutput"></div> 29 | 30 | .. py-terminal:: 31 | 32 | Press `shift+enter` to run the code. 33 | 34 | .. py-repl:: 35 | :output: replOutput 36 | 37 | print("hallo world") 38 | import matplotlib.pyplot as plt 39 | plt.plot([1, 2, 3]) 40 | plt.gcf() 41 | 42 | .. raw:: html 43 | 44 | <div id="replOutput"></div> 45 | 46 | .. py-terminal:: 47 | 48 | `py-script` application 49 | ----------------------- 50 | 51 | Here is a simple application to replace "a" with "b", using the `py-script` directive: 52 | 53 | .. code-block:: restructuredtext 54 | 55 | .. py-script:: 56 | :file: convert_json_to_toml.py 57 | 58 | .. raw:: html 59 | 60 | <form method="post"> 61 | <label for="input_text" style="display: block">Input</label> 62 | <textarea id="input_text" name="input_text" style="width: 90%">a</textarea> 63 | <label for="output_text" style="display: block">Output</label> 64 | <textarea id="output_text" name="output_text" readonly="true" style="width: 90%">b</textarea> 65 | </form> 66 | 67 | With the following code: 68 | 69 | .. literalinclude:: convert_json_to_toml.py 70 | :language: python 71 | 72 | .. py-script:: 73 | :file: convert_json_to_toml.py 74 | 75 | .. raw:: html 76 | 77 | <form method="post"> 78 | <label for="input_text" style="display: block">Input</label> 79 | <textarea id="input_text" name="input_text" style="width: 90%">a</textarea> 80 | <label for="output_text" style="display: block">Output</label> 81 | <textarea id="output_text" name="output_text" readonly="true" style="width: 90%">b</textarea> 82 | </form> 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /src/sphinx_pyscript.py: -------------------------------------------------------------------------------- 1 | """A sphinx extension for adding pyscript to a page""" 2 | 3 | __version__ = "0.1.0" 4 | 5 | import json 6 | from pathlib import Path 7 | 8 | import yaml 9 | from docutils import nodes 10 | from docutils.parsers.rst import directives 11 | from sphinx.application import Sphinx 12 | from sphinx.util.docutils import SphinxDirective 13 | from sphinx.util.logging import getLogger 14 | 15 | 16 | def setup(app: Sphinx): 17 | """Setup the extension""" 18 | app.add_config_value( 19 | "pyscript_js", "https://pyscript.net/releases/2022.12.1/pyscript.js", "env" 20 | ) 21 | app.add_config_value( 22 | "pyscript_css", "https://pyscript.net/releases/2022.12.1/pyscript.css", "env" 23 | ) 24 | app.add_directive("py-config", PyConfig) 25 | app.add_directive("py-script", PyScript) 26 | app.add_directive("py-repl", PyRepl) 27 | app.add_directive("py-terminal", PyTerminal) 28 | app.connect("doctree-read", doctree_read) 29 | app.connect("html-page-context", add_html_context) 30 | return {"version": __version__, "parallel_read_safe": True} 31 | 32 | 33 | LOGGER = getLogger(__name__) 34 | 35 | 36 | class PyConfig(SphinxDirective): 37 | """Parse the py-config as a directive""" 38 | 39 | has_content = True 40 | 41 | def run(self): 42 | """Parse the config""" 43 | if self.content: 44 | try: 45 | config = yaml.safe_load("\n".join(self.content)) 46 | except Exception: 47 | raise self.error("Could not read config as YAML") 48 | else: 49 | config = {} 50 | self.env.metadata[self.env.docname]["py-config"] = json.dumps(config) 51 | return [] 52 | 53 | 54 | class PyScript(SphinxDirective): 55 | """Add a py-script tag""" 56 | 57 | has_content = True 58 | option_spec = { 59 | "file": directives.path, 60 | } 61 | 62 | def run(self): 63 | """Add the py-script tag""" 64 | if "file" in self.options: 65 | path = Path(self.env.relfn2path(self.options["file"])[1]) 66 | try: 67 | code = path.read_text(encoding="utf8") 68 | except Exception as exc: 69 | raise self.error(f"Could not read file: {exc}") 70 | self.env.note_dependency(path) 71 | elif self.content: 72 | code = "\n".join(self.content) 73 | else: 74 | raise self.error("Must provide either content or the 'file' option") 75 | return [nodes.raw("", f"<py-script>\n{code}\n</py-script>\n", format="html")] 76 | 77 | 78 | class PyRepl(SphinxDirective): 79 | """Add a py-repl tag""" 80 | 81 | has_content = True 82 | option_spec = { 83 | "auto-generate": directives.flag, 84 | "output": directives.unchanged, 85 | } 86 | 87 | def run(self): 88 | """Add the py-repl tag""" 89 | attrs = "" 90 | code = "" 91 | if "auto-generate" in self.options: 92 | attrs += ' auto-generate="true"' 93 | if "output" in self.options: 94 | attrs += f' output="{self.options["output"]}"' 95 | if self.content: 96 | code = "\n".join(self.content) 97 | return [nodes.raw("", f"<py-repl{attrs}>\n{code}\n</py-repl>\n", format="html")] 98 | 99 | 100 | class PyTerminal(SphinxDirective): 101 | """Add a py-terminal tag""" 102 | 103 | option_spec = { 104 | "auto": directives.flag, 105 | } 106 | 107 | def run(self): 108 | """Add the py-terminal tag""" 109 | attrs = "" 110 | if "auto" in self.options: 111 | attrs += " auto" 112 | return [nodes.raw("", f"<py-terminal{attrs}></py-terminal>\n", format="html")] 113 | 114 | 115 | def add_html_context( 116 | app: Sphinx, pagename: str, templatename: str, context, doctree: nodes.document 117 | ): 118 | """Add extra variables to the HTML template context.""" 119 | if doctree and "pyscript" in doctree: 120 | app.add_js_file(app.config.pyscript_js, loading_method="defer") 121 | app.add_css_file(app.config.pyscript_css) 122 | 123 | 124 | def doctree_read(app: Sphinx, doctree: nodes.document): 125 | """Setup the doctree.""" 126 | metadata = app.env.metadata[app.env.docname] 127 | if "py-config" in metadata: 128 | try: 129 | data = json.loads(metadata["py-config"]) 130 | assert isinstance(data, dict), "must be a dictionary" 131 | except Exception as exc: 132 | LOGGER.warning( 133 | f"Could not parse pyscript config: {exc}", location=(app.env.docname, 0) 134 | ) 135 | else: 136 | doctree["pyscript"] = True 137 | data_str = json.dumps(data, indent=2) 138 | doctree.append( 139 | nodes.raw( 140 | "", 141 | f'<py-config type="json">\n{data_str}\n</py-config>\n', 142 | format="html", 143 | ) 144 | ) 145 | --------------------------------------------------------------------------------