├── .circleci └── config.yml ├── .flake8 ├── .github └── workflows │ ├── artifacts.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── .scss-lint.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── _static │ ├── ebp-logo.png │ ├── footer-banner.jpg │ └── sphinx-logo.png ├── conf.py └── index.rst ├── git_rebase_theme_branches.sh ├── setup.py ├── sphinx_panels ├── __init__.py ├── _css │ ├── __init__.py │ ├── panels-bootstrap.5fd3999ee7762ccc51105388f4a9d115.css │ └── panels-main.c949a650a448cc0ae9fd3441c0e17fb0.css ├── button.py ├── data │ ├── LICENSE │ └── opticons.json ├── dropdown.py ├── icons.py ├── panels.py ├── scss │ ├── bootstrap │ │ ├── _badge.scss │ │ ├── _borders.scss │ │ ├── _buttons.scss │ │ ├── _cards.scss │ │ ├── _colors.scss │ │ ├── _grids.scss │ │ ├── _overrides.scss │ │ └── index.scss │ └── panels │ │ ├── _dropdown.scss │ │ ├── _icons.scss │ │ ├── _tabs.scss │ │ └── index.scss ├── tabs.py └── utils.py ├── tests ├── conftest.py ├── sources │ ├── dropdown_basic │ │ ├── conf.py │ │ └── index.rst │ └── tabbed_basic │ │ ├── conf.py │ │ └── index.rst ├── test_icons.py ├── test_panels.py ├── test_sphinx.py ├── test_sphinx │ ├── test_sources_dropdown_basic_.xml │ └── test_sources_tabbed_basic_.xml └── test_utils.py ├── tox.ini └── web-compile-config.yml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | docs_sphinx_book_theme: 4 | docker: 5 | - image: circleci/python:3.6-stretch 6 | steps: 7 | # Get our data and merge with upstream 8 | - run: sudo apt-get update 9 | - checkout 10 | 11 | - restore_cache: 12 | keys: 13 | - cache-pip 14 | 15 | - run: pip install --user .[themes] 16 | 17 | - save_cache: 18 | key: cache-pip 19 | paths: 20 | - ~/.cache/pip 21 | 22 | # Build the docs 23 | - run: 24 | name: Build docs to store 25 | command: | 26 | export HTML_THEME=sphinx_book_theme 27 | sphinx-build -W -b html docs docs/_build/html 28 | - store_artifacts: 29 | path: docs/_build/html/ 30 | destination: html 31 | 32 | 33 | workflows: 34 | version: 2 35 | default: 36 | jobs: 37 | - docs_sphinx_book_theme 38 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore=E203,W503 4 | -------------------------------------------------------------------------------- /.github/workflows/artifacts.yml: -------------------------------------------------------------------------------- 1 | on: [status] 2 | 3 | jobs: 4 | circleci_artifacts_redirector_job: 5 | runs-on: ubuntu-latest 6 | name: Run CircleCI artifacts redirector 7 | steps: 8 | - name: GitHub Action step 9 | uses: larsoner/circleci-artifacts-redirector-action@master 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | artifact-path: 0/html/index.html 13 | circleci-job: docs_sphinx_book_theme 14 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: continuous-integration 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: 7 | - 'v*' 8 | pull_request: 9 | 10 | jobs: 11 | 12 | pre-commit: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v1 20 | with: 21 | python-version: 3.9 22 | - uses: pre-commit/action@v2.0.0 23 | 24 | tests: 25 | 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | python-version: [3.6, 3.7, 3.8, 3.9] 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Set up Python ${{ matrix.python-version }} 34 | uses: actions/setup-python@v1 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install .[testing] 41 | - name: Run pytest 42 | run: | 43 | pytest 44 | 45 | publish: 46 | 47 | name: Publish to PyPi 48 | needs: [pre-commit, tests] 49 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout source 53 | uses: actions/checkout@v2 54 | - name: Set up Python 3.9 55 | uses: actions/setup-python@v1 56 | with: 57 | python-version: 3.9 58 | - name: Build package 59 | run: | 60 | pip install wheel 61 | python setup.py sdist bdist_wheel 62 | - name: Publish 63 | uses: pypa/gh-action-pypi-publish@v1.1.0 64 | with: 65 | user: __token__ 66 | password: ${{ secrets.PYPI_KEY }} 67 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .DS_Store 132 | .vscode/ 133 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Install pre-commit hooks via 2 | # pre-commit install 3 | 4 | exclude: > 5 | (?x)^( 6 | \.vscode/settings\.json 7 | )$ 8 | 9 | repos: 10 | 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v2.2.3 13 | hooks: 14 | - id: check-json 15 | - id: check-yaml 16 | - id: end-of-file-fixer 17 | - id: trailing-whitespace 18 | - id: flake8 19 | 20 | - repo: https://github.com/pre-commit/mirrors-scss-lint 21 | rev: v0.59.0 22 | hooks: 23 | - id: scss-lint 24 | 25 | - repo: https://github.com/mgedmin/check-manifest 26 | rev: "0.39" 27 | hooks: 28 | - id: check-manifest 29 | 30 | - repo: https://github.com/psf/black 31 | rev: "22.3.0" 32 | hooks: 33 | - id: black 34 | 35 | - repo: https://github.com/executablebooks/web-compile 36 | rev: v0.2.2 37 | hooks: 38 | - id: web-compile 39 | files: >- 40 | (?x)^( 41 | web-compile-config.yml| 42 | sphinx_panels/scss/.*| 43 | sphinx_panels/_css/.* 44 | )$ 45 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | python: 3 | version: 3 4 | install: 5 | - method: pip 6 | path: . 7 | extra_requirements: 8 | - themes 9 | 10 | sphinx: 11 | builder: html 12 | fail_on_warning: true 13 | -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | ImportantRule: 3 | enabled: false 4 | NestingDepth: 5 | enabled: true 6 | max_depth: 4 7 | SelectorDepth: 8 | enabled: true 9 | max_depth: 4 10 | QualifyingElement: 11 | enabled: false 12 | VendorPrefix: 13 | enabled: false 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.6.0 - 2021-06-03 4 | 5 | ⬆️ UPGRADE: Unpin sphinx v4 6 | 7 | 👌 IMPROVE: specify post-transforms by format: 8 | This applies them to the "html" format, rathther than a subset of diretive html builders. 9 | 10 | ## v0.5.2 - 2020-10-12 11 | 12 | ‼️ Deprecate `panels_add_boostrap_css` config, the typo here (no T!) has now been fixed to `panels_add_bootstrap_css`. 13 | Use of the former will now emit a deprecation warning. 14 | 15 | ## v0.5.1 - 2020-09-22 16 | 17 | 👌 IMPROVE: Make default label font-size configurable for `tabbed` components. 18 | See the `panels_css_variables` [in this documentation section](https://sphinx-panels.readthedocs.io/en/latest/#tabbed-content). 19 | 20 | ## v0.5.0 - 2020-09-15 21 | 22 | ✨ NEW: Add `tabbed` directive, to create tab groups! 23 | See [this documentation section](https://sphinx-panels.readthedocs.io/en/latest/#tabbed-content). 24 | 25 | ♻️ REFACTOR: Move from CSS to SCSS: 26 | Under the hood, sphinx-panels now utilises CSS compiled from source SCSS, 27 | allowing for a better development environment. 28 | The CSS files are also "hashed", to ensure that documentation using sphinx-panels will not show 29 | old, cached CSS stylings after future updates to sphinx-panels. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Executable Books 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude docs 2 | recursive-exclude docs * 3 | exclude tests 4 | recursive-exclude tests * 5 | exclude .circleci 6 | recursive-exclude .circleci * 7 | recursive-exclude ** __pycache__ 8 | recursive-exclude **/__pycache__ * 9 | 10 | exclude .pre-commit-config.yaml 11 | exclude .readthedocs.yml 12 | exclude .flake8 13 | exclude .scss-lint.yml 14 | exclude tox.ini 15 | exclude git_rebase_theme_branches.sh 16 | 17 | include LICENSE 18 | include README.md 19 | include CHANGELOG.md 20 | include web-compile-config.yml 21 | 22 | recursive-include sphinx_panels/scss * 23 | recursive-include sphinx_panels/_css * 24 | recursive-include sphinx_panels/data * 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sphinx-panels 2 | 3 | [![Doc Status][rtd-badge]][rtd-link] 4 | [![Code style: black][black-badge]][black-link] 5 | [![PyPI][pypi-badge]][pypi-link] 6 | 7 | 🚨This repository is not actively maintained. Use [`sphinx-design`](https://github.com/executablebooks/sphinx-design) instead! See [the migration guide](https://sphinx-design.readthedocs.io/en/latest/get_started.html#migrating-from-sphinx-panels) and [this github issue](https://github.com/executablebooks/sphinx-design/issues/51) for more information.🚨 8 | 9 | A sphinx extension for creating document components optimised for HTML+CSS. 10 | 11 | - The `panels` directive creates panels of content in a grid layout, utilising both the Bootstrap 4 [grid system](https://getbootstrap.com/docs/4.0/layout/grid/), and [cards layout](https://getbootstrap.com/docs/4.0/components/card/). 12 | 13 | - The `link-button` directive creates a click-able button, linking to a URL or reference, and can also be used to make an entire panel click-able. 14 | 15 | - The `dropdown` directive creates toggle-able content. 16 | 17 | - The `tabbed` directive creates tabbed content. 18 | 19 | - `opticon` and `fa` (fontawesome) roles allow for inline icons to be added. 20 | 21 | 22 | ```rst 23 | .. panels:: 24 | 25 | Content of the top-left panel 26 | 27 | --- 28 | 29 | Content of the top-right panel 30 | 31 | --- 32 | 33 | Content of the bottom-left panel 34 | 35 | --- 36 | 37 | Content of the bottom-right panel 38 | ``` 39 | 40 | The `link-button` directive can be used to create buttons, which link to a URL (default) or reference. 41 | They can be styled by [Bootstrap button classes](https://getbootstrap.com/docs/4.0/components/buttons/): 42 | 43 | ```rst 44 | .. panels:: 45 | 46 | .. link-button:: https://example.com 47 | :type: url 48 | :tooltip: hallo 49 | :classes: btn-success 50 | 51 | --- 52 | 53 | This entire panel is clickable. 54 | 55 | +++ 56 | 57 | .. link-button:: panels/usage 58 | :type: ref 59 | :text: Go To Reference 60 | :classes: btn-outline-primary btn-block stretched-link 61 | ``` 62 | 63 | The `dropdown` directive combines a [Bootstrap card](https://getbootstrap.com/docs/4.0/components/card/) 64 | with the [HTML details tag](https://www.w3schools.com/tags/tag_details.asp) to create a collapsible 65 | drop-down panel. 66 | 67 | ```rst 68 | .. dropdown:: Click on me to see my content! 69 | 70 | I'm the content which can be anything: 71 | 72 | .. link-button:: https://example.com 73 | :text: Like a Button 74 | :classes: btn-primary 75 | ``` 76 | 77 | ## Development 78 | 79 | To run the tests: 80 | 81 | ```console 82 | pip install tox 83 | tox -e py37-sphinx3 84 | ``` 85 | 86 | To test building the docs: 87 | 88 | ```console 89 | tox -e docs-clean html 90 | tox -e docs-rebuild html 91 | ``` 92 | 93 | For live builds of the docs: 94 | 95 | ```console 96 | tox -e docs-live html 97 | ``` 98 | 99 | You can also build the docs in different themes, by setting `HTML_THEME` to one of `alabaster`, `sphinx_rtd_theme`, `pydata_sphinx_theme`, `sphinx_book_theme`: 100 | 101 | ```console 102 | export HTML_THEME=sphinx_book_theme 103 | tox -e docs-live 104 | ``` 105 | 106 | For code style and SCSS -> CSS updating: 107 | 108 | ```console 109 | pip install pre-commit 110 | pre-commit run --all 111 | ``` 112 | 113 | [rtd-badge]: https://readthedocs.org/projects/sphinx-panels/badge/?version=latest 114 | [rtd-link]: https://sphinx-panels.readthedocs.io/en/latest/?badge=latest 115 | [black-badge]: https://img.shields.io/badge/code%20style-black-000000.svg 116 | [black-link]: https://github.com/ambv/black 117 | [pypi-badge]: https://img.shields.io/pypi/v/sphinx-panels.svg 118 | [pypi-link]: https://pypi.org/project/sphinx-panels 119 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = SphinxCopybutton 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | clean: 23 | rm -r $(BUILDDIR) 24 | -------------------------------------------------------------------------------- /docs/_static/ebp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executablebooks/sphinx-panels/44bab2341d97707365396efc0b23a1edac26da7c/docs/_static/ebp-logo.png -------------------------------------------------------------------------------- /docs/_static/footer-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executablebooks/sphinx-panels/44bab2341d97707365396efc0b23a1edac26da7c/docs/_static/footer-banner.jpg -------------------------------------------------------------------------------- /docs/_static/sphinx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executablebooks/sphinx-panels/44bab2341d97707365396efc0b23a1edac26da7c/docs/_static/sphinx-logo.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | import os 10 | from sphinx_panels import __version__ 11 | 12 | # -- Project information ----------------------------------------------------- 13 | 14 | project = "Sphinx-Panels" 15 | copyright = "2020, Executable Books Project" 16 | author = "Chris Sewell" 17 | 18 | # The short X.Y version 19 | version = __version__ 20 | # The full version, including alpha/beta/rc tags 21 | release = __version__ 22 | 23 | # -- General configuration --------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | # 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = ["sphinx_panels"] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ["_templates"] 36 | 37 | # The suffix(es) of source filenames. 38 | # You can specify multiple suffix as a list of string: 39 | # 40 | # source_suffix = ['.rst', '.md'] 41 | source_suffix = ".rst" 42 | 43 | # The master toctree document. 44 | master_doc = "index" 45 | 46 | # The language for content autogenerated by Sphinx. Refer to documentation 47 | # for a list of supported languages. 48 | # 49 | # This is also used if you do content translation via gettext catalogs. 50 | # Usually you set "language" from the command line for these cases. 51 | language = None 52 | 53 | # List of patterns, relative to source directory, that match files and 54 | # directories to ignore when looking for source files. 55 | # This pattern also affects html_static_path and html_extra_path . 56 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 57 | 58 | # The name of the Pygments (syntax highlighting) style to use. 59 | pygments_style = "sphinx" 60 | 61 | # -- Options for HTML output ------------------------------------------------- 62 | 63 | # The theme to use for HTML and HTML Help pages. See the documentation for 64 | # a list of builtin themes. 65 | # 66 | # html_theme = "alabaster" 67 | 68 | theme_name = os.environ.get("HTML_THEME", "sphinx_rtd_theme") 69 | 70 | if theme_name == "sphinx_rtd_theme": 71 | html_theme = "sphinx_rtd_theme" 72 | elif theme_name == "alabaster": 73 | html_theme = "alabaster" 74 | html_css_files = [ 75 | ( 76 | "https://cdnjs.cloudflare.com/ajax/libs/font-awesome" 77 | "/4.7.0/css/font-awesome.min.css" 78 | ) 79 | ] 80 | elif theme_name == "pydata_sphinx_theme": 81 | html_theme = "pydata_sphinx_theme" 82 | panels_add_bootstrap_css = False 83 | elif theme_name == "sphinx_book_theme": 84 | html_theme = "sphinx_book_theme" 85 | panels_add_bootstrap_css = False 86 | html_theme_options = { 87 | "single_page": True, 88 | } 89 | else: 90 | raise ValueError(f"HTML_THEME name not recognised: {theme_name}") 91 | 92 | # ensures all pages are rebuilt if CSS changes 93 | panels_dev_mode = True 94 | 95 | 96 | # -- Options for HTMLHelp output --------------------------------------------- 97 | 98 | # Output file base name for HTML help builder. 99 | htmlhelp_basename = "ExecutableBooksProjectdoc" 100 | 101 | 102 | # -- Options for LaTeX output ------------------------------------------------ 103 | 104 | latex_elements = { 105 | # The paper size ('letterpaper' or 'a4paper'). 106 | # 107 | # 'papersize': 'letterpaper', 108 | # The font size ('10pt', '11pt' or '12pt'). 109 | # 110 | # 'pointsize': '10pt', 111 | # Additional stuff for the LaTeX preamble. 112 | # 113 | # 'preamble': '', 114 | # Latex figure (float) alignment 115 | # 116 | # 'figure_align': 'htbp', 117 | } 118 | 119 | # Grouping the document tree into LaTeX files. List of tuples 120 | # (source start file, target name, title, 121 | # author, documentclass [howto, manual, or own class]). 122 | latex_documents = [ 123 | ( 124 | master_doc, 125 | "ExecutableBooksProject.tex", 126 | "Executable Books Project Documentation", 127 | "Chris Holdgraf", 128 | "manual", 129 | ) 130 | ] 131 | 132 | 133 | # -- Options for manual page output ------------------------------------------ 134 | 135 | # One entry per manual page. List of tuples 136 | # (source start file, name, description, authors, manual section). 137 | man_pages = [ 138 | ( 139 | master_doc, 140 | "ExecutableBooksProject", 141 | "Executable Books Project Documentation", 142 | [author], 143 | 1, 144 | ) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ---------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | ( 155 | master_doc, 156 | "ExecutableBooksProject", 157 | "Executable Books Project Documentation", 158 | author, 159 | "ExecutableBooksProject", 160 | "One line description of project.", 161 | "Miscellaneous", 162 | ) 163 | ] 164 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _panels/usage: 2 | 3 | ============= 4 | sphinx-panels 5 | ============= 6 | 7 | .. warning:: 8 | 9 | This repository is not actively maintained. 10 | Use `sphinx-design `_ instead! 11 | See `the migration guide `_ and `this github issue `_ for more information. 12 | 13 | A sphinx extension for creating panels in a grid layout or as drop-downs. 14 | 15 | - The ``panels`` directive creates panels of content in a grid layout, utilising both the bootstrap 4 16 | `grid system `_, 17 | and `cards layout `_. 18 | - The ``link-button`` directive creates a click-able button, linking to a URL or reference, 19 | and can also be used to make an entire panel click-able. 20 | - The ``dropdown`` directive creates toggle-able content. 21 | - The ``tabbed`` directive creates tabbed content. 22 | - ``opticon`` and ``fa`` roles allow for inline icons to be added. 23 | 24 | .. tabbed:: ReStructuredText 25 | 26 | .. code-block:: rst 27 | 28 | .. panels:: 29 | 30 | Content of the top-left panel 31 | 32 | --- 33 | 34 | Content of the top-right panel 35 | 36 | :badge:`example,badge-primary` 37 | 38 | --- 39 | 40 | .. dropdown:: :fa:`eye,mr-1` Bottom-left panel 41 | 42 | Hidden content 43 | 44 | --- 45 | 46 | .. link-button:: https://example.com 47 | :text: Clickable Panel 48 | :classes: stretched-link 49 | 50 | .. tabbed:: MyST Markdown 51 | 52 | .. code-block:: md 53 | 54 | ````{panels} 55 | Content of the top-left panel 56 | 57 | --- 58 | 59 | Content of the top-right panel 60 | 61 | {badge}`example,badge-primary` 62 | 63 | --- 64 | 65 | ```{dropdown} :fa:`eye,mr-1` Bottom-left panel 66 | Hidden content 67 | ``` 68 | 69 | --- 70 | 71 | ```{link-button} https://example.com 72 | :text: Clickable Panel 73 | :classes: stretched-link 74 | ``` 75 | 76 | ```` 77 | 78 | .. panels:: 79 | 80 | Content of the top-left panel 81 | 82 | --- 83 | 84 | Content of the top-right panel 85 | 86 | :badge:`example,badge-primary` 87 | 88 | --- 89 | 90 | .. dropdown:: :fa:`eye,mr-1` Bottom-left panel 91 | 92 | Hidden content 93 | 94 | --- 95 | 96 | .. link-button:: https://example.com 97 | :text: Clickable Panel 98 | :classes: stretched-link 99 | 100 | .. dropdown:: :fa:`eye,mr-1` See this documentation in other themes 101 | :title: text-info font-weight-bold 102 | 103 | Click the links to see the documentation built with: 104 | 105 | - `alabaster `_ 106 | - `sphinx-rtd-theme `_ 107 | - `sphinx-pydata-theme `_ 108 | - `sphinx-book-theme `_ 109 | 110 | 111 | .. panels:: 112 | :column: col-lg-12 p-0 113 | :header: text-secondary font-weight-bold 114 | 115 | :fa:`arrows-alt,mr-1` Adaptive Sizing 116 | 117 | ^^^ 118 | 119 | Try shrinking the size of this window, 120 | to see how the panels above realign to compensate for small screens. 121 | 122 | .. contents:: 123 | :local: 124 | :depth: 2 125 | 126 | Installation 127 | ============ 128 | 129 | You can install ``sphinx-panels`` with ``pip``: 130 | 131 | .. code-block:: bash 132 | 133 | pip install sphinx-panels 134 | 135 | Sphinx Configuration 136 | ===================== 137 | 138 | In your ``conf.py`` configuration file, simply add ``sphinx_panels`` 139 | to your extensions list, e.g.: 140 | 141 | .. code-block:: python 142 | 143 | extensions = [ 144 | ... 145 | 'sphinx_panels' 146 | ... 147 | ] 148 | 149 | This extension includes the bootstrap 4 CSS classes relevant to panels and loads it by default. 150 | However if you already load your own Bootstrap CSS (e.g., if your theme loads it already), you may choose *not* to add it with ``sphinx-panels``. 151 | To do so, use the following configuration in ``conf.py``: 152 | 153 | .. code-block:: python 154 | 155 | panels_add_bootstrap_css = False 156 | 157 | You can also change the delimiter regexes used by adding ``panel_delimiters`` to your ``conf.py``, 158 | e.g. the default value (panels, header, footer) is: 159 | 160 | .. code-block:: python 161 | 162 | panels_delimiters = (r"^\-{3,}$", r"^\^{3,}$", r"^\+{3,}$") 163 | 164 | .. _components-panels: 165 | 166 | Panels Usage 167 | ============ 168 | 169 | Grid Layout 170 | ----------- 171 | 172 | Panels are split by three or more ``-`` characters. 173 | The layout of panels is then set by using the bootstrap classes. 174 | Default classes for all panels may be set in the directive options, 175 | then panel specific classes can be added at the start of each panel. 176 | 177 | By default the new classes will override those set previously 178 | (as defaults or in the top level options), 179 | but starting the option value with ``+`` will make the classes additive. 180 | For example the following options will set the first panel's card to have both the ``shadow`` and ``bg-info`` classes: 181 | 182 | .. code-block:: rst 183 | 184 | .. panels:: 185 | :card: shadow 186 | 187 | --- 188 | :card: + bg-info 189 | 190 | .. seealso:: 191 | 192 | The bootstrap 4 `grid documentation `_, 193 | and this `grid tutorial `_ 194 | 195 | .. note:: 196 | 197 | The default classes are: 198 | 199 | .. code-block:: rst 200 | 201 | .. panels:: 202 | :container: container pb-4 203 | :column: col-lg-6 col-md-6 col-sm-6 col-xs-12 p-2 204 | :card: shadow 205 | 206 | .. code-block:: rst 207 | 208 | .. panels:: 209 | :container: container-lg pb-3 210 | :column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2 211 | 212 | panel1 213 | --- 214 | panel2 215 | --- 216 | panel3 217 | --- 218 | :column: col-lg-12 p-2 219 | panel4 220 | 221 | .. panels:: 222 | :container: container-lg pb-3 223 | :column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2 224 | 225 | panel1 226 | --- 227 | panel2 228 | --- 229 | panel3 230 | --- 231 | :column: col-lg-12 p-2 232 | panel4 233 | 234 | Card Layout 235 | ----------- 236 | 237 | Each panel contains a card, which can itself contain a header and/or footer, 238 | split by three or more ``^^^`` and ``+++`` respectively. 239 | 240 | .. seealso:: 241 | 242 | The bootstrap 4 `card documentation `_, 243 | and this `card tutorial `_ 244 | 245 | .. code-block:: rst 246 | 247 | .. panels:: 248 | 249 | panel 1 header 250 | ^^^^^^^^^^^^^^ 251 | 252 | panel 1 content 253 | 254 | more content 255 | 256 | ++++++++++++++ 257 | panel 1 footer 258 | 259 | --- 260 | 261 | panel 2 header 262 | ^^^^^^^^^^^^^^ 263 | 264 | panel 2 content 265 | 266 | ++++++++++++++ 267 | panel 2 footer 268 | 269 | .. panels:: 270 | 271 | panel 1 header 272 | ^^^^^^^^^^^^^^ 273 | 274 | panel 1 content 275 | 276 | more content 277 | 278 | ++++++++++++++ 279 | panel 1 footer 280 | 281 | --- 282 | 283 | panel 2 header 284 | ^^^^^^^^^^^^^^ 285 | 286 | panel 2 content 287 | 288 | ++++++++++++++ 289 | panel 2 footer 290 | 291 | 292 | Card Styling 293 | ------------ 294 | 295 | To style the look of cards, 296 | you may use the directive options to add default CSS classes for each element, 297 | or use the per-panel option syntax to add to or override these: 298 | 299 | - container: the top-level container 300 | - column: the panel container 301 | - card: the panel card 302 | - body: the panel card 303 | - header: the panel header 304 | - footer: the panel footer 305 | 306 | You can add your own CSS (see 307 | `the html_css_files option `_) 308 | but it is advised you use the built-in bootstrap classes: 309 | 310 | - `Card colouring `_ contextual classes: ``bg-primary``, ``bg-success``, ``bg-info``, ``bg-warning``, ``bg-danger``, ``bg-secondary`, ``bg-dark`` and ``bg-light``. 311 | - `Padding and margins `_: ``border-0``, ``p-2``, ``m-2``, --- 312 | - `Text alignment `_: ``text-justify``, ``text-left``, ``text-center``, ``text-right`` 313 | 314 | .. code-block:: rst 315 | 316 | .. panels:: 317 | :body: bg-primary text-justify 318 | :header: text-center 319 | :footer: text-right 320 | 321 | --- 322 | :column: + p-1 323 | 324 | panel 1 header 325 | ^^^^^^^^^^^^^^ 326 | 327 | panel 1 content 328 | 329 | ++++++++++++++ 330 | panel 1 footer 331 | 332 | --- 333 | :column: + p-1 text-center border-0 334 | :body: bg-info 335 | :header: bg-success 336 | :footer: bg-secondary 337 | 338 | panel 2 header 339 | ^^^^^^^^^^^^^^ 340 | 341 | panel 2 content 342 | 343 | ++++++++++++++ 344 | panel 2 footer 345 | 346 | .. panels:: 347 | :body: bg-primary text-justify 348 | :header: text-center 349 | :footer: text-right 350 | 351 | --- 352 | :column: + p-1 353 | 354 | panel 1 header 355 | ^^^^^^^^^^^^^^ 356 | 357 | panel 1 content 358 | 359 | ++++++++++++++ 360 | panel 1 footer 361 | 362 | --- 363 | :column: + p-1 text-center border-0 364 | :body: bg-info 365 | :header: bg-success 366 | :footer: bg-secondary 367 | 368 | panel 2 header 369 | ^^^^^^^^^^^^^^ 370 | 371 | panel 2 content 372 | 373 | ++++++++++++++ 374 | panel 2 footer 375 | 376 | 377 | Image Caps 378 | ---------- 379 | 380 | Images can be added to the top and/or bottom of the panel. 381 | By default they will expand to fit the width of the card, 382 | but classes can also be used to add padding: 383 | 384 | .. code-block:: rst 385 | 386 | .. panels:: 387 | :img-top-cls: pl-5 pr-5 388 | 389 | --- 390 | :img-top: _static/ebp-logo.png 391 | :img-bottom: _static/footer-banner.jpg 392 | 393 | header 1 394 | ^^^^^^^^ 395 | 396 | Panel 1 content 397 | 398 | More **content** 399 | 400 | ++++++ 401 | tail 1 402 | 403 | --- 404 | :img-top: _static/sphinx-logo.png 405 | :img-top-cls: + bg-success 406 | :img-bottom: _static/footer-banner.jpg 407 | 408 | header 2 409 | ^^^^^^^^ 410 | 411 | Panel 2 content 412 | 413 | ++++++ 414 | tail 1 415 | 416 | .. panels:: 417 | :img-top-cls: pl-5 pr-5 418 | :body: text-center 419 | 420 | --- 421 | :img-top: _static/ebp-logo.png 422 | :img-bottom: _static/footer-banner.jpg 423 | 424 | header 1 425 | ^^^^^^^^ 426 | 427 | Panel 1 content 428 | 429 | More **content** 430 | 431 | ++++++ 432 | tail 1 433 | 434 | --- 435 | :img-top: _static/sphinx-logo.png 436 | :img-top-cls: + bg-success 437 | :img-bottom: _static/footer-banner.jpg 438 | 439 | header 2 440 | ^^^^^^^^ 441 | 442 | Panel 2 content 443 | 444 | ++++++ 445 | tail 1 446 | 447 | .. _components-buttons: 448 | 449 | Link Buttons 450 | ============ 451 | 452 | The ``link-button`` directive can be used to create buttons, which link to a URL (default) or reference. 453 | They can be styled by `Bootstrap button classes `_: 454 | 455 | .. code-block:: rst 456 | 457 | .. link-button:: https://example.com 458 | :type: url 459 | :text: some text 460 | :tooltip: hallo 461 | 462 | .. link-button:: panels/usage 463 | :type: ref 464 | :text: some other text 465 | :classes: btn-outline-primary btn-block 466 | 467 | .. link-button:: https://example.com 468 | :type: url 469 | :text: some text 470 | :tooltip: hallo 471 | 472 | .. link-button:: panels/usage 473 | :type: ref 474 | :text: some other text 475 | :classes: btn-outline-primary btn-block 476 | 477 | When used inside a panel, you can use the `stretched-link class `_, 478 | to make the entire panel clickable: 479 | 480 | .. code-block:: rst 481 | 482 | .. panels:: 483 | 484 | .. link-button:: https://example.com 485 | :classes: btn-success 486 | 487 | --- 488 | 489 | This entire panel is clickable. 490 | 491 | +++ 492 | 493 | .. link-button:: panels/usage 494 | :type: ref 495 | :text: Go To Reference 496 | :classes: btn-outline-primary btn-block stretched-link 497 | 498 | .. panels:: 499 | 500 | .. link-button:: https://example.com 501 | :classes: btn-success 502 | 503 | --- 504 | 505 | This entire panel is clickable. 506 | 507 | +++ 508 | 509 | .. link-button:: panels/usage 510 | :type: ref 511 | :text: Go To Reference 512 | :classes: btn-outline-primary btn-block stretched-link 513 | 514 | .. _components-badges: 515 | 516 | Link Badges 517 | =========== 518 | 519 | Badges are inline text with special formatting. Use the ``badge`` role to assign 520 | `Bootstrap badge formatting `_. 521 | Text and classes are delimited by a comma: 522 | 523 | .. code-block:: rst 524 | 525 | :badge:`primary,badge-primary` 526 | 527 | :badge:`primary,badge-primary badge-pill` 528 | 529 | :badge:`primary,badge-primary` 530 | :badge:`secondary,badge-secondary` 531 | :badge:`info,badge-info` 532 | :badge:`success,badge-success` 533 | :badge:`danger,badge-danger` 534 | :badge:`warning,badge-warning` 535 | :badge:`light,badge-light` 536 | :badge:`dark,badge-dark` 537 | 538 | :badge:`primary,badge-primary badge-pill` 539 | :badge:`secondary,badge-secondary badge-pill` 540 | :badge:`info,badge-info badge-pill` 541 | :badge:`success,badge-success badge-pill` 542 | :badge:`danger,badge-danger badge-pill` 543 | :badge:`warning,badge-warning badge-pill` 544 | :badge:`light,badge-light badge-pill` 545 | :badge:`dark,badge-dark badge-pill` 546 | 547 | The ``link-badge`` also adds the ability to use a link to a URI or reference: 548 | 549 | .. code-block:: rst 550 | 551 | :link-badge:`https://example.com,cls=badge-primary text-white,tooltip=a tooltip` 552 | :link-badge:`https://example.com,"my, text",cls=badge-dark text-white` 553 | :link-badge:`panels/usage,my reference,ref,badge-success text-white,hallo` 554 | 555 | :link-badge:`https://example.com,cls=badge-primary text-white,tooltip=a tooltip` 556 | :link-badge:`https://example.com,"my, text",cls=badge-dark text-white` 557 | :link-badge:`panels/usage,my reference,ref,badge-success text-white` 558 | 559 | Note the inputs are parsed by the following functions. The role text therefore uses these 560 | function signatures, except you don't need to use quoted strings, 561 | unless the string contains a comma. 562 | 563 | .. code-block:: python 564 | 565 | def get_badge_inputs(text, cls: str = ""): 566 | return text, cls.split() 567 | 568 | def get_link_badge_inputs(link, text=None, type="link", cls: str = "", tooltip=None): 569 | return link, text or link, type, cls.split(), tooltip 570 | 571 | .. _components-dropdown: 572 | 573 | Dropdown Usage 574 | ============== 575 | 576 | The ``dropdown`` directive combines a `Bootstrap card `_ 577 | with the `HTML details tag `_ to create a collapsible 578 | drop-down panel. 579 | 580 | .. code-block:: rst 581 | 582 | .. dropdown:: Click on me to see my content! 583 | 584 | I'm the content which can be anything: 585 | 586 | .. link-button:: https://example.com 587 | :text: Like a Button 588 | :classes: btn-primary 589 | 590 | .. dropdown:: Click on me to see my content! 591 | 592 | I'm the content which can be anything: 593 | 594 | .. link-button:: https://example.com 595 | :text: Like a Button 596 | :classes: btn-primary 597 | 598 | You can start with the panel open by default using the ``open`` option: 599 | 600 | .. code-block:: rst 601 | 602 | .. dropdown:: My Content 603 | :open: 604 | 605 | Is already visible 606 | 607 | .. dropdown:: My Content 608 | :open: 609 | 610 | Is already visible 611 | 612 | If the drop-down has no title assigned, it will display an ellipsis, which is hidden when open: 613 | 614 | .. code-block:: rst 615 | 616 | .. dropdown:: 617 | 618 | My Content 619 | 620 | .. dropdown:: 621 | 622 | My Content 623 | 624 | The overarching container, title banner and body panel can all be styled by assigning classes. 625 | Adding ``+`` at the start appends the classes to any default ones. 626 | 627 | .. code-block:: rst 628 | 629 | .. dropdown:: My Content 630 | :container: + shadow 631 | :title: bg-primary text-white text-center font-weight-bold 632 | :body: bg-light text-right font-italic 633 | 634 | Is formatted 635 | 636 | .. dropdown:: My Content 637 | :container: + shadow 638 | :title: bg-primary text-white text-center font-weight-bold 639 | :body: bg-light text-right font-italic 640 | 641 | Is formatted 642 | 643 | Transition Animation 644 | -------------------- 645 | 646 | Adding the ``animate`` option will trigger an animation when the content of the drop-down is opened. 647 | 648 | .. code-block:: rst 649 | 650 | .. dropdown:: My content will fade in 651 | :animate: fade-in 652 | 653 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 654 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 655 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 656 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 657 | 658 | .. dropdown:: My content will fade in 659 | :animate: fade-in 660 | 661 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 662 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 663 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 664 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 665 | 666 | .. dropdown:: My content will fade in and slide down 667 | :animate: fade-in-slide-down 668 | 669 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 670 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 671 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 672 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 673 | 674 | .. note:: 675 | 676 | Current available inputs: ``fade-in``, ``fade-in-slide-down`` 677 | 678 | .. _components-tabbed: 679 | 680 | Tabbed Content 681 | ============== 682 | 683 | The ``tabbed`` directive generates tabbed selection panels. 684 | 685 | Sequential directives will be grouped together, unless the ``:new-group`` option is added. 686 | You can set which tab will be shown by default, using the ``:selected:`` option. 687 | 688 | Tab directives can contain any content, and you can also set CSS classes with ``:class-label:`` and ``:class-content:``: 689 | 690 | .. code-block:: rst 691 | 692 | .. tabbed:: Tab 1 693 | 694 | Tab 1 content 695 | 696 | .. tabbed:: Tab 2 697 | :class-content: pl-1 bg-primary 698 | 699 | Tab 2 content 700 | 701 | .. tabbed:: Tab 3 702 | :new-group: 703 | 704 | .. code-block:: python 705 | 706 | import pip 707 | 708 | .. tabbed:: Tab 4 709 | :selected: 710 | 711 | .. dropdown:: Nested Dropdown 712 | 713 | Some content 714 | 715 | .. tabbed:: Tab 1 716 | 717 | Tab 1 content 718 | 719 | .. tabbed:: Tab 2 720 | :class-content: pl-1 bg-primary 721 | 722 | Tab 2 content 723 | 724 | .. tabbed:: Tab 3 725 | :new-group: 726 | 727 | .. code-block:: python 728 | 729 | import pip 730 | 731 | .. tabbed:: Tab 4 732 | :selected: 733 | 734 | .. dropdown:: Nested Dropdown 735 | 736 | Some content 737 | 738 | Here's an example of showing an example in multiple programming languages: 739 | 740 | .. tabbed:: c++ 741 | 742 | .. code-block:: c++ 743 | 744 | int main(const int argc, const char **argv) { 745 | return 0; 746 | } 747 | 748 | .. tabbed:: python 749 | 750 | .. code-block:: python 751 | 752 | def main(): 753 | return 754 | 755 | .. tabbed:: java 756 | 757 | .. code-block:: java 758 | 759 | class Main { 760 | public static void main(String[] args) { 761 | } 762 | } 763 | 764 | .. tabbed:: julia 765 | 766 | .. code-block:: julia 767 | 768 | function main() 769 | end 770 | 771 | .. tabbed:: fortran 772 | 773 | .. code-block:: fortran 774 | 775 | PROGRAM main 776 | END PROGRAM main 777 | 778 | You can also control the colors of the labels and lines, setting ``panels_css_variables`` in your ``conf.py``. 779 | Here are the defaults: 780 | 781 | .. code-block:: python 782 | 783 | panels_css_variables = { 784 | "tabs-color-label-active": "hsla(231, 99%, 66%, 1)", 785 | "tabs-color-label-inactive": "rgba(178, 206, 245, 0.62)", 786 | "tabs-color-overline": "rgb(207, 236, 238)", 787 | "tabs-color-underline": "rgb(207, 236, 238)", 788 | "tabs-size-label": "1rem", 789 | } 790 | 791 | .. seealso:: 792 | 793 | Note, the `sphinx-tabs `__ package also offers directives to create tabs. 794 | The key difference is that, whereas ``sphinx-tabs`` uses JavaScript to implement this functionality, ``sphinx-panels`` only uses CSS. 795 | A CSS only solution has the benefit of faster load-times, and working when JS is disabled, although JS allows ``sphinx-tabs`` to implement some extended functionality (like synchronized selections). 796 | 797 | .. _components-icons: 798 | 799 | Inline Icons 800 | ============ 801 | 802 | Inline icons can be added to your text from either the 803 | `GitHub octicon `_ or 804 | `FontAwesome `_ libraries. 805 | 806 | ====================================================== =============================================== 807 | rST Output 808 | ====================================================== =============================================== 809 | ``:opticon:`report``` :opticon:`report` 810 | ``:opticon:`x-circle,text-white bg-danger,size=24``` :opticon:`x-circle,text-white bg-danger,size=24` 811 | ``:fa:`save``` :fa:`save` 812 | ``:fa:`spinner,text-white bg-primary fa-2x,style=fa``` :fa:`spinner,text-white bg-primary fa-2x,style=fa` 813 | ====================================================== =============================================== 814 | 815 | Note that the theme you are using does not already include the FontAwesome CSS, 816 | it should be loaded in your ``conf.py``, 817 | with the `html_css_files `_ option, e.g.: 818 | 819 | .. code-block:: python 820 | 821 | html_css_files = ["https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"] 822 | 823 | By default, icons will only be output in HTML formats. 824 | But if you want fontawesome icons to be output on LaTeX, using the `fontawesome package `_, 825 | you can add to your ``conf.py``: 826 | 827 | .. code-block:: python 828 | 829 | panels_add_fontawesome_latex = True 830 | 831 | Additional classes can be added after a comma delimiter. 832 | Also the size (16px or 24px) can be set for opticons, and the style/prefix for fontawesome (version 5). 833 | 834 | .. seealso:: 835 | 836 | https://www.w3schools.com/icons/fontawesome_icons_intro.asp 837 | 838 | .. _components-div: 839 | 840 | Div Directive 841 | ============= 842 | 843 | The ``div`` directive is the same as the `container directive `_, 844 | but does not add a ``container`` class in HTML outputs, which is incompatible with Bootstrap CSS: 845 | 846 | .. code-block:: rst 847 | 848 | .. div:: text-primary 849 | 850 | hallo 851 | 852 | .. div:: text-primary 853 | 854 | hallo 855 | 856 | 857 | Combined Example 858 | ================ 859 | 860 | .. code-block:: rst 861 | 862 | .. dropdown:: Panels in a drop-down 863 | :title: bg-success text-warning 864 | :open: 865 | :animate: fade-in-slide-down 866 | 867 | .. panels:: 868 | :container: container-fluid pb-1 869 | :column: col-lg-6 col-md-6 col-sm-12 col-xs-12 p-2 870 | :card: shadow 871 | :header: border-0 872 | :footer: border-0 873 | 874 | --- 875 | :card: + bg-warning 876 | 877 | header 878 | ^^^^^^ 879 | 880 | Content of the top-left panel 881 | 882 | ++++++ 883 | footer 884 | 885 | --- 886 | :card: + bg-info 887 | :footer: + bg-danger 888 | 889 | header 890 | ^^^^^^ 891 | 892 | Content of the top-right panel 893 | 894 | ++++++ 895 | footer 896 | 897 | --- 898 | :column: col-lg-12 p-3 899 | :card: + text-center 900 | 901 | .. link-button:: panels/usage 902 | :type: ref 903 | :text: Clickable Panel 904 | :classes: btn-link stretched-link font-weight-bold 905 | 906 | .. dropdown:: Panels in a drop-down 907 | :title: bg-success text-warning 908 | :open: 909 | :animate: fade-in-slide-down 910 | 911 | .. panels:: 912 | :container: container-fluid pb-1 913 | :column: col-lg-6 col-md-6 col-sm-12 col-xs-12 p-2 914 | :card: shadow 915 | :header: border-0 916 | :footer: border-0 917 | 918 | --- 919 | :card: + bg-warning 920 | 921 | header 922 | ^^^^^^ 923 | 924 | Content of the top-left panel 925 | 926 | ++++++ 927 | footer 928 | 929 | --- 930 | :card: + bg-info 931 | :footer: + bg-danger 932 | 933 | header 934 | ^^^^^^ 935 | 936 | Content of the top-right panel 937 | 938 | ++++++ 939 | footer 940 | 941 | --- 942 | :column: col-lg-12 p-3 943 | :card: + text-center 944 | 945 | .. link-button:: panels/usage 946 | :type: ref 947 | :text: Clickable Panel 948 | :classes: btn-link stretched-link font-weight-bold 949 | 950 | 951 | Acknowledgements 952 | ================ 953 | 954 | - Panels originally adapted from the `pandas documentation `_. 955 | - Dropdown originally adapted from `tk0miya/sphinxcontrib-details-directive `_. 956 | -------------------------------------------------------------------------------- /git_rebase_theme_branches.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | fmt=' 6 | git checkout %(refname) 7 | git rebase master 8 | git push origin HEAD:%(refname:strip=3) --force 9 | ' 10 | 11 | eval "$(git for-each-ref --shell --format="$fmt" refs/remotes/origin/*-theme)" 12 | 13 | git checkout master 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from setuptools import setup, find_packages 3 | 4 | version = [ 5 | line 6 | for line in Path("sphinx_panels/__init__.py").read_text().split("\n") 7 | if "__version__" in line 8 | ] 9 | version = version[0].split(" = ")[-1].strip('"') 10 | 11 | with open("./README.md", "r") as ff: 12 | readme_text = ff.read() 13 | 14 | setup( 15 | name="sphinx-panels", 16 | version=version, 17 | description="A sphinx extension for creating panels in a grid layout.", 18 | long_description=readme_text, 19 | long_description_content_type="text/markdown", 20 | author="Chris Sewell", 21 | author_email="chrisj_sewell@hotmail.com", 22 | url="https://github.com/executablebooks/sphinx-panels", 23 | project_urls={"Documentation": "https://sphinx-panels.readthedocs.io"}, 24 | license="MIT", 25 | packages=find_packages(), 26 | include_package_data=True, 27 | install_requires=[ 28 | "docutils", 29 | "sphinx>=2,<5", 30 | 'importlib-resources~=3.0.0; python_version < "3.7"', 31 | ], 32 | extras_require={ 33 | "themes": [ 34 | # ref: https://github.com/sphinx-doc/sphinx/issues/10291 35 | "Jinja2<3.1", 36 | "sphinx-rtd-theme", 37 | "pydata-sphinx-theme~=0.4.0", 38 | "sphinx-book-theme~=0.0.36", 39 | "myst-parser~=0.12.9", 40 | ], 41 | "code_style": ["pre-commit~=2.7.0"], 42 | "testing": ["pytest~=6.0.1", "pytest-regressions~=2.0.1"], 43 | "live-dev": ["sphinx-autobuild", "web-compile~=0.2.0"], 44 | }, 45 | classifiers=[ 46 | "License :: OSI Approved :: MIT License", 47 | "Programming Language :: Python :: 3", 48 | "Topic :: Software Development :: Libraries :: Python Modules", 49 | "Framework :: Sphinx :: Extension", 50 | ], 51 | keywords="sphinx html bootstrap grid card dropdown button badge", 52 | ) 53 | -------------------------------------------------------------------------------- /sphinx_panels/__init__.py: -------------------------------------------------------------------------------- 1 | """"A sphinx extension to add a ``panels`` directive.""" 2 | import hashlib 3 | from pathlib import Path 4 | 5 | try: 6 | import importlib.resources as resources 7 | except ImportError: 8 | # python < 3.7 9 | import importlib_resources as resources 10 | 11 | from docutils import nodes 12 | from docutils.parsers.rst import directives, Directive 13 | from sphinx.application import Sphinx 14 | from sphinx.environment import BuildEnvironment 15 | from sphinx.util.logging import getLogger 16 | 17 | from .button import setup_link_button 18 | from .dropdown import setup_dropdown 19 | from .panels import setup_panels 20 | from .tabs import setup_tabs 21 | from .icons import setup_icons 22 | 23 | from . import _css as css_module 24 | 25 | __version__ = "0.6.0" 26 | 27 | LOGGER = getLogger(__name__) 28 | 29 | 30 | def get_default_css_variables(): 31 | return { 32 | "tabs-color-label-active": "hsla(231, 99%, 66%, 1)", 33 | "tabs-color-label-inactive": "rgba(178, 206, 245, 0.62)", 34 | "tabs-color-overline": "rgb(207, 236, 238)", 35 | "tabs-color-underline": "rgb(207, 236, 238)", 36 | "tabs-size-label": "1rem", 37 | } 38 | 39 | 40 | def update_css(app: Sphinx): 41 | """Compile SCSS to the build directory.""" 42 | # merge user CSS variables with defaults 43 | css_variables = get_default_css_variables() 44 | for key, value in app.config.panels_css_variables.items(): 45 | if key not in css_variables: 46 | LOGGER.warning(f"key in 'panels_css_variables' is not recognised: {key}") 47 | else: 48 | css_variables[key] = value 49 | 50 | # reset css changed attribute 51 | app.env.panels_css_changed = False 52 | 53 | # setup up new static path in output dir 54 | static_path = (Path(app.outdir) / "_panels_static").absolute() 55 | static_path.mkdir(exist_ok=True) 56 | app.config.html_static_path.append(str(static_path)) 57 | 58 | # record current resources 59 | old_resources = {path.name for path in static_path.glob("*") if path.is_file()} 60 | 61 | # Add core CSS 62 | css_files = [r for r in resources.contents(css_module) if r.endswith(".css")] 63 | if app.config.panels_add_boostrap_css is not None: 64 | LOGGER.warning( 65 | "`panels_add_boostrap_css` will be deprecated. Please use" 66 | "`panels_add_bootstrap_css`." 67 | ) 68 | app.config.panels_add_bootstrap_css = app.config.panels_add_boostrap_css 69 | 70 | if app.config.panels_add_bootstrap_css is False: 71 | css_files = [name for name in css_files if "bootstrap" not in name] 72 | for filename in css_files: 73 | app.add_css_file(filename) 74 | if not (static_path / filename).exists(): 75 | content = resources.read_text(css_module, filename) 76 | (static_path / filename).write_text(content) 77 | app.env.panels_css_changed = True 78 | if filename in old_resources: 79 | old_resources.remove(filename) 80 | 81 | # add variables CSS file 82 | css_lines = [":root {"] 83 | for name, value in css_variables.items(): 84 | css_lines.append(f"--{name}: {value};") 85 | css_lines.append("}") 86 | css_str = "\n".join(css_lines) 87 | css_variables_name = ( 88 | f"panels-variables.{hashlib.md5(css_str.encode('utf8')).hexdigest()}.css" 89 | ) 90 | app.add_css_file(css_variables_name) 91 | if not (static_path / css_variables_name).exists(): 92 | (static_path / css_variables_name).write_text(css_str) 93 | app.env.panels_css_changed = True 94 | if css_variables_name in old_resources: 95 | old_resources.remove(css_variables_name) 96 | 97 | # remove old resources 98 | for name in old_resources: 99 | for path in Path(app.outdir).glob(f"**/{name}"): 100 | path.unlink() 101 | 102 | 103 | def update_css_links(app: Sphinx, env: BuildEnvironment): 104 | """If CSS has changed, all files must be re-written, 105 | to include the correct stylesheets. 106 | """ 107 | if env.panels_css_changed and app.config.panels_dev_mode: 108 | LOGGER.debug("sphinx-panels CSS changed; re-writing all files") 109 | return list(env.all_docs.keys()) 110 | 111 | 112 | class Div(Directive): 113 | """Same as the ``container`` directive, 114 | but does not add the ``container`` class in HTML outputs, 115 | which can interfere with Bootstrap CSS. 116 | """ 117 | 118 | optional_arguments = 1 119 | final_argument_whitespace = True 120 | option_spec = {"name": directives.unchanged} 121 | has_content = True 122 | 123 | def run(self): 124 | self.assert_has_content() 125 | text = "\n".join(self.content) 126 | try: 127 | if self.arguments: 128 | classes = directives.class_option(self.arguments[0]) 129 | else: 130 | classes = [] 131 | except ValueError: 132 | raise self.error( 133 | 'Invalid class attribute value for "%s" directive: "%s".' 134 | % (self.name, self.arguments[0]) 135 | ) 136 | node = nodes.container(text, is_div=True) 137 | node["classes"].extend(classes) 138 | self.add_name(node) 139 | self.state.nested_parse(self.content, self.content_offset, node) 140 | return [node] 141 | 142 | 143 | def visit_container(self, node: nodes.Node): 144 | classes = "docutils container" 145 | if node.get("is_div", False): 146 | # we don't want the CSS for container for these nodes 147 | classes = "docutils" 148 | self.body.append(self.starttag(node, "div", CLASS=classes)) 149 | 150 | 151 | def depart_container(self, node: nodes.Node): 152 | self.body.append("\n") 153 | 154 | 155 | def setup(app: Sphinx): 156 | app.add_directive("div", Div) 157 | app.add_config_value("panels_add_bootstrap_css", None, "env") 158 | app.add_config_value("panels_add_boostrap_css", None, "env") 159 | app.add_config_value("panels_css_variables", {}, "env") 160 | app.add_config_value("panels_dev_mode", False, "env") 161 | app.connect("builder-inited", update_css) 162 | app.connect("env-updated", update_css_links) 163 | # we override container html visitors, to stop the default behaviour 164 | # of adding the `container` class to all nodes.container 165 | app.add_node( 166 | nodes.container, override=True, html=(visit_container, depart_container) 167 | ) 168 | 169 | setup_panels(app) 170 | setup_link_button(app) 171 | setup_dropdown(app) 172 | setup_tabs(app) 173 | setup_icons(app) 174 | 175 | return { 176 | "version": __version__, 177 | "parallel_read_safe": True, 178 | "parallel_write_safe": True, 179 | } 180 | -------------------------------------------------------------------------------- /sphinx_panels/_css/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executablebooks/sphinx-panels/44bab2341d97707365396efc0b23a1edac26da7c/sphinx_panels/_css/__init__.py -------------------------------------------------------------------------------- /sphinx_panels/_css/panels-bootstrap.5fd3999ee7762ccc51105388f4a9d115.css: -------------------------------------------------------------------------------- 1 | .badge{border-radius:.25rem;display:inline-block;font-size:75%;font-weight:700;line-height:1;padding:.25em .4em;text-align:center;vertical-align:baseline;white-space:nowrap}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{border-radius:10rem;padding-left:.6em;padding-right:.6em}.badge-primary{background-color:#007bff;color:#fff}.badge-primary[href]:focus,.badge-primary[href]:hover{background-color:#0062cc;color:#fff;text-decoration:none}.badge-secondary{background-color:#6c757d;color:#fff}.badge-secondary[href]:focus,.badge-secondary[href]:hover{background-color:#545b62;color:#fff;text-decoration:none}.badge-success{background-color:#28a745;color:#fff}.badge-success[href]:focus,.badge-success[href]:hover{background-color:#1e7e34;color:#fff;text-decoration:none}.badge-info{background-color:#17a2b8;color:#fff}.badge-info[href]:focus,.badge-info[href]:hover{background-color:#117a8b;color:#fff;text-decoration:none}.badge-warning{background-color:#ffc107;color:#212529}.badge-warning[href]:focus,.badge-warning[href]:hover{background-color:#d39e00;color:#212529;text-decoration:none}.badge-danger{background-color:#dc3545;color:#fff}.badge-danger[href]:focus,.badge-danger[href]:hover{background-color:#bd2130;color:#fff;text-decoration:none}.badge-light{background-color:#f8f9fa;color:#212529}.badge-light[href]:focus,.badge-light[href]:hover{background-color:#dae0e5;color:#212529;text-decoration:none}.badge-dark{background-color:#343a40;color:#fff}.badge-dark[href]:focus,.badge-dark[href]:hover{background-color:#1d2124;color:#fff;text-decoration:none}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:.25rem !important}.pt-1,.py-1{padding-top:.25rem !important}.pr-1,.px-1{padding-right:.25rem !important}.pb-1,.py-1{padding-bottom:.25rem !important}.pl-1,.px-1{padding-left:.25rem !important}.p-2{padding:.5rem !important}.pt-2,.py-2{padding-top:.5rem !important}.pr-2,.px-2{padding-right:.5rem !important}.pb-2,.py-2{padding-bottom:.5rem !important}.pl-2,.px-2{padding-left:.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:.25rem !important}.mt-1,.my-1{margin-top:.25rem !important}.mr-1,.mx-1{margin-right:.25rem !important}.mb-1,.my-1{margin-bottom:.25rem !important}.ml-1,.mx-1{margin-left:.25rem !important}.m-2{margin:.5rem !important}.mt-2,.my-2{margin-top:.5rem !important}.mr-2,.mx-2{margin-right:.5rem !important}.mb-2,.my-2{margin-bottom:.5rem !important}.ml-2,.mx-2{margin-left:.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;color:#212529;cursor:pointer;display:inline-block;font-size:1rem;font-weight:400;line-height:1.5;padding:.375rem .75rem;text-align:center;transition:color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none;vertical-align:middle}.btn:hover{color:#212529;text-decoration:none}.btn:visited{color:#212529}.btn.focus,.btn:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,0.25);outline:0}.btn.disabled,.btn:disabled{opacity:.65}@media (prefers-reduced-motion: reduce){.btn{transition:none}}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{background-color:#007bff;border-color:#007bff;color:#fff}.btn-primary:visited{color:#fff}.btn-primary:hover{background-color:#0069d9;border-color:#0062cc;color:#fff}.btn-primary.focus,.btn-primary:focus{background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(0,123,255,0.5);color:#fff}.btn-primary.disabled,.btn-primary:disabled{background-color:#007bff;border-color:#007bff;color:#fff}.btn-primary.active:not(:disabled):not(.disabled),.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{background-color:#0062cc;border-color:#005cbf;color:#fff}.btn-primary.active:not(:disabled):not(.disabled):focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,0.5)}.btn-secondary{background-color:#6c757d;border-color:#6c757d;color:#fff}.btn-secondary:visited{color:#fff}.btn-secondary:hover{background-color:#5a6268;border-color:#545b62;color:#fff}.btn-secondary.focus,.btn-secondary:focus{background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(108,117,125,0.5);color:#fff}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#6c757d;border-color:#6c757d;color:#fff}.btn-secondary.active:not(:disabled):not(.disabled),.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{background-color:#545b62;border-color:#4e555b;color:#fff}.btn-secondary.active:not(:disabled):not(.disabled):focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.btn-success{background-color:#28a745;border-color:#28a745;color:#fff}.btn-success:visited{color:#fff}.btn-success:hover{background-color:#218838;border-color:#1e7e34;color:#fff}.btn-success.focus,.btn-success:focus{background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(40,167,69,0.5);color:#fff}.btn-success.disabled,.btn-success:disabled{background-color:#28a745;border-color:#28a745;color:#fff}.btn-success.active:not(:disabled):not(.disabled),.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{background-color:#1e7e34;border-color:#1c7430;color:#fff}.btn-success.active:not(:disabled):not(.disabled):focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.btn-info{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.btn-info:visited{color:#fff}.btn-info:hover{background-color:#138496;border-color:#117a8b;color:#fff}.btn-info.focus,.btn-info:focus{background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(23,162,184,0.5);color:#fff}.btn-info.disabled,.btn-info:disabled{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.btn-info.active:not(:disabled):not(.disabled),.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{background-color:#117a8b;border-color:#10707f;color:#fff}.btn-info.active:not(:disabled):not(.disabled):focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,0.5)}.btn-warning{background-color:#ffc107;border-color:#ffc107;color:#212529}.btn-warning:visited{color:#212529}.btn-warning:hover{background-color:#e0a800;border-color:#d39e00;color:#212529}.btn-warning.focus,.btn-warning:focus{background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(255,193,7,0.5);color:#212529}.btn-warning.disabled,.btn-warning:disabled{background-color:#ffc107;border-color:#ffc107;color:#212529}.btn-warning.active:not(:disabled):not(.disabled),.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{background-color:#d39e00;border-color:#c69500;color:#212529}.btn-warning.active:not(:disabled):not(.disabled):focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-danger{background-color:#dc3545;border-color:#dc3545;color:#fff}.btn-danger:visited{color:#fff}.btn-danger:hover{background-color:#c82333;border-color:#bd2130;color:#fff}.btn-danger.focus,.btn-danger:focus{background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(220,53,69,0.5);color:#fff}.btn-danger.disabled,.btn-danger:disabled{background-color:#dc3545;border-color:#dc3545;color:#fff}.btn-danger.active:not(:disabled):not(.disabled),.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{background-color:#bd2130;border-color:#b21f2d;color:#fff}.btn-danger.active:not(:disabled):not(.disabled):focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.btn-light{background-color:#f8f9fa;border-color:#f8f9fa;color:#212529}.btn-light:visited{color:#212529}.btn-light:hover{background-color:#e2e6ea;border-color:#dae0e5;color:#212529}.btn-light.focus,.btn-light:focus{background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(248,249,250,0.5);color:#212529}.btn-light.disabled,.btn-light:disabled{background-color:#f8f9fa;border-color:#f8f9fa;color:#212529}.btn-light.active:not(:disabled):not(.disabled),.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{background-color:#dae0e5;border-color:#d3d9df;color:#212529}.btn-light.active:not(:disabled):not(.disabled):focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.btn-dark{background-color:#343a40;border-color:#343a40;color:#fff}.btn-dark:visited{color:#fff}.btn-dark:hover{background-color:#23272b;border-color:#1d2124;color:#fff}.btn-dark.focus,.btn-dark:focus{background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(52,58,64,0.5);color:#fff}.btn-dark.disabled,.btn-dark:disabled{background-color:#343a40;border-color:#343a40;color:#fff}.btn-dark.active:not(:disabled):not(.disabled),.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{background-color:#1d2124;border-color:#171a1d;color:#fff}.btn-dark.active:not(:disabled):not(.disabled):focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.btn-outline-primary{border-color:#007bff;color:#007bff}.btn-outline-primary:visited{color:#007bff}.btn-outline-primary:hover{background-color:#007bff;border-color:#007bff;color:#fff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{background-color:transparent;color:#007bff}.btn-outline-primary.active:not(:disabled):not(.disabled),.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{background-color:#007bff;border-color:#007bff;color:#fff}.btn-outline-primary.active:not(:disabled):not(.disabled):focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,0.5)}.btn-outline-secondary{border-color:#6c757d;color:#6c757d}.btn-outline-secondary:visited{color:#6c757d}.btn-outline-secondary:hover{background-color:#6c757d;border-color:#6c757d;color:#fff}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{background-color:transparent;color:#6c757d}.btn-outline-secondary.active:not(:disabled):not(.disabled),.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{background-color:#6c757d;border-color:#6c757d;color:#fff}.btn-outline-secondary.active:not(:disabled):not(.disabled):focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,0.5)}.btn-outline-success{border-color:#28a745;color:#28a745}.btn-outline-success:visited{color:#28a745}.btn-outline-success:hover{background-color:#28a745;border-color:#28a745;color:#fff}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{background-color:transparent;color:#28a745}.btn-outline-success.active:not(:disabled):not(.disabled),.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{background-color:#28a745;border-color:#28a745;color:#fff}.btn-outline-success.active:not(:disabled):not(.disabled):focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,0.5)}.btn-outline-info{border-color:#17a2b8;color:#17a2b8}.btn-outline-info:visited{color:#17a2b8}.btn-outline-info:hover{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{background-color:transparent;color:#17a2b8}.btn-outline-info.active:not(:disabled):not(.disabled),.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.btn-outline-info.active:not(:disabled):not(.disabled):focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,0.5)}.btn-outline-warning{border-color:#ffc107;color:#ffc107}.btn-outline-warning:visited{color:#ffc107}.btn-outline-warning:hover{background-color:#ffc107;border-color:#ffc107;color:#212529}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{background-color:transparent;color:#ffc107}.btn-outline-warning.active:not(:disabled):not(.disabled),.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{background-color:#ffc107;border-color:#ffc107;color:#212529}.btn-outline-warning.active:not(:disabled):not(.disabled):focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,0.5)}.btn-outline-danger{border-color:#dc3545;color:#dc3545}.btn-outline-danger:visited{color:#dc3545}.btn-outline-danger:hover{background-color:#dc3545;border-color:#dc3545;color:#fff}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{background-color:transparent;color:#dc3545}.btn-outline-danger.active:not(:disabled):not(.disabled),.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{background-color:#dc3545;border-color:#dc3545;color:#fff}.btn-outline-danger.active:not(:disabled):not(.disabled):focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,0.5)}.btn-outline-light{border-color:#f8f9fa;color:#f8f9fa}.btn-outline-light:visited{color:#f8f9fa}.btn-outline-light:hover{background-color:#f8f9fa;border-color:#f8f9fa;color:#212529}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{background-color:transparent;color:#f8f9fa}.btn-outline-light.active:not(:disabled):not(.disabled),.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{background-color:#f8f9fa;border-color:#f8f9fa;color:#212529}.btn-outline-light.active:not(:disabled):not(.disabled):focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,0.5)}.btn-outline-dark{border-color:#343a40;color:#343a40}.btn-outline-dark:visited{color:#343a40}.btn-outline-dark:hover{background-color:#343a40;border-color:#343a40;color:#fff}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{background-color:transparent;color:#343a40}.btn-outline-dark.active:not(:disabled):not(.disabled),.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{background-color:#343a40;border-color:#343a40;color:#fff}.btn-outline-dark.active:not(:disabled):not(.disabled):focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,0.5)}.btn-link{color:#007bff;font-weight:400;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{box-shadow:none;text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{border-radius:.3rem;font-size:1.25rem;line-height:1.5;padding:.5rem 1rem}.btn-group-sm>.btn,.btn-sm{border-radius:.2rem;font-size:.875rem;line-height:1.5;padding:.25rem .5rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input.btn-block[type=button],input.btn-block[type=reset],input.btn-block[type=submit]{width:100%}.stretched-link::after{background-color:rgba(0,0,0,0);bottom:0;content:'';left:0;pointer-events:auto;position:absolute;right:0;top:0;z-index:1}.text-wrap{white-space:normal !important}.card{background-clip:border-box;background-color:#fff;border:1px solid rgba(0,0,0,0.125);border-radius:.25rem;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.card>hr{margin-left:0;margin-right:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-bottom:0;margin-top:-.375rem}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{background-color:rgba(0,0,0,0.03);border-bottom:1px solid rgba(0,0,0,0.125);margin-bottom:0;padding:.75rem 1.25rem}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{background-color:rgba(0,0,0,0.03);border-top:1px solid rgba(0,0,0,0.125);padding:.75rem 1.25rem}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{border-bottom:0;margin-bottom:-.75rem;margin-left:-.625rem;margin-right:-.625rem}.card-header-pills{margin-left:-.625rem;margin-right:-.625rem}.card-img-overlay{bottom:0;left:0;padding:1.25rem;position:absolute;right:0;top:0}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-left-radius:calc(.25rem - 1px);border-bottom-right-radius:calc(.25rem - 1px)}.w-100{width:100% !important}.shadow{box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.bg-primary{background-color:#007bff !important}button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc !important}a.bg-primary:focus,a.bg-primary:hover{background-color:#0062cc !important}a.text-primary:focus,a.text-primary:hover{color:#121416 !important}.bg-secondary{background-color:#6c757d !important}button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62 !important}a.bg-secondary:focus,a.bg-secondary:hover{background-color:#545b62 !important}a.text-secondary:focus,a.text-secondary:hover{color:#121416 !important}.bg-success{background-color:#28a745 !important}button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34 !important}a.bg-success:focus,a.bg-success:hover{background-color:#1e7e34 !important}a.text-success:focus,a.text-success:hover{color:#121416 !important}.bg-info{background-color:#17a2b8 !important}button.bg-info:focus,button.bg-info:hover{background-color:#117a8b !important}a.bg-info:focus,a.bg-info:hover{background-color:#117a8b !important}a.text-info:focus,a.text-info:hover{color:#121416 !important}.bg-warning{background-color:#ffc107 !important}button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00 !important}a.bg-warning:focus,a.bg-warning:hover{background-color:#d39e00 !important}a.text-warning:focus,a.text-warning:hover{color:#121416 !important}.bg-danger{background-color:#dc3545 !important}button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130 !important}a.bg-danger:focus,a.bg-danger:hover{background-color:#bd2130 !important}a.text-danger:focus,a.text-danger:hover{color:#121416 !important}.bg-light{background-color:#f8f9fa !important}button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5 !important}a.bg-light:focus,a.bg-light:hover{background-color:#dae0e5 !important}a.text-light:focus,a.text-light:hover{color:#121416 !important}.bg-dark{background-color:#343a40 !important}button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124 !important}a.bg-dark:focus,a.bg-dark:hover{background-color:#1d2124 !important}a.text-dark:focus,a.text-dark:hover{color:#121416 !important}.bg-white{background-color:#fff !important}button.bg-white:focus,button.bg-white:hover{background-color:#e6e6e6 !important}a.bg-white:focus,a.bg-white:hover{background-color:#e6e6e6 !important}a.text-white:focus,a.text-white:hover{color:#121416 !important}.text-primary{color:#007bff !important}.text-secondary{color:#6c757d !important}.text-success{color:#28a745 !important}.text-info{color:#17a2b8 !important}.text-warning{color:#ffc107 !important}.text-danger{color:#dc3545 !important}.text-light{color:#f8f9fa !important}.text-dark{color:#343a40 !important}.text-white{color:#fff !important}.text-body{color:#212529 !important}.text-muted{color:#6c757d !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.bg-transparent{background-color:transparent !important}.text-justify{text-align:justify !important}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.container{margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px;width:100%}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px;width:100%}@media (min-width: 576px){.container,.container-sm{max-width:540px}}@media (min-width: 768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width: 992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width: 1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-left:-15px;margin-right:-15px}.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{padding-left:15px;padding-right:15px;position:relative;width:100%}@media (min-width: 576px){.col-sm{flex-basis:0;flex-grow:1;-ms-flex-positive:1;-ms-flex-preferred-size:0;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;max-width:100%;width:auto}.col-sm-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}}@media (min-width: 768px){.col-md{flex-basis:0;flex-grow:1;-ms-flex-positive:1;-ms-flex-preferred-size:0;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;max-width:100%;width:auto}.col-md-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}}@media (min-width: 992px){.col-lg{flex-basis:0;flex-grow:1;-ms-flex-positive:1;-ms-flex-preferred-size:0;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;max-width:100%;width:auto}.col-lg-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}}@media (min-width: 1200px){.col-xl{flex-basis:0;flex-grow:1;-ms-flex-positive:1;-ms-flex-preferred-size:0;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;max-width:100%;width:auto}.col-xl-1{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}}.d-flex{display:-ms-flexbox !important;display:flex !important}.sphinx-bs,.sphinx-bs *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sphinx-bs p{margin-top:0} 2 | -------------------------------------------------------------------------------- /sphinx_panels/_css/panels-main.c949a650a448cc0ae9fd3441c0e17fb0.css: -------------------------------------------------------------------------------- 1 | details.dropdown .summary-title{padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.dropdown:hover{cursor:pointer}details.dropdown .summary-content{cursor:default}details.dropdown summary{list-style:none;padding:1em}details.dropdown summary .octicon.no-title{vertical-align:middle}details.dropdown[open] summary .octicon.no-title{visibility:hidden}details.dropdown summary::-webkit-details-marker{display:none}details.dropdown summary:focus{outline:none}details.dropdown summary:hover .summary-up svg,details.dropdown summary:hover .summary-down svg{opacity:1}details.dropdown .summary-up svg,details.dropdown .summary-down svg{display:block;opacity:.6}details.dropdown .summary-up,details.dropdown .summary-down{pointer-events:none;position:absolute;right:1em;top:.75em}details.dropdown[open] .summary-down{visibility:hidden}details.dropdown:not([open]) .summary-up{visibility:hidden}details.dropdown.fade-in[open] summary~*{-moz-animation:panels-fade-in .5s ease-in-out;-webkit-animation:panels-fade-in .5s ease-in-out;animation:panels-fade-in .5s ease-in-out}details.dropdown.fade-in-slide-down[open] summary~*{-moz-animation:panels-fade-in .5s ease-in-out, panels-slide-down .5s ease-in-out;-webkit-animation:panels-fade-in .5s ease-in-out, panels-slide-down .5s ease-in-out;animation:panels-fade-in .5s ease-in-out, panels-slide-down .5s ease-in-out}@keyframes panels-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes panels-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.octicon{display:inline-block;fill:currentColor;vertical-align:text-top}.tabbed-content{box-shadow:0 -.0625rem var(--tabs-color-overline),0 .0625rem var(--tabs-color-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.tabbed-content>:first-child{margin-top:0 !important}.tabbed-content>:last-child{margin-bottom:0 !important}.tabbed-content>.tabbed-set{margin:0}.tabbed-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.tabbed-set>input{opacity:0;position:absolute}.tabbed-set>input:checked+label{border-color:var(--tabs-color-label-active);color:var(--tabs-color-label-active)}.tabbed-set>input:checked+label+.tabbed-content{display:block}.tabbed-set>input:focus+label{outline-style:auto}.tabbed-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.tabbed-set>label{border-bottom:.125rem solid transparent;color:var(--tabs-color-label-inactive);cursor:pointer;font-size:var(--tabs-size-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .tabbed-set>label:hover{color:var(--tabs-color-label-active)} 2 | -------------------------------------------------------------------------------- /sphinx_panels/button.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import unquote 2 | 3 | from docutils import nodes 4 | from docutils.utils import unescape 5 | from docutils.parsers.rst import directives 6 | from sphinx import addnodes 7 | from sphinx.util.docutils import SphinxDirective 8 | 9 | from .utils import string_to_func_inputs 10 | 11 | 12 | def setup_link_button(app): 13 | app.add_directive("link-button", LinkButton) 14 | # TODO hide badges in non-HTML? 15 | app.add_role("badge", badge_role) 16 | app.add_role("link-badge", link_badge_role) 17 | 18 | 19 | def create_ref_node(link_type, uri, text, tooltip): 20 | innernode = nodes.inline(text, text) 21 | if link_type == "ref": 22 | ref_node = addnodes.pending_xref( 23 | reftarget=unquote(uri), 24 | reftype="any", 25 | # refdoc=self.env.docname, 26 | refdomain="", 27 | refexplicit=True, 28 | refwarn=True, 29 | ) 30 | innernode["classes"] = ["xref", "any"] 31 | # if tooltip: 32 | # ref_node["reftitle"] = tooltip 33 | # ref_node["title"] = tooltip 34 | # TODO this doesn't work 35 | else: 36 | ref_node = nodes.reference() 37 | ref_node["refuri"] = uri 38 | if tooltip: 39 | ref_node["reftitle"] = tooltip 40 | ref_node += innernode 41 | return ref_node 42 | 43 | 44 | class LinkButton(SphinxDirective): 45 | """A directive to turn a link into a button.""" 46 | 47 | has_content = False 48 | required_arguments = 1 49 | final_argument_whitespace = True 50 | option_spec = { 51 | "type": lambda arg: directives.choice(arg, ("url", "ref")), 52 | "text": directives.unchanged, 53 | "tooltip": directives.unchanged, 54 | "classes": directives.unchanged, 55 | } 56 | 57 | def run(self): 58 | 59 | uri = self.arguments[0] 60 | link_type = self.options.get("type", "url") 61 | 62 | text = self.options.get("text", uri) 63 | 64 | ref_node = create_ref_node( 65 | link_type, uri, text, self.options.get("tooltip", None) 66 | ) 67 | self.set_source_info(ref_node) 68 | ref_node["classes"] = ["sphinx-bs", "btn", "text-wrap"] + self.options.get( 69 | "classes", "" 70 | ).split() 71 | 72 | # sphinx requires that a reference be inside a block element 73 | container = nodes.paragraph() 74 | container += ref_node 75 | 76 | return [container] 77 | 78 | 79 | def get_badge_inputs(text, cls: str = ""): 80 | return text, cls.split() 81 | 82 | 83 | def badge_role(role, rawtext, text, lineno, inliner, options={}, content=[]): 84 | try: 85 | args, kwargs = string_to_func_inputs(text) 86 | text, classes = get_badge_inputs(*args, **kwargs) 87 | except Exception as err: 88 | msg = inliner.reporter.error(f"badge input is invalid: {err}", line=lineno) 89 | prb = inliner.problematic(rawtext, rawtext, msg) 90 | return [prb], [msg] 91 | node = nodes.inline( 92 | rawtext, unescape(text), classes=["sphinx-bs", "badge"] + classes 93 | ) 94 | # textnodes, messages = inliner.parse(text, lineno, node, memo) 95 | # TODO this would require the memo with reporter, document and language 96 | return [node], [] 97 | 98 | 99 | def get_link_badge_inputs(link, text=None, type="link", cls: str = "", tooltip=None): 100 | return link, text or link, type, cls.split(), tooltip 101 | 102 | 103 | def link_badge_role(role, rawtext, text, lineno, inliner, options={}, content=[]): 104 | try: 105 | args, kwargs = string_to_func_inputs(text) 106 | uri, text, link_type, classes, tooltip = get_link_badge_inputs(*args, **kwargs) 107 | except Exception as err: 108 | msg = inliner.reporter.error(f"badge input is invalid: {err}", line=lineno) 109 | prb = inliner.problematic(rawtext, rawtext, msg) 110 | return [prb], [msg] 111 | ref_node = create_ref_node(link_type, uri, text, tooltip) 112 | if lineno is not None: 113 | ref_node.source, ref_node.line = inliner.reporter.get_source_and_line(lineno) 114 | ref_node["classes"] = ["sphinx-bs", "badge"] + classes 115 | return [ref_node], [] 116 | -------------------------------------------------------------------------------- /sphinx_panels/data/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GitHub Inc. 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 | -------------------------------------------------------------------------------- /sphinx_panels/dropdown.py: -------------------------------------------------------------------------------- 1 | """Originally Adapted from sphinxcontrib.details.directive 2 | """ 3 | from docutils import nodes 4 | from docutils.parsers.rst import directives 5 | from sphinx.util.docutils import SphinxDirective 6 | from sphinx.transforms.post_transforms import SphinxPostTransform 7 | from sphinx.util.nodes import NodeMatcher 8 | 9 | from .icons import get_opticon 10 | 11 | 12 | def setup_dropdown(app): 13 | app.add_node(dropdown_main, html=(visit_dropdown_main, depart_dropdown_main)) 14 | app.add_node(dropdown_title, html=(visit_dropdown_title, depart_dropdown_title)) 15 | app.add_directive("dropdown", DropdownDirective) 16 | app.add_post_transform(DropdownHtmlTransform) 17 | 18 | 19 | class dropdown_main(nodes.Element, nodes.General): 20 | pass 21 | 22 | 23 | class dropdown_title(nodes.TextElement, nodes.General): 24 | pass 25 | 26 | 27 | def visit_dropdown_main(self, node): 28 | if node.get("opened"): 29 | self.body.append(self.starttag(node, "details", open="open")) 30 | else: 31 | self.body.append(self.starttag(node, "details")) 32 | 33 | 34 | def depart_dropdown_main(self, node): 35 | self.body.append("") 36 | 37 | 38 | def visit_dropdown_title(self, node): 39 | self.body.append(self.starttag(node, "summary")) 40 | 41 | 42 | def depart_dropdown_title(self, node): 43 | self.body.append("") 44 | 45 | 46 | class DropdownDirective(SphinxDirective): 47 | optional_arguments = 1 48 | final_argument_whitespace = True 49 | has_content = True 50 | option_spec = { 51 | "container": directives.unchanged, 52 | "title": directives.unchanged, 53 | "body": directives.unchanged, 54 | "open": directives.flag, 55 | "name": directives.unchanged, 56 | "animate": lambda a: directives.choice(a, ("fade-in", "fade-in-slide-down")), 57 | } 58 | 59 | def run(self): 60 | 61 | # default classes 62 | classes = { 63 | "container_classes": ["mb-3"], 64 | "title_classes": [], 65 | "body_classes": [], 66 | } 67 | 68 | # add classes from options 69 | for element in ["container", "title", "body"]: 70 | if element not in self.options: 71 | continue 72 | value = self.options.get(element).strip() 73 | if value.startswith("+"): 74 | classes.setdefault(element + "_classes", []).extend(value[1:].split()) 75 | else: 76 | classes[element + "_classes"] = value.split() 77 | 78 | # add animation classes 79 | if ( 80 | "animate" in self.options 81 | and self.options["animate"] not in classes["container_classes"] 82 | ): 83 | classes["container_classes"].append(self.options["animate"]) 84 | 85 | container = nodes.container( 86 | "", 87 | opened="open" in self.options, 88 | type="dropdown", 89 | has_title=len(self.arguments) > 0, 90 | **classes 91 | ) 92 | if self.arguments: 93 | textnodes, messages = self.state.inline_text(self.arguments[0], self.lineno) 94 | container += nodes.paragraph(self.arguments[0], "", *textnodes) 95 | container += messages 96 | self.state.nested_parse(self.content, self.content_offset, container) 97 | self.add_name(container) 98 | return [container] 99 | 100 | 101 | KEBAB = """\ 102 | """ 110 | 111 | 112 | class DropdownHtmlTransform(SphinxPostTransform): 113 | default_priority = 200 114 | formats = ("html",) 115 | 116 | def run(self): 117 | matcher = NodeMatcher(nodes.container, type="dropdown") 118 | for node in self.document.traverse(matcher): 119 | 120 | open_marker = nodes.container( 121 | "", 122 | nodes.raw( 123 | "", nodes.Text(get_opticon("chevron-up", size=24)), format="html" 124 | ), 125 | is_div=True, 126 | classes=["summary-up"], 127 | ) 128 | closed_marker = nodes.container( 129 | "", 130 | nodes.raw( 131 | "", nodes.Text(get_opticon("chevron-down", size=24)), format="html" 132 | ), 133 | is_div=True, 134 | classes=["summary-down"], 135 | ) 136 | 137 | newnode = dropdown_main( 138 | opened=node["opened"], 139 | classes=["sphinx-bs", "dropdown", "card"] + node["container_classes"], 140 | ) 141 | 142 | if node["has_title"]: 143 | title_children = node[0] 144 | body_children = node[1:] 145 | else: 146 | title_children = [ 147 | nodes.raw( 148 | "...", 149 | nodes.Text( 150 | KEBAB 151 | # Note the custom opticon here has thicker dots 152 | # get_opticon("kebab-horizontal", classes="no-title", 153 | # size=24) 154 | ), 155 | format="html", 156 | ) 157 | ] 158 | body_children = node 159 | 160 | newnode += dropdown_title( 161 | "", 162 | "", 163 | *title_children, 164 | closed_marker, 165 | open_marker, 166 | classes=["summary-title", "card-header"] + node["title_classes"] 167 | ) 168 | body_node = nodes.container( 169 | "", 170 | *body_children, 171 | is_div=True, 172 | classes=["summary-content", "card-body"] + node["body_classes"] 173 | ) 174 | for para in body_node.traverse(nodes.paragraph): 175 | para["classes"] = ([] if "classes" in para else para["classes"]) + [ 176 | "card-text" 177 | ] 178 | newnode += body_node 179 | # newnode += open_marker 180 | node.replace_self(newnode) 181 | -------------------------------------------------------------------------------- /sphinx_panels/icons.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | import json 3 | from pathlib import Path 4 | 5 | from docutils import nodes 6 | 7 | from .utils import string_to_func_inputs 8 | 9 | 10 | OPTICON_VERSION = "0.0.0-dd899ea" 11 | 12 | OPTICON_CSS = """\ 13 | .octicon { 14 | display: inline-block; 15 | vertical-align: text-top; 16 | fill: currentColor; 17 | }""" 18 | 19 | 20 | @lru_cache(1) 21 | def get_opticon_data(): 22 | path = Path(__file__).parent.joinpath("data", "opticons.json") 23 | return json.loads(path.read_text()) 24 | 25 | 26 | def list_opticons(): 27 | return list(get_opticon_data().keys()) 28 | 29 | 30 | def get_opticon( 31 | name: str, 32 | classes: str = None, 33 | width: int = None, 34 | height: int = None, 35 | aria_label: str = None, 36 | size: int = 16, 37 | ): 38 | assert size in [16, 24], "size must be 16 or 24" 39 | try: 40 | data = get_opticon_data()[name] 41 | except KeyError: 42 | raise KeyError(f"Unrecognised opticon: {name}") 43 | 44 | content = data["heights"][str(size)]["path"] 45 | options = { 46 | "version": "1.1", 47 | "width": data["heights"][str(size)]["width"], 48 | "height": int(size), 49 | "class": f"octicon octicon-{name}", 50 | } 51 | 52 | if width is not None or height is not None: 53 | if width is None: 54 | width = round((int(height) * options["width"]) / options["height"], 2) 55 | if height is None: 56 | height = round((int(width) * options["height"]) / options["width"], 2) 57 | options["width"] = width 58 | options["height"] = height 59 | 60 | options["viewBox"] = f'0 0 {options["width"]} {options["height"]}' 61 | 62 | if classes is not None: 63 | options["class"] += " " + classes.strip() 64 | 65 | if aria_label is not None: 66 | options["aria-label"] = aria_label 67 | options["role"] = "img" 68 | else: 69 | options["aria-hidden"] = "true" 70 | 71 | opt_string = " ".join(f'{k}="{v}"' for k, v in options.items()) 72 | return f"{content}" 73 | 74 | 75 | def opticon_role( 76 | role, rawtext: str, text: str, lineno, inliner, options={}, content=[] 77 | ): 78 | try: 79 | args, kwargs = string_to_func_inputs(text) 80 | svg = get_opticon(*args, **kwargs) 81 | except Exception as err: 82 | msg = inliner.reporter.error(f"Opticon input is invalid: {err}", line=lineno) 83 | prb = inliner.problematic(rawtext, rawtext, msg) 84 | return [prb], [msg] 85 | node = nodes.raw("", nodes.Text(svg), format="html") 86 | return [node], [] 87 | 88 | 89 | class fontawesome(nodes.Element, nodes.General): 90 | pass 91 | 92 | 93 | def create_fa_node(name, classes: str = None, style="fa"): 94 | assert style.startswith("fa"), "style must be a valid prefix, e.g. fa, fas, etc" 95 | return fontawesome( 96 | icon_name=name, 97 | classes=[style, f"fa-{name}"] + (classes.split() if classes else []), 98 | ) 99 | 100 | 101 | def fontawesome_role(role, rawtext, text, lineno, inliner, options={}, content=[]): 102 | try: 103 | args, kwargs = string_to_func_inputs(text) 104 | node = create_fa_node(*args, **kwargs) 105 | except Exception as err: 106 | msg = inliner.reporter.error( 107 | f"FontAwesome input is invalid: {err}", line=lineno 108 | ) 109 | prb = inliner.problematic(rawtext, rawtext, msg) 110 | return [prb], [msg] 111 | return [node], [] 112 | 113 | 114 | def visit_fontawesome_html(self, node): 115 | self.body.append(self.starttag(node, "span", "")) 116 | 117 | 118 | def depart_fontawesome_html(self, node): 119 | self.body.append("") 120 | 121 | 122 | def visit_fontawesome_latex(self, node): 123 | if self.config.panels_add_fontawesome_latex: 124 | self.body.append(f"\\faicon{{{node['icon_name']}}}") 125 | raise nodes.SkipNode 126 | 127 | 128 | def add_fontawesome_pkg(app, config): 129 | if app.config.panels_add_fontawesome_latex: 130 | app.add_latex_package("fontawesome") 131 | 132 | 133 | def setup_icons(app): 134 | app.add_role("opticon", opticon_role) 135 | app.add_role("fa", fontawesome_role) 136 | 137 | app.add_config_value("panels_add_fontawesome_latex", False, "env") 138 | app.connect("config-inited", add_fontawesome_pkg) 139 | app.add_node( 140 | fontawesome, 141 | html=(visit_fontawesome_html, depart_fontawesome_html), 142 | latex=(visit_fontawesome_latex, None), 143 | text=(None, None), 144 | man=(None, None), 145 | texinfo=(None, None), 146 | ) 147 | -------------------------------------------------------------------------------- /sphinx_panels/panels.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from docutils import nodes 4 | from docutils.parsers.rst import directives 5 | from sphinx.util.docutils import SphinxDirective 6 | 7 | DEFAULT_CONTAINER = "container pb-4" 8 | DEFAULT_COLUMN = "col-lg-6 col-md-6 col-sm-6 col-xs-12 p-2" 9 | DEFAULT_CARD = "shadow" 10 | 11 | RE_OPTIONS = re.compile( 12 | r"\:(column|card|body|header|footer|" 13 | r"img-top|img-bottom|img-top-cls|img-bottom-cls)\:\s*(\+?)\s*(.*)" 14 | ) 15 | 16 | 17 | def setup_panels(app): 18 | app.add_directive("panels", Panels) 19 | app.add_config_value( 20 | "panels_delimiters", (r"^\-{3,}$", r"^\^{3,}$", r"^\+{3,}$"), "env" 21 | ) 22 | app.connect("config-inited", validate_config) 23 | 24 | 25 | def parse_panels( 26 | content, 27 | content_offset, 28 | default_classes, 29 | panel_regex=None, 30 | head_regex=None, 31 | foot_regex=None, 32 | ): 33 | """split a block of content into panels. 34 | 35 | example:: 36 | 37 | --- 38 | header 39 | === 40 | body 41 | ... 42 | footer 43 | --- 44 | next panel 45 | 46 | """ 47 | panel_regex = panel_regex or re.compile(r"^\-{3,}$") 48 | head_regex = head_regex or re.compile(r"^\^{3,}$") 49 | foot_regex = foot_regex or re.compile(r"^\+{3,}$") 50 | 51 | if isinstance(content, str): 52 | content = content.splitlines() 53 | 54 | panel_blocks = [] 55 | start_line = 0 56 | header_split = footer_split = None 57 | for i, line in enumerate(content): 58 | if panel_regex.match(line.strip()): 59 | if i != 0: 60 | panel_blocks.append( 61 | parse_single_panel( 62 | content[start_line:i], 63 | start_line, 64 | header_split, 65 | footer_split, 66 | content_offset, 67 | default_classes, 68 | ) 69 | ) 70 | start_line = i + 1 71 | header_split = footer_split = None 72 | if head_regex.match(line.strip()) and footer_split is None: 73 | header_split = i - start_line 74 | if foot_regex.match(line.strip()): 75 | footer_split = i - start_line 76 | # TODO warn if multiple header_split or footer_split 77 | # TODO assert header_split is before footer_split 78 | try: 79 | panel_blocks.append( 80 | parse_single_panel( 81 | content[start_line:], 82 | start_line, 83 | header_split, 84 | footer_split, 85 | content_offset, 86 | default_classes, 87 | ) 88 | ) 89 | except IndexError: 90 | pass 91 | return panel_blocks 92 | 93 | 94 | def parse_single_panel( 95 | content, offset, header_split, footer_split, content_offset, default_classes 96 | ): 97 | """parse each panel data to dict.""" 98 | output = {} 99 | body_start = 0 100 | body_end = len(content) 101 | 102 | # parse the classes required for this panel, and top/bottom images 103 | classes = default_classes.copy() 104 | for opt_offset, line in enumerate(content): 105 | opt_match = RE_OPTIONS.match(line) 106 | if not opt_match: 107 | break 108 | body_start += 1 109 | if opt_match.group(1) in ["img-top", "img-bottom"]: 110 | output[opt_match.group(1)] = opt_match.group(3) 111 | continue 112 | if opt_match.group(2) == "+": 113 | classes[opt_match.group(1)] = ( 114 | classes.get(opt_match.group(1), []) + opt_match.group(3).split() 115 | ) 116 | else: 117 | classes[opt_match.group(1)] = opt_match.group(3).split() 118 | 119 | if classes: 120 | output["classes"] = classes 121 | 122 | if header_split is not None: 123 | header_content = content[opt_offset:header_split] 124 | header_offset = content_offset + offset + opt_offset 125 | body_start = header_split + 1 126 | output["header"] = (header_content, header_offset) 127 | 128 | if footer_split is not None: 129 | footer_content = content[footer_split + 1 :] 130 | footer_offset = content_offset + offset + footer_split 131 | body_end = footer_split 132 | output["footer"] = (footer_content, footer_offset) 133 | 134 | body_content = content[body_start:body_end] 135 | body_offset = content_offset + offset + body_start 136 | output["body"] = (body_content, body_offset) 137 | return output 138 | 139 | 140 | def add_child_classes(node): 141 | """Add classes to specific child nodes.""" 142 | for para in node.traverse(nodes.paragraph): 143 | para["classes"] = ([] if "classes" in para else para["classes"]) + ["card-text"] 144 | for title in node.traverse(nodes.title): 145 | title["classes"] = ([] if "classes" in title else title["classes"]) + [ 146 | "card-title" 147 | ] 148 | 149 | 150 | class Panels(SphinxDirective): 151 | """Two Column Panels.""" 152 | 153 | has_content = True 154 | option_spec = { 155 | "container": directives.unchanged, 156 | "column": directives.unchanged, 157 | "card": directives.unchanged, 158 | "body": directives.unchanged, 159 | "header": directives.unchanged, 160 | "footer": directives.unchanged, 161 | "img-top-cls": directives.unchanged, 162 | "img-bottom-cls": directives.unchanged, 163 | } 164 | 165 | def run(self): 166 | default_classes = { 167 | "container": DEFAULT_CONTAINER.split(), 168 | "column": DEFAULT_COLUMN.split(), 169 | "card": DEFAULT_CARD.split(), 170 | "body": [], 171 | "header": [], 172 | "footer": [], 173 | "img-top-cls": [], 174 | "img-bottom-cls": [], 175 | } 176 | 177 | # set classes from the directive options 178 | for key, value in default_classes.items(): 179 | if key not in self.options: 180 | continue 181 | option_value = self.options[key].strip() 182 | if option_value.startswith("+"): 183 | default_classes[key] += option_value[1:].split() 184 | else: 185 | default_classes[key] = option_value.split() 186 | 187 | # split the block into panels 188 | panel_blocks = parse_panels( 189 | self.content, 190 | self.content_offset, 191 | default_classes, 192 | panel_regex=self.env.app.config.panels_delimiters[0], 193 | head_regex=self.env.app.config.panels_delimiters[1], 194 | foot_regex=self.env.app.config.panels_delimiters[2], 195 | ) 196 | 197 | # set the top-level containers 198 | parent = nodes.container( 199 | is_div=True, classes=["sphinx-bs"] + default_classes["container"] 200 | ) 201 | rows = nodes.container(is_div=True, classes=["row"]) 202 | parent += rows 203 | 204 | for data in panel_blocks: 205 | 206 | classes = data["classes"] 207 | 208 | column = nodes.container( 209 | is_div=True, classes=["d-flex"] + classes["column"] 210 | ) 211 | rows += column 212 | card = nodes.container( 213 | is_div=True, classes=["card", "w-100"] + classes["card"] 214 | ) 215 | column += card 216 | 217 | if "img-top" in data: 218 | image_top = nodes.image( 219 | "", 220 | uri=directives.uri(data["img-top"]), 221 | alt="img-top", 222 | classes=["card-img-top"] + classes["img-top-cls"], 223 | ) 224 | self.add_name(image_top) 225 | card += image_top 226 | 227 | if "header" in data: 228 | header = nodes.container( 229 | is_div=True, classes=["card-header"] + classes["header"] 230 | ) 231 | card += header 232 | 233 | header_content, header_offset = data["header"] 234 | self.state.nested_parse(header_content, header_offset, header) 235 | add_child_classes(header) 236 | 237 | body = nodes.container(is_div=True, classes=["card-body"] + classes["body"]) 238 | card += body 239 | 240 | body_content, body_offset = data["body"] 241 | self.state.nested_parse(body_content, body_offset, body) 242 | add_child_classes(body) 243 | 244 | if "footer" in data: 245 | footer = nodes.container( 246 | is_div=True, classes=["card-footer"] + classes["footer"] 247 | ) 248 | card += footer 249 | 250 | footer_content, footer_offset = data["footer"] 251 | self.state.nested_parse(footer_content, footer_offset, footer) 252 | add_child_classes(footer) 253 | 254 | if "img-bottom" in data: 255 | image_top = nodes.image( 256 | "", 257 | uri=directives.uri(data["img-bottom"]), 258 | alt="img-bottom", 259 | classes=["card-img-bottom"] + classes["img-bottom-cls"], 260 | ) 261 | self.add_name(image_top) 262 | card += image_top 263 | 264 | return [parent] 265 | 266 | 267 | def validate_config(app, config): 268 | if len(app.config.panels_delimiters) != 3: 269 | raise AssertionError( 270 | "panels_delimiters config must be of form: (header, body, footer)" 271 | ) 272 | if len(set(app.config.panels_delimiters)) != 3: 273 | raise AssertionError("panels_delimiters config must contain unique values") 274 | try: 275 | app.config.panels_delimiters = tuple( 276 | [re.compile(s) for s in app.config.panels_delimiters] 277 | ) 278 | except Exception as err: 279 | raise AssertionError( 280 | "panels_delimiters config must contain only compilable regexes: {}".format( 281 | err 282 | ) 283 | ) 284 | -------------------------------------------------------------------------------- /sphinx_panels/scss/bootstrap/_badge.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap v4.4.1 (https://getbootstrap.com/) 2 | // Copyright 2011-2019 The Bootstrap Authors 3 | // Copyright 2011-2019 Twitter, Inc. 4 | // Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | 6 | .badge { 7 | border-radius: .25rem; 8 | display: inline-block; 9 | font-size: 75%; 10 | font-weight: 700; 11 | line-height: 1; 12 | padding: .25em .4em; 13 | text-align: center; 14 | vertical-align: baseline; 15 | white-space: nowrap; 16 | 17 | // Empty badges collapse automatically 18 | &:empty { 19 | display: none; 20 | } 21 | } 22 | 23 | // Quick fix for badges in buttons 24 | .btn .badge { 25 | position: relative; 26 | top: -1px; 27 | } 28 | 29 | .badge-pill { 30 | border-radius: 10rem; 31 | padding-left: .6em; 32 | padding-right: .6em; 33 | } 34 | 35 | @each $name, $color in $semantic-colors { 36 | .badge-#{$name} { 37 | background-color: $color; 38 | color: text-color($name); 39 | } 40 | 41 | .badge-#{$name}[href]:focus, 42 | .badge-#{$name}[href]:hover { 43 | background-color: darken($color, 10%); 44 | color: text-color($name); 45 | text-decoration: none; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sphinx_panels/scss/bootstrap/_borders.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap v4.4.1 (https://getbootstrap.com/) 2 | // Copyright 2011-2019 The Bootstrap Authors 3 | // Copyright 2011-2019 Twitter, Inc. 4 | // Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | 6 | .border-0 { 7 | border: 0 !important; 8 | } 9 | 10 | .border-top-0 { 11 | border-top: 0 !important; 12 | } 13 | 14 | .border-right-0 { 15 | border-right: 0 !important; 16 | } 17 | 18 | .border-bottom-0 { 19 | border-bottom: 0 !important; 20 | } 21 | 22 | .border-left-0 { 23 | border-left: 0 !important; 24 | } 25 | 26 | $pad-width: ( 27 | "0": 0, 28 | "1": .25rem, 29 | "2": .5rem, 30 | "3": 1rem, 31 | "4": 1.5rem, 32 | "5": 3rem, 33 | ) !default; 34 | 35 | @each $id, $value in $pad-width { 36 | .p-#{$id} { 37 | padding: $value !important; 38 | } 39 | 40 | .pt-#{$id}, 41 | .py-#{$id} { 42 | padding-top: $value !important; 43 | } 44 | 45 | .pr-#{$id}, 46 | .px-#{$id} { 47 | padding-right: $value !important; 48 | } 49 | 50 | .pb-#{$id}, 51 | .py-#{$id} { 52 | padding-bottom: $value !important; 53 | } 54 | 55 | .pl-#{$id}, 56 | .px-#{$id} { 57 | padding-left: $value !important; 58 | } 59 | } 60 | 61 | @each $id, $value in $pad-width { 62 | .m-#{$id} { 63 | margin: $value !important; 64 | } 65 | 66 | .mt-#{$id}, 67 | .my-#{$id} { 68 | margin-top: $value !important; 69 | } 70 | 71 | .mr-#{$id}, 72 | .mx-#{$id} { 73 | margin-right: $value !important; 74 | } 75 | 76 | .mb-#{$id}, 77 | .my-#{$id} { 78 | margin-bottom: $value !important; 79 | } 80 | 81 | .ml-#{$id}, 82 | .mx-#{$id} { 83 | margin-left: $value !important; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /sphinx_panels/scss/bootstrap/_buttons.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap v4.4.1 (https://getbootstrap.com/) 2 | // Copyright 2011-2019 The Bootstrap Authors 3 | // Copyright 2011-2019 Twitter, Inc. 4 | // Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | 6 | .btn { 7 | background-color: transparent; 8 | border: 1px solid transparent; 9 | border-radius: .25rem; 10 | color: $gray-900; 11 | cursor: pointer; 12 | display: inline-block; 13 | font-size: 1rem; 14 | font-weight: 400; 15 | line-height: 1.5; 16 | padding: .375rem .75rem; 17 | text-align: center; 18 | transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out; 19 | -moz-user-select: none; 20 | -ms-user-select: none; 21 | -webkit-user-select: none; 22 | user-select: none; 23 | vertical-align: middle; 24 | 25 | &:hover { 26 | color: $gray-900; 27 | text-decoration: none; 28 | } 29 | 30 | &:visited { 31 | color: $gray-900; 32 | } 33 | 34 | &.focus, 35 | &:focus { 36 | box-shadow: 0 0 0 $btn-focus-width rgba(mix($blue, $blue, 15%), .25); 37 | outline: 0; 38 | } 39 | 40 | &.disabled, 41 | &:disabled { 42 | opacity: .65; 43 | } 44 | } 45 | 46 | @media (prefers-reduced-motion:reduce) { 47 | .btn { 48 | transition: none; 49 | } 50 | } 51 | 52 | a.btn.disabled, 53 | fieldset:disabled a.btn { 54 | pointer-events: none; 55 | } 56 | 57 | @each $name, $color in $semantic-colors { 58 | .btn-#{$name} { 59 | background-color: $color; 60 | border-color: $color; 61 | color: text-color($name); 62 | 63 | &:visited { 64 | color: text-color($name); 65 | } 66 | 67 | &:hover { 68 | background-color: darken($color, 7.5%); 69 | border-color: darken($color, 10%); 70 | color: text-color($name); 71 | } 72 | 73 | &.focus, 74 | &:focus { 75 | background-color: darken($color, 7.5%); 76 | border-color: darken($color, 10%); 77 | box-shadow: 0 0 0 $btn-focus-width rgba(mix($color, $color, 15%), .5); 78 | color: text-color($name); 79 | } 80 | 81 | &.disabled, 82 | &:disabled { 83 | background-color: $color; 84 | border-color: $color; 85 | color: text-color($name); 86 | } 87 | 88 | &:not(:disabled):not(.disabled).active, 89 | &:not(:disabled):not(.disabled):active, 90 | .show>&.dropdown-toggle { 91 | background-color: darken($color, 10%); 92 | border-color: darken($color, 12.5%); 93 | color: text-color($name); 94 | } 95 | 96 | &:not(:disabled):not(.disabled).active:focus, 97 | &:not(:disabled):not(.disabled):active:focus, 98 | .show>&.dropdown-toggle:focus { 99 | box-shadow: 0 0 0 $btn-focus-width rgba(mix($color, $color, 15%), .5); 100 | } 101 | } 102 | } 103 | 104 | @each $name, $color in $semantic-colors { 105 | .btn-outline-#{$name} { 106 | border-color: $color; 107 | color: $color; 108 | 109 | &:visited { 110 | color: $color; 111 | } 112 | 113 | &:hover { 114 | background-color: $color; 115 | border-color: $color; 116 | color: text-color($name); 117 | } 118 | 119 | &.focus, 120 | &:focus { 121 | box-shadow: 0 0 0 $btn-focus-width rgba(mix($color, $color, 15%), .5); 122 | } 123 | 124 | &.disabled, 125 | &:disabled { 126 | background-color: transparent; 127 | color: $color; 128 | } 129 | 130 | &:not(:disabled):not(.disabled).active, 131 | &:not(:disabled):not(.disabled):active, 132 | .show>&.dropdown-toggle { 133 | background-color: $color; 134 | border-color: $color; 135 | color: text-color($name); 136 | } 137 | 138 | &:not(:disabled):not(.disabled).active:focus, 139 | &:not(:disabled):not(.disabled):active:focus, 140 | .show>&.dropdown-toggle:focus { 141 | box-shadow: 0 0 0 $btn-focus-width rgba(mix($color, $color, 15%), .5); 142 | } 143 | } 144 | } 145 | 146 | .btn-link { 147 | color: $blue; 148 | font-weight: 400; 149 | text-decoration: none; 150 | 151 | &:hover { 152 | color: darken($blue, 15%); 153 | text-decoration: underline; 154 | } 155 | 156 | &.focus, 157 | &:focus { 158 | box-shadow: none; 159 | text-decoration: underline; 160 | } 161 | 162 | &.disabled, 163 | &:disabled { 164 | color: $gray-600; 165 | pointer-events: none; 166 | } 167 | } 168 | 169 | .btn-group-lg>.btn, 170 | .btn-lg { 171 | border-radius: .3rem; 172 | font-size: 1.25rem; 173 | line-height: 1.5; 174 | padding: .5rem 1rem; 175 | } 176 | 177 | .btn-group-sm>.btn, 178 | .btn-sm { 179 | border-radius: .2rem; 180 | font-size: .875rem; 181 | line-height: 1.5; 182 | padding: .25rem .5rem; 183 | } 184 | 185 | .btn-block { 186 | display: block; 187 | width: 100%; 188 | } 189 | 190 | .btn-block+.btn-block { 191 | margin-top: .5rem; 192 | } 193 | 194 | input[type=button].btn-block, 195 | input[type=reset].btn-block, 196 | input[type=submit].btn-block { 197 | width: 100%; 198 | } 199 | 200 | .stretched-link::after { 201 | background-color: $black-0; 202 | bottom: 0; 203 | content: ''; 204 | left: 0; 205 | pointer-events: auto; 206 | position: absolute; 207 | right: 0; 208 | top: 0; 209 | z-index: 1; 210 | } 211 | 212 | .text-wrap { 213 | white-space: normal !important; 214 | } 215 | -------------------------------------------------------------------------------- /sphinx_panels/scss/bootstrap/_cards.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap v4.4.1 (https://getbootstrap.com/) 2 | // Copyright 2011-2019 The Bootstrap Authors 3 | // Copyright 2011-2019 Twitter, Inc. 4 | // Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | 6 | .card { 7 | background-clip: border-box; 8 | background-color: $white; 9 | border: 1px solid $black-12-5; 10 | border-radius: .25rem; 11 | display: -ms-flexbox; 12 | display: flex; 13 | -ms-flex-direction: column; 14 | flex-direction: column; 15 | min-width: 0; 16 | position: relative; 17 | word-wrap: break-word; 18 | 19 | >hr { 20 | margin-left: 0; 21 | margin-right: 0; 22 | } 23 | 24 | >.list-group:first-child .list-group-item:first-child { 25 | border-top-left-radius: .25rem; 26 | border-top-right-radius: .25rem; 27 | } 28 | 29 | >.list-group:last-child .list-group-item:last-child { 30 | border-bottom-left-radius: .25rem; 31 | border-bottom-right-radius: .25rem; 32 | } 33 | } 34 | 35 | .card-body { 36 | -ms-flex: 1 1 auto; 37 | flex: 1 1 auto; 38 | min-height: 1px; 39 | padding: 1.25rem; 40 | } 41 | 42 | .card-title { 43 | margin-bottom: .75rem; 44 | } 45 | 46 | .card-subtitle { 47 | margin-bottom: 0; 48 | margin-top: -.375rem; 49 | } 50 | 51 | .card-text:last-child { 52 | margin-bottom: 0; 53 | } 54 | 55 | .card-link:hover { 56 | text-decoration: none; 57 | } 58 | 59 | .card-link+.card-link { 60 | margin-left: 1.25rem; 61 | } 62 | 63 | .card-header { 64 | background-color: $black-3; 65 | border-bottom: 1px solid $black-12-5; 66 | margin-bottom: 0; 67 | padding: .75rem 1.25rem; 68 | } 69 | 70 | .card-header:first-child { 71 | border-radius: calc(.25rem - 1px) calc(.25rem - 1px) 0 0; 72 | } 73 | 74 | .card-header+.list-group .list-group-item:first-child { 75 | border-top: 0; 76 | } 77 | 78 | .card-footer { 79 | background-color: $black-3; 80 | border-top: 1px solid $black-12-5; 81 | padding: .75rem 1.25rem; 82 | } 83 | 84 | .card-footer:last-child { 85 | border-radius: 0 0 calc(.25rem - 1px) calc(.25rem - 1px); 86 | } 87 | 88 | .card-header-tabs { 89 | border-bottom: 0; 90 | margin-bottom: -.75rem; 91 | margin-left: -.625rem; 92 | margin-right: -.625rem; 93 | } 94 | 95 | .card-header-pills { 96 | margin-left: -.625rem; 97 | margin-right: -.625rem; 98 | } 99 | 100 | .card-img-overlay { 101 | bottom: 0; 102 | left: 0; 103 | padding: 1.25rem; 104 | position: absolute; 105 | right: 0; 106 | top: 0; 107 | } 108 | 109 | .card-img, 110 | .card-img-bottom, 111 | .card-img-top { 112 | -ms-flex-negative: 0; 113 | flex-shrink: 0; 114 | width: 100%; 115 | } 116 | 117 | .card-img, 118 | .card-img-top { 119 | border-top-left-radius: calc(.25rem - 1px); 120 | border-top-right-radius: calc(.25rem - 1px); 121 | } 122 | 123 | .card-img, 124 | .card-img-bottom { 125 | border-bottom-left-radius: calc(.25rem - 1px); 126 | border-bottom-right-radius: calc(.25rem - 1px); 127 | } 128 | 129 | .w-100 { 130 | width: 100% !important; 131 | } 132 | 133 | .shadow { 134 | box-shadow: 0 .5rem 1rem $black-15 !important; 135 | } 136 | 137 | // scss-docs-start theme-colors-map 138 | $semantic-colors-text: map-merge( 139 | $semantic-colors, 140 | ( 141 | "white": $white, 142 | "body": $gray-900, 143 | "muted": $gray-600, 144 | "black-50": $black-50, 145 | "white-50": $white-50, 146 | ) 147 | ) !default; 148 | // scss-docs-end theme-colors-map 149 | 150 | @each $color, $value in map-merge($semantic-colors, ("white": $white)) { 151 | .bg-#{$color} { 152 | background-color: $value !important; 153 | } 154 | 155 | button { 156 | 157 | &.bg-#{$color}:focus, 158 | &.bg-#{$color}:hover { 159 | background-color: darken($value, 10%) !important; 160 | } 161 | } 162 | 163 | a { 164 | 165 | &.bg-#{$color}:focus, 166 | &.bg-#{$color}:hover { 167 | background-color: darken($value, 10%) !important; 168 | } 169 | 170 | &.text-#{$color}:focus, 171 | &.text-#{$color}:hover { 172 | color: darken($dark, 15%) !important; 173 | } 174 | } 175 | } 176 | 177 | @each $color, $value in $semantic-colors-text { 178 | .text-#{$color} { 179 | color: $value !important; 180 | } 181 | } 182 | 183 | .bg-transparent { 184 | background-color: transparent !important; 185 | } 186 | 187 | .text-justify { 188 | text-align: justify !important; 189 | } 190 | 191 | .text-left { 192 | text-align: left !important; 193 | } 194 | 195 | .text-right { 196 | text-align: right !important; 197 | } 198 | 199 | .text-center { 200 | text-align: center !important; 201 | } 202 | 203 | .font-weight-light { 204 | font-weight: 300 !important; 205 | } 206 | 207 | .font-weight-lighter { 208 | font-weight: lighter !important; 209 | } 210 | 211 | .font-weight-normal { 212 | font-weight: 400 !important; 213 | } 214 | 215 | .font-weight-bold { 216 | font-weight: 700 !important; 217 | } 218 | 219 | .font-weight-bolder { 220 | font-weight: bolder !important; 221 | } 222 | 223 | .font-italic { 224 | font-style: italic !important; 225 | } 226 | -------------------------------------------------------------------------------- /sphinx_panels/scss/bootstrap/_colors.scss: -------------------------------------------------------------------------------- 1 | $white: #fff !default; 2 | $white-50: rgba(255, 255, 255, .5) !default; 3 | $gray-100: #f8f9fa !default; 4 | $gray-200: #e9ecef !default; 5 | $gray-300: #dee2e6 !default; 6 | $gray-400: #ced4da !default; 7 | $gray-500: #adb5bd !default; 8 | $gray-600: #6c757d !default; 9 | $gray-700: #495057 !default; 10 | $gray-800: #343a40 !default; 11 | $gray-900: #212529 !default; 12 | $black: #000 !default; 13 | $black-0: rgba(0, 0, 0, 0) !default; 14 | $black-3: rgba(0, 0, 0, .03) !default; 15 | $black-12-5: rgba(0, 0, 0, .125) !default; 16 | $black-15: rgba(0, 0, 0, .15) !default; 17 | $black-50: rgba(0, 0, 0, .5) !default; 18 | 19 | // $blue: #0d6efd !default; 20 | $blue: #007bff !default; 21 | $indigo: #6610f2 !default; 22 | $purple: #6f42c1 !default; 23 | $pink: #d63384 !default; 24 | $red: #dc3545 !default; 25 | $orange: #fd7e14 !default; 26 | $yellow: #ffc107 !default; 27 | // $green: #198754 !default; 28 | $green: #28a745 !default; 29 | $teal: #20c997 !default; 30 | // $cyan: #0dcaf0 !default; 31 | $cyan: #17a2b8 !default; 32 | 33 | $primary: $blue !default; 34 | $secondary: $gray-600 !default; 35 | $success: $green !default; 36 | $info: $cyan !default; 37 | $warning: $yellow !default; 38 | $danger: $red !default; 39 | $light: $gray-100 !default; 40 | $dark: $gray-800 !default; 41 | 42 | // scss-docs-start theme-colors-map 43 | $semantic-colors: ( 44 | "primary": $primary, 45 | "secondary": $secondary, 46 | "success": $success, 47 | "info": $info, 48 | "warning": $warning, 49 | "danger": $danger, 50 | "light": $light, 51 | "dark": $dark, 52 | ) !default; 53 | // scss-docs-end theme-colors-map 54 | 55 | @function text-color($name) { 56 | // color: if(lightness($value) > 70, $gray-900, $white); 57 | @return if($name == 'light' or $name == 'warning', $gray-900, $white); 58 | } 59 | 60 | $btn-focus-width: .2rem !default; 61 | -------------------------------------------------------------------------------- /sphinx_panels/scss/bootstrap/_grids.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap v4.4.1 (https://getbootstrap.com/) 2 | // Copyright 2011-2019 The Bootstrap Authors 3 | // Copyright 2011-2019 Twitter, Inc. 4 | // Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | 6 | .container { 7 | margin-left: auto; 8 | margin-right: auto; 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | width: 100%; 12 | } 13 | 14 | $media-min-sm: 576px; 15 | $media-min-md: 768px; 16 | $media-min-lg: 992px; 17 | $media-min-xl: 1200px; 18 | 19 | $media-max-sm: 540px; 20 | $media-max-md: 720px; 21 | $media-max-lg: 960px; 22 | $media-max-xl: 1140px; 23 | 24 | $media-widths: ( 25 | 'sm': $media-min-sm, 26 | 'md': $media-min-md, 27 | 'lg': $media-min-lg, 28 | 'xl': $media-min-xl, 29 | ) !default; 30 | 31 | @media (min-width:$media-min-sm) { 32 | .container { 33 | max-width: $media-max-sm; 34 | } 35 | } 36 | 37 | @media (min-width:$media-min-md) { 38 | .container { 39 | max-width: $media-max-md; 40 | } 41 | } 42 | 43 | @media (min-width:$media-min-lg) { 44 | .container { 45 | max-width: $media-max-lg; 46 | } 47 | } 48 | 49 | @media (min-width:$media-min-xl) { 50 | .container { 51 | max-width: $media-max-xl; 52 | } 53 | } 54 | 55 | .container-fluid, 56 | .container-lg, 57 | .container-md, 58 | .container-sm, 59 | .container-xl { 60 | margin-left: auto; 61 | margin-right: auto; 62 | padding-left: 15px; 63 | padding-right: 15px; 64 | width: 100%; 65 | } 66 | 67 | @media (min-width:$media-min-sm) { 68 | 69 | .container, 70 | .container-sm { 71 | max-width: $media-max-sm; 72 | } 73 | } 74 | 75 | @media (min-width:$media-min-md) { 76 | 77 | .container, 78 | .container-md, 79 | .container-sm { 80 | max-width: $media-max-md; 81 | } 82 | } 83 | 84 | @media (min-width:$media-min-lg) { 85 | 86 | .container, 87 | .container-lg, 88 | .container-md, 89 | .container-sm { 90 | max-width: $media-max-lg; 91 | } 92 | } 93 | 94 | @media (min-width:$media-min-xl) { 95 | 96 | .container, 97 | .container-lg, 98 | .container-md, 99 | .container-sm, 100 | .container-xl { 101 | max-width: $media-max-xl; 102 | } 103 | } 104 | 105 | .row { 106 | display: -ms-flexbox; 107 | display: flex; 108 | -ms-flex-wrap: wrap; 109 | flex-wrap: wrap; 110 | margin-left: -15px; 111 | margin-right: -15px; 112 | } 113 | 114 | .col-lg, 115 | .col-lg-1, 116 | .col-lg-10, 117 | .col-lg-11, 118 | .col-lg-12, 119 | .col-lg-2, 120 | .col-lg-3, 121 | .col-lg-4, 122 | .col-lg-5, 123 | .col-lg-6, 124 | .col-lg-7, 125 | .col-lg-8, 126 | .col-lg-9, 127 | .col-lg-auto, 128 | .col-md, 129 | .col-md-1, 130 | .col-md-10, 131 | .col-md-11, 132 | .col-md-12, 133 | .col-md-2, 134 | .col-md-3, 135 | .col-md-4, 136 | .col-md-5, 137 | .col-md-6, 138 | .col-md-7, 139 | .col-md-8, 140 | .col-md-9, 141 | .col-md-auto, 142 | .col-sm, 143 | .col-sm-1, 144 | .col-sm-10, 145 | .col-sm-11, 146 | .col-sm-12, 147 | .col-sm-2, 148 | .col-sm-3, 149 | .col-sm-4, 150 | .col-sm-5, 151 | .col-sm-6, 152 | .col-sm-7, 153 | .col-sm-8, 154 | .col-sm-9, 155 | .col-sm-auto, 156 | .col-xl, 157 | .col-xl-1, 158 | .col-xl-10, 159 | .col-xl-11, 160 | .col-xl-12, 161 | .col-xl-2, 162 | .col-xl-3, 163 | .col-xl-4, 164 | .col-xl-5, 165 | .col-xl-6, 166 | .col-xl-7, 167 | .col-xl-8, 168 | .col-xl-9, 169 | .col-xl-auto { 170 | padding-left: 15px; 171 | padding-right: 15px; 172 | position: relative; 173 | width: 100%; 174 | } 175 | 176 | $col-widths: ( 177 | "1": 8.333333%, 178 | "2": 16.666667%, 179 | "3": 25%, 180 | "4": 33.333333%, 181 | "5": 41.666667%, 182 | "6": 50%, 183 | "7": 58.333333%, 184 | "8": 66.666667%, 185 | "9": 75%, 186 | "10": 83.333333%, 187 | "11": 91.666667%, 188 | "12": 100%, 189 | ) !default; 190 | 191 | 192 | @each $cat, $width in $media-widths { 193 | @media (min-width:$width) { 194 | .col-#{$cat} { 195 | flex-basis: 0; 196 | flex-grow: 1; 197 | -ms-flex-positive: 1; 198 | -ms-flex-preferred-size: 0; 199 | max-width: 100%; 200 | } 201 | 202 | .col-#{$cat}-auto { 203 | -ms-flex: 0 0 auto; 204 | flex: 0 0 auto; 205 | max-width: 100%; 206 | width: auto; 207 | } 208 | 209 | @each $twelth, $percent in $col-widths { 210 | .col-#{$cat}-#{$twelth} { 211 | -ms-flex: 0 0 $percent; 212 | flex: 0 0 $percent; 213 | max-width: $percent; 214 | } 215 | } 216 | } 217 | } 218 | 219 | .d-flex { 220 | display: -ms-flexbox !important; 221 | display: flex !important; 222 | } 223 | -------------------------------------------------------------------------------- /sphinx_panels/scss/bootstrap/_overrides.scss: -------------------------------------------------------------------------------- 1 | // Overrides for non-bootstrap themes (such as alabaster) 2 | 3 | .sphinx-bs, 4 | .sphinx-bs * { 5 | -moz-box-sizing: border-box; 6 | -webkit-box-sizing: border-box; 7 | box-sizing: border-box; 8 | } 9 | 10 | .sphinx-bs p { 11 | margin-top: 0; 12 | } 13 | -------------------------------------------------------------------------------- /sphinx_panels/scss/bootstrap/index.scss: -------------------------------------------------------------------------------- 1 | // Minimal bootstrap required for non-bootstrap themes 2 | @import './colors'; 3 | @import './badge'; 4 | @import './borders'; 5 | @import './buttons'; 6 | @import './cards'; 7 | @import './grids'; 8 | @import './overrides'; 9 | -------------------------------------------------------------------------------- /sphinx_panels/scss/panels/_dropdown.scss: -------------------------------------------------------------------------------- 1 | details.dropdown { 2 | .summary-title { 3 | // don't overlap the chevron 4 | padding-right: 3em !important; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | -webkit-user-select: none; 8 | user-select: none; 9 | } 10 | 11 | &:hover { 12 | cursor: pointer; 13 | } 14 | 15 | .summary-content { 16 | cursor: default; 17 | } 18 | 19 | summary { 20 | // hide the default triangle marker 21 | list-style: none; 22 | padding: 1em; 23 | 24 | // Ellipsis added when no title 25 | .octicon.no-title { 26 | vertical-align: middle; 27 | } 28 | } 29 | 30 | &[open] summary .octicon.no-title { 31 | visibility: hidden; 32 | } 33 | 34 | // chrome doesn't yet support list-style 35 | summary::-webkit-details-marker { 36 | display: none; 37 | } 38 | 39 | summary:focus { 40 | outline: none; 41 | } 42 | 43 | summary:hover .summary-up svg, 44 | summary:hover .summary-down svg { 45 | opacity: 1; 46 | } 47 | 48 | .summary-up svg, 49 | .summary-down svg { 50 | display: block; 51 | opacity: .6; 52 | } 53 | 54 | .summary-up, 55 | .summary-down { 56 | pointer-events: none; 57 | position: absolute; 58 | right: 1em; 59 | top: .75em; 60 | } 61 | 62 | &[open] .summary-down { 63 | visibility: hidden; 64 | } 65 | 66 | &:not([open]) .summary-up { 67 | visibility: hidden; 68 | } 69 | 70 | // Transition animation 71 | &.fade-in[open] summary~* { 72 | -moz-animation: panels-fade-in .5s ease-in-out; 73 | -webkit-animation: panels-fade-in .5s ease-in-out; 74 | animation: panels-fade-in .5s ease-in-out; 75 | } 76 | 77 | &.fade-in-slide-down[open] summary~* { 78 | -moz-animation: panels-fade-in .5s ease-in-out, panels-slide-down .5s ease-in-out; 79 | -webkit-animation: panels-fade-in .5s ease-in-out, panels-slide-down .5s ease-in-out; 80 | animation: panels-fade-in .5s ease-in-out, panels-slide-down .5s ease-in-out; 81 | } 82 | } 83 | 84 | @keyframes panels-fade-in { 85 | 0% { 86 | opacity: 0; 87 | } 88 | 89 | 100% { 90 | opacity: 1; 91 | } 92 | } 93 | 94 | @keyframes panels-slide-down { 95 | 0% { 96 | transform: translate(0, -10px); 97 | } 98 | 99 | 100% { 100 | transform: translate(0, 0); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /sphinx_panels/scss/panels/_icons.scss: -------------------------------------------------------------------------------- 1 | .octicon { 2 | display: inline-block; 3 | fill: currentColor; 4 | vertical-align: text-top; 5 | } 6 | -------------------------------------------------------------------------------- /sphinx_panels/scss/panels/_tabs.scss: -------------------------------------------------------------------------------- 1 | // --------------------------------------------------------------------------------- 2 | // Adapted from https://squidfunk.github.io/mkdocs-material/reference/content-tabs/ 3 | /// Copyright (c) 2016-2020 Martin Donath 4 | // --------------------------------------------------------------------------------- 5 | /// 6 | /// Convert font size in px to rem 7 | /// 8 | @function px2rem($size, $base: 16px) { 9 | @if unit($size) == px { 10 | @if unit($base) == px { 11 | @return ($size / $base) * 1rem; 12 | } @else { 13 | @error "Invalid base: #{$base} - unit must be 'px'"; 14 | } 15 | } @else { 16 | @error "Invalid size: #{$size} - unit must be 'px'"; 17 | } 18 | } 19 | 20 | /// 21 | /// Convert font size in px to em 22 | /// 23 | @function px2em($size, $base: 16px) { 24 | @if unit($size) == px { 25 | @if unit($base) == px { 26 | @return ($size / $base) * 1em; 27 | } @else { 28 | @error "Invalid base: #{$base} - unit must be 'px'"; 29 | } 30 | } @else { 31 | @error "Invalid size: #{$size} - unit must be 'px'"; 32 | } 33 | } 34 | 35 | // Tabbed block content 36 | .tabbed-content { 37 | box-shadow: 0 px2rem(-1px) var(--tabs-color-overline), 0 px2rem(1px) var(--tabs-color-underline); 38 | display: none; 39 | order: 99; 40 | padding-bottom: px2rem(12px); 41 | padding-top: px2rem(12px); 42 | width: 100%; 43 | 44 | >:first-child { 45 | margin-top: 0 !important; 46 | } 47 | 48 | >:last-child { 49 | margin-bottom: 0 !important; 50 | } 51 | 52 | // Nested tabs 53 | >.tabbed-set { 54 | margin: 0; 55 | } 56 | } 57 | 58 | // Tabbed block container 59 | .tabbed-set { 60 | border-radius: px2rem(2px); 61 | display: flex; 62 | flex-wrap: wrap; 63 | margin: 1em 0; 64 | position: relative; 65 | 66 | // Hide radio buttons 67 | >input { 68 | opacity: 0; 69 | position: absolute; 70 | 71 | // Active tab label 72 | &:checked+label { 73 | border-color: var(--tabs-color-label-active); 74 | color: var(--tabs-color-label-active); 75 | 76 | // Show tabbed block content 77 | +.tabbed-content { 78 | display: block; 79 | } 80 | } 81 | 82 | // Focused tab label 83 | &:focus+label { 84 | outline-style: auto; 85 | } 86 | 87 | // Disable focus indicator for pointer devices 88 | &:not(.focus-visible)+label { 89 | outline: none; 90 | -webkit-tap-highlight-color: transparent; 91 | } 92 | } 93 | 94 | // Tab label 95 | >label { 96 | border-bottom: px2rem(2px) solid transparent; 97 | color: var(--tabs-color-label-inactive); 98 | cursor: pointer; 99 | font-size: var(--tabs-size-label); 100 | font-weight: 700; 101 | padding: px2em(20px, 20px) 1.25em px2em(10px, 20px); 102 | transition: color 250ms; 103 | width: auto; 104 | z-index: 1; 105 | 106 | // Hovered label 107 | html &:hover { 108 | color: var(--tabs-color-label-active); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /sphinx_panels/scss/panels/index.scss: -------------------------------------------------------------------------------- 1 | // SCSS For sphinx panels 2 | @import './dropdown'; 3 | @import './icons'; 4 | @import './tabs'; 5 | -------------------------------------------------------------------------------- /sphinx_panels/tabs.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | from typing import Optional 3 | 4 | from docutils import nodes 5 | from docutils.parsers.rst import directives 6 | from sphinx.transforms.post_transforms import SphinxPostTransform 7 | from sphinx.util.docutils import SphinxDirective 8 | from sphinx.util.logging import getLogger 9 | from sphinx.util.nodes import NodeMatcher 10 | 11 | LOGGER = getLogger(__name__) 12 | 13 | 14 | def setup_tabs(app): 15 | app.add_directive("tabbed", TabbedDirective) 16 | app.add_post_transform(TabbedHtmlTransform) 17 | app.add_node(tabbed_input, html=(visit_tabbed_input, depart_tabbed_input)) 18 | app.add_node(tabbed_label, html=(visit_tabbed_label, depart_tabbed_label)) 19 | 20 | 21 | class tabbed_input(nodes.Element, nodes.General): 22 | pass 23 | 24 | 25 | class tabbed_label(nodes.TextElement, nodes.General): 26 | pass 27 | 28 | 29 | def visit_tabbed_input(self, node): 30 | attributes = {"ids": [node["id"]], "type": node["type"], "name": node["set_id"]} 31 | if node["checked"]: 32 | attributes["checked"] = "checked" 33 | self.body.append(self.starttag(node, "input", **attributes)) 34 | 35 | 36 | def depart_tabbed_input(self, node): 37 | self.body.append("") 38 | 39 | 40 | def visit_tabbed_label(self, node): 41 | attributes = {"for": node["input_id"]} 42 | self.body.append(self.starttag(node, "label", **attributes)) 43 | 44 | 45 | def depart_tabbed_label(self, node): 46 | self.body.append("") 47 | 48 | 49 | class TabbedDirective(SphinxDirective): 50 | """CSS-based tabs.""" 51 | 52 | required_arguments = 1 53 | final_argument_whitespace = True 54 | has_content = True 55 | option_spec = { 56 | "new-group": directives.flag, 57 | "selected": directives.flag, 58 | "name": directives.unchanged, 59 | "class-label": directives.class_option, 60 | "class-content": directives.class_option, 61 | } 62 | 63 | def run(self): 64 | self.assert_has_content() 65 | 66 | container = nodes.container( 67 | "", 68 | type="tabbed", 69 | new_group="new-group" in self.options, 70 | selected="selected" in self.options, 71 | classes=["tabbed-container"], 72 | ) 73 | self.set_source_info(container) 74 | 75 | # add label as a rubric (to degrade nicely for non-html outputs) 76 | textnodes, messages = self.state.inline_text(self.arguments[0], self.lineno) 77 | label = nodes.rubric(self.arguments[0], *textnodes, classes=["tabbed-label"]) 78 | label["classes"] += self.options.get("class-label", []) 79 | self.add_name(label) 80 | container += label 81 | 82 | # add content 83 | content = nodes.container("", is_div=True, classes=["tabbed-content"]) 84 | content["classes"] += self.options.get("class-content", []) 85 | self.state.nested_parse(self.content, self.content_offset, content) 86 | 87 | container += content 88 | 89 | return [container] 90 | 91 | 92 | class TabSet: 93 | def __init__(self, node): 94 | self._nodes = [node] 95 | 96 | def is_next(self, node): 97 | if self.parent != node.parent: 98 | return False 99 | if node.parent.index(node) != (self.indices[-1] + 1): 100 | return False 101 | return True 102 | 103 | def append(self, node): 104 | assert self.is_next(node) 105 | self._nodes.append(node) 106 | 107 | @property 108 | def parent(self) -> int: 109 | return self._nodes[0].parent 110 | 111 | @property 112 | def nodes(self) -> int: 113 | return self._nodes[:] 114 | 115 | @property 116 | def indices(self) -> int: 117 | return [n.parent.index(n) for n in self._nodes] 118 | 119 | 120 | class TabbedHtmlTransform(SphinxPostTransform): 121 | default_priority = 200 122 | formats = ("html",) 123 | 124 | def get_unique_key(self): 125 | return str(uuid4()) 126 | 127 | def run(self): 128 | matcher = NodeMatcher(nodes.container, type="tabbed") 129 | tab_set = None 130 | for node in self.document.traverse(matcher): # type: nodes.container 131 | if tab_set is None: 132 | tab_set = TabSet(node) 133 | elif node["new_group"]: 134 | self.render_tab_set(tab_set) 135 | tab_set = TabSet(node) 136 | elif tab_set.is_next(node): 137 | tab_set.append(node) 138 | else: 139 | self.render_tab_set(tab_set) 140 | tab_set = TabSet(node) 141 | self.render_tab_set(tab_set) 142 | 143 | def render_tab_set(self, tab_set: Optional[TabSet]): 144 | 145 | if tab_set is None: 146 | return 147 | 148 | container = nodes.container("", is_div=True, classes=["tabbed-set"]) 149 | container.parent = tab_set.parent 150 | set_identity = self.get_unique_key() 151 | 152 | # get the first selected node 153 | selected_idx = None 154 | for idx, tab in enumerate(tab_set.nodes): 155 | if tab["selected"]: 156 | if selected_idx is None: 157 | selected_idx = idx 158 | else: 159 | LOGGER.warning("multiple selected tabbed directives", location=tab) 160 | selected_idx = 0 if selected_idx is None else selected_idx 161 | 162 | for idx, tab in enumerate(tab_set.nodes): 163 | # TODO warn and continue if incorrect children 164 | title, content = tab.children 165 | # input 166 | identity = self.get_unique_key() 167 | input_node = tabbed_input( 168 | "", 169 | id=identity, 170 | set_id=set_identity, 171 | type="radio", 172 | checked=(idx == selected_idx), 173 | ) 174 | input_node.source, input_node.line = tab.source, tab.line 175 | container += input_node 176 | # label 177 | # TODO this actually has to be text only 178 | label = tabbed_label("", *title.children, input_id=identity) 179 | label["classes"] = title["classes"] 180 | container += label 181 | input_node.source, input_node.line = tab.source, tab.line 182 | # content 183 | container += content 184 | 185 | # replace all nodes 186 | tab_set.parent.children = ( 187 | tab_set.parent.children[: tab_set.indices[0]] 188 | + [container] 189 | + tab_set.parent.children[tab_set.indices[-1] + 1 :] 190 | ) 191 | -------------------------------------------------------------------------------- /sphinx_panels/utils.py: -------------------------------------------------------------------------------- 1 | from ast import literal_eval 2 | import re 3 | 4 | REGEX = re.compile( 5 | r'\s*(?:(?P[a-zA-Z0-9_]+)\s*\=)?\s*(?P".*"|[^,]+)\s*(?:,|$)' 6 | ) 7 | 8 | 9 | def eval_literal(string): 10 | try: 11 | value = literal_eval(string) 12 | except Exception: 13 | value = string 14 | return value 15 | 16 | 17 | def string_to_func_inputs(text): 18 | args = [] 19 | kwargs = {} 20 | for key, value in REGEX.findall(text): 21 | if key: 22 | kwargs[key.strip()] = eval_literal(value.strip()) 23 | else: 24 | args.append(eval_literal(value.strip())) 25 | return args, kwargs 26 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = "sphinx.testing.fixtures" 2 | -------------------------------------------------------------------------------- /tests/sources/dropdown_basic/conf.py: -------------------------------------------------------------------------------- 1 | extensions = ["sphinx_panels"] 2 | -------------------------------------------------------------------------------- /tests/sources/dropdown_basic/index.rst: -------------------------------------------------------------------------------- 1 | Title 2 | ===== 3 | 4 | .. dropdown:: My Content 5 | :container: + shadow 6 | :title: bg-primary text-white text-center font-weight-bold 7 | :body: bg-light text-right font-italic 8 | 9 | Is formatted 10 | 11 | .. dropdown:: Fade In 12 | :animate: fade-in-slide-down 13 | 14 | Content 15 | -------------------------------------------------------------------------------- /tests/sources/tabbed_basic/conf.py: -------------------------------------------------------------------------------- 1 | extensions = ["sphinx_panels"] 2 | -------------------------------------------------------------------------------- /tests/sources/tabbed_basic/index.rst: -------------------------------------------------------------------------------- 1 | Title 2 | ===== 3 | 4 | .. tabbed:: Tab 1 5 | 6 | Tab 1 content 7 | 8 | .. tabbed:: Tab 2 9 | :class-content: pl-1 bg-primary 10 | 11 | Tab 2 content 12 | 13 | .. tabbed:: Tab 3 14 | :new-group: 15 | 16 | Tab 3 content 17 | 18 | .. code-block:: python 19 | 20 | import pip 21 | 22 | .. tabbed:: Tab 4 23 | :selected: 24 | 25 | Tab 4 content 26 | -------------------------------------------------------------------------------- /tests/test_icons.py: -------------------------------------------------------------------------------- 1 | from sphinx_panels import icons 2 | 3 | 4 | def test_opticon_simple(): 5 | string = icons.get_opticon("report") 6 | assert string == ( 7 | '' 16 | ) 17 | 18 | 19 | def test_opticon_with_options(): 20 | string = icons.get_opticon( 21 | "kebab-horizontal", width=36.0, size=24, classes="custom", aria_label="other" 22 | ) 23 | assert string == ( 24 | '' 27 | '' 29 | ) 30 | -------------------------------------------------------------------------------- /tests/test_panels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from sphinx_panels.panels import parse_panels 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "content,expected", 8 | ( 9 | ("a", [{"body": (["a"], 0)}]), 10 | ("---\na", [{"body": (["a"], 1)}]), 11 | ("a\n^^^", [{"body": ([], 2), "header": (["a"], 0)}]), 12 | ("a\n+++", [{"body": (["a"], 0), "footer": ([], 1)}]), 13 | ( 14 | "a\n^^^\nb\n+++\nc", 15 | [{"body": (["b"], 2), "footer": (["c"], 3), "header": (["a"], 0)}], 16 | ), 17 | ("---\n:card: a", [{"body": ([], 2), "classes": {"card": ["a"]}}]), 18 | ("a\n---\nb", [{"body": (["a"], 0)}, {"body": (["b"], 2)}]), 19 | ), 20 | ) 21 | def test_parse_panels(content, expected): 22 | output = parse_panels(content, content_offset=0, default_classes={}) 23 | assert output == expected 24 | -------------------------------------------------------------------------------- /tests/test_sphinx.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import shutil 3 | 4 | import pytest 5 | from sphinx.testing.path import path 6 | 7 | from sphinx_panels.tabs import TabbedHtmlTransform 8 | 9 | 10 | @pytest.fixture() 11 | def sphinx_app_factory(make_app, tmp_path: Path, monkeypatch): 12 | monkeypatch.setattr(TabbedHtmlTransform, "get_unique_key", lambda self: "mock-uuid") 13 | 14 | def _func(src_folder, **kwargs): 15 | shutil.copytree( 16 | (Path(__file__).parent / "sources" / src_folder), tmp_path / src_folder 17 | ) 18 | app = make_app(srcdir=path(str((tmp_path / src_folder).absolute())), **kwargs) 19 | return app 20 | 21 | yield _func 22 | 23 | 24 | @pytest.mark.parametrize("folder", ["tabbed_basic", "dropdown_basic"]) 25 | def test_sources(sphinx_app_factory, file_regression, folder): 26 | app = sphinx_app_factory(folder) 27 | app.build() 28 | assert app._warning.getvalue() == "" 29 | doctree = app.env.get_and_resolve_doctree("index", app.builder) 30 | doctree["source"] = "source" 31 | file_regression.check( 32 | doctree.pformat(), 33 | encoding="utf8", 34 | extension=".xml", 35 | ) 36 | -------------------------------------------------------------------------------- /tests/test_sphinx/test_sources_dropdown_basic_.xml: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | Title 5 | <dropdown_main classes="sphinx-bs dropdown card mb-3 shadow" opened="False"> 6 | <dropdown_title classes="summary-title card-header bg-primary text-white text-center font-weight-bold"> 7 | My Content 8 | <container classes="summary-down" is_div="True"> 9 | <raw format="html" xml:space="preserve"> 10 | <svg version="1.1" width="24" height="24" class="octicon octicon-chevron-down" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M5.22 8.72a.75.75 0 000 1.06l6.25 6.25a.75.75 0 001.06 0l6.25-6.25a.75.75 0 00-1.06-1.06L12 14.44 6.28 8.72a.75.75 0 00-1.06 0z"></path></svg> 11 | <container classes="summary-up" is_div="True"> 12 | <raw format="html" xml:space="preserve"> 13 | <svg version="1.1" width="24" height="24" class="octicon octicon-chevron-up" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M18.78 15.28a.75.75 0 000-1.06l-6.25-6.25a.75.75 0 00-1.06 0l-6.25 6.25a.75.75 0 101.06 1.06L12 9.56l5.72 5.72a.75.75 0 001.06 0z"></path></svg> 14 | <container classes="summary-content card-body bg-light text-right font-italic" is_div="True"> 15 | <paragraph classes="card-text"> 16 | Is formatted 17 | <dropdown_main classes="sphinx-bs dropdown card mb-3 fade-in-slide-down" opened="False"> 18 | <dropdown_title classes="summary-title card-header"> 19 | Fade In 20 | <container classes="summary-down" is_div="True"> 21 | <raw format="html" xml:space="preserve"> 22 | <svg version="1.1" width="24" height="24" class="octicon octicon-chevron-down" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M5.22 8.72a.75.75 0 000 1.06l6.25 6.25a.75.75 0 001.06 0l6.25-6.25a.75.75 0 00-1.06-1.06L12 14.44 6.28 8.72a.75.75 0 00-1.06 0z"></path></svg> 23 | <container classes="summary-up" is_div="True"> 24 | <raw format="html" xml:space="preserve"> 25 | <svg version="1.1" width="24" height="24" class="octicon octicon-chevron-up" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M18.78 15.28a.75.75 0 000-1.06l-6.25-6.25a.75.75 0 00-1.06 0l-6.25 6.25a.75.75 0 101.06 1.06L12 9.56l5.72 5.72a.75.75 0 001.06 0z"></path></svg> 26 | <container classes="summary-content card-body" is_div="True"> 27 | <paragraph classes="card-text"> 28 | Content 29 | -------------------------------------------------------------------------------- /tests/test_sphinx/test_sources_tabbed_basic_.xml: -------------------------------------------------------------------------------- 1 | <document source="source"> 2 | <section ids="title" names="title"> 3 | <title> 4 | Title 5 | <container classes="tabbed-set" is_div="True"> 6 | <tabbed_input checked="True" id="mock-uuid" set_id="mock-uuid" type="radio"> 7 | <tabbed_label classes="tabbed-label" input_id="mock-uuid"> 8 | Tab 1 9 | <container classes="tabbed-content" is_div="True"> 10 | <paragraph> 11 | Tab 1 content 12 | <tabbed_input checked="False" id="mock-uuid" set_id="mock-uuid" type="radio"> 13 | <tabbed_label classes="tabbed-label" input_id="mock-uuid"> 14 | Tab 2 15 | <container classes="tabbed-content pl-1 bg-primary" is_div="True"> 16 | <paragraph> 17 | Tab 2 content 18 | <container classes="tabbed-set" is_div="True"> 19 | <tabbed_input checked="False" id="mock-uuid" set_id="mock-uuid" type="radio"> 20 | <tabbed_label classes="tabbed-label" input_id="mock-uuid"> 21 | Tab 3 22 | <container classes="tabbed-content" is_div="True"> 23 | <paragraph> 24 | Tab 3 content 25 | <literal_block force="False" highlight_args="{}" language="python" linenos="False" xml:space="preserve"> 26 | import pip 27 | <tabbed_input checked="True" id="mock-uuid" set_id="mock-uuid" type="radio"> 28 | <tabbed_label classes="tabbed-label" input_id="mock-uuid"> 29 | Tab 4 30 | <container classes="tabbed-content" is_div="True"> 31 | <paragraph> 32 | Tab 4 content 33 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from sphinx_panels import utils 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "string,expected", 8 | [ 9 | ("", ([], {})), 10 | ("a", (["a"], {})), 11 | ("a,b", (["a", "b"], {})), 12 | ("a,1", (["a", 1], {})), 13 | ("1,a", ([1, "a"], {})), 14 | ("a,b=1", (["a"], {"b": 1})), 15 | ('a,b="1"', (["a"], {"b": "1"})), 16 | ('a , b = "1,2" ', (["a"], {"b": "1,2"})), 17 | ('a , b = "1,2", sdf=4 ', (["a"], {"b": "1,2", "sdf": 4})), 18 | ('a,b="""', (["a"], {"b": '"""'})), # This is kind of wrong 19 | ], 20 | ) 21 | def test_string_to_func_inputs(string, expected): 22 | assert utils.string_to_func_inputs(string) == expected 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # To use tox, see https://tox.readthedocs.io 2 | # Simply pip or conda install tox 3 | # If you use conda, you may also want to install tox-conda 4 | # then run `tox` or `tox -- {pytest args}` 5 | # To run in parallel using `tox -p` (this does not appear to work for this repo) 6 | 7 | # To rebuild the tox environment, for example when dependencies change, use 8 | # `tox -r` 9 | 10 | # Note: if the following error is encountered: `ImportError while loading conftest` 11 | # then then deleting compiled files has been found to fix it: `find . -name \*.pyc -delete` 12 | 13 | [tox] 14 | envlist = py38-sphinx4 15 | 16 | [testenv] 17 | usedevelop = true 18 | 19 | [testenv:py{36,37,38,39}-sphinx{2,3,4}] 20 | extras = testing 21 | deps = 22 | sphinx2: sphinx>=2,<3 23 | sphinx3: sphinx>=3,<4 24 | sphinx4: sphinx>=4,<5 25 | commands = pytest {posargs} 26 | 27 | [testenv:docs-{update,clean}] 28 | extras = themes 29 | passenv = HTML_THEME 30 | whitelist_externals = rm 31 | commands = 32 | clean: rm -rf docs/_build 33 | sphinx-build -nW --keep-going -b {posargs:html} docs/ docs/_build/{posargs:html} 34 | 35 | [testenv:docs-live] 36 | extras = 37 | themes 38 | live-dev 39 | passenv = HTML_THEME 40 | usedevelop = true 41 | commands = 42 | sphinx-autobuild \ 43 | --watch sphinx_panels \ 44 | --pre-build "web-compile --no-git-add" \ 45 | --re-ignore sphinx_panels/_css/.* \ 46 | --re-ignore _build/.* \ 47 | --port 0 --open-browser \ 48 | -n -b {posargs:html} docs/ docs/_build/{posargs:html} 49 | -------------------------------------------------------------------------------- /web-compile-config.yml: -------------------------------------------------------------------------------- 1 | web-compile: 2 | sass: 3 | files: 4 | sphinx_panels/scss/bootstrap/index.scss: sphinx_panels/_css/panels-bootstrap.[hash].css 5 | sphinx_panels/scss/panels/index.scss: sphinx_panels/_css/panels-main.[hash].css 6 | precision: 5 7 | sourcemap: false 8 | format: compressed 9 | --------------------------------------------------------------------------------