├── .bumpversion.cfg ├── .cookiecutterrc ├── .coveragerc ├── .editorconfig ├── .github └── workflows │ ├── examples.yml │ └── github-actions.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── ci ├── bootstrap.py ├── requirements.txt └── templates │ └── .github │ └── workflows │ └── github-actions.yml ├── docs ├── authors.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── index.rst ├── installation.rst ├── readme.rst ├── reference │ ├── darkslide.rst │ └── index.rst ├── requirements.txt ├── spelling_wordlist.txt └── usage.rst ├── examples ├── _assets │ ├── SourceSansPro.woff2 │ └── landscape.jpg ├── config-file │ └── presentation.cfg ├── markdown │ └── slides.md ├── multiple-files │ ├── 1.md │ ├── 2.rst │ └── 3.textile ├── other-features │ └── slides.md ├── restructuredtext │ └── slides.rst ├── textile │ └── slides.textile └── user_css │ └── style.css ├── setup.cfg ├── setup.py ├── src └── darkslide │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── generator.py │ ├── macro.py │ ├── mods │ ├── showpagenums │ │ ├── README.md │ │ └── css │ │ │ └── mod.css │ └── wide16x9 │ │ └── css │ │ └── mod.css │ ├── parser.py │ ├── rst.py │ ├── themes │ ├── abyss │ │ └── css │ │ │ └── theme.css │ ├── default │ │ ├── css │ │ │ ├── base.css │ │ │ ├── print.css │ │ │ ├── screen.css │ │ │ └── theme.css │ │ ├── js │ │ │ └── slides.js │ │ └── template.html │ ├── void │ │ └── css │ │ │ └── theme.css │ └── white │ │ └── css │ │ └── theme.css │ ├── utils.py │ └── watcher.py ├── tests ├── test-data │ ├── căcăneață.rst │ ├── encoding.rst │ ├── full.cfg │ ├── img.png │ ├── test.css │ ├── test.js │ └── test.md └── test_landslide.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 6.0.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file (badge):README.rst] 11 | search = /v{current_version}.svg 12 | replace = /v{new_version}.svg 13 | 14 | [bumpversion:file (link):README.rst] 15 | search = /v{current_version}...master 16 | replace = /v{new_version}...master 17 | 18 | [bumpversion:file:docs/conf.py] 19 | search = version = release = '{current_version}' 20 | replace = version = release = '{new_version}' 21 | 22 | [bumpversion:file:src/darkslide/__init__.py] 23 | search = __version__ = '{current_version}' 24 | replace = __version__ = '{new_version}' 25 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | # Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) 2 | 3 | default_context: 4 | allow_tests_inside_package: no 5 | appveyor: no 6 | c_extension_function: '-' 7 | c_extension_module: '-' 8 | c_extension_optional: no 9 | c_extension_support: no 10 | c_extension_test_pypi: no 11 | c_extension_test_pypi_username: '-' 12 | codacy: no 13 | codacy_projectid: 862e7946eabb4112be6503a667381b71 14 | codeclimate: no 15 | codecov: yes 16 | command_line_interface: plain 17 | command_line_interface_bin_name: darkslide 18 | coveralls: yes 19 | distribution_name: darkslide 20 | email: contact@ionelmc.ro 21 | full_name: Ionel Cristian Mărieș 22 | github_actions: yes 23 | legacy_python: no 24 | license: Apache Software License 2.0 25 | linter: flake8 26 | package_name: darkslide 27 | pre_commit: yes 28 | project_name: Darkslide 29 | project_short_description: Lightweight markup language-based html5 slideshow generator. Forked from Landslide. 30 | pypi_badge: yes 31 | pypi_disable_upload: no 32 | release_date: '2020-07-24' 33 | repo_hosting: github.com 34 | repo_hosting_domain: github.com 35 | repo_main_branch: master 36 | repo_name: python-darkslide 37 | repo_username: ionelmc 38 | requiresio: yes 39 | scrutinizer: no 40 | setup_py_uses_setuptools_scm: no 41 | setup_py_uses_test_runner: no 42 | sphinx_docs: yes 43 | sphinx_docs_hosting: https://darkslide.readthedocs.io/ 44 | sphinx_doctest: no 45 | sphinx_theme: sphinx-py3doc-enhanced-theme 46 | test_matrix_configurator: no 47 | test_matrix_separate_coverage: no 48 | test_runner: pytest 49 | travis: no 50 | travis_osx: no 51 | version: 6.0.0 52 | version_manager: bump2version 53 | website: http://blog.ionelmc.ro 54 | year_from: '2015' 55 | year_to: '2021' 56 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = 3 | src 4 | */site-packages 5 | 6 | [run] 7 | branch = true 8 | source = 9 | darkslide 10 | tests 11 | parallel = true 12 | 13 | [report] 14 | show_missing = true 15 | precision = 2 16 | omit = *migrations* 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | # Use Unix-style newlines for most files (except Windows files, see below). 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | insert_final_newline = true 10 | indent_size = 4 11 | charset = utf-8 12 | 13 | [*.{bat,cmd,ps1}] 14 | end_of_line = crlf 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | 19 | [*.tsv] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: examples 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | examples: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 30 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-python@v2 15 | with: 16 | python-version: '3.9' 17 | - name: install dependencies 18 | run: | 19 | python -mpip install --progress-bar=off -r ci/requirements.txt 20 | virtualenv --version 21 | pip --version 22 | tox --version 23 | pip list --format=freeze 24 | - name: build 25 | run: > 26 | tox -e examples -v 27 | - name: publish 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./dist/examples 32 | -------------------------------------------------------------------------------- /.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: ${{ matrix.name }} 6 | runs-on: ${{ matrix.os }} 7 | timeout-minutes: 30 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - name: 'check' 13 | python: '3.9' 14 | toxpython: 'python3.9' 15 | tox_env: 'check' 16 | os: 'ubuntu-latest' 17 | - name: 'docs' 18 | python: '3.9' 19 | toxpython: 'python3.9' 20 | tox_env: 'docs' 21 | os: 'ubuntu-latest' 22 | - name: 'py36 (ubuntu)' 23 | python: '3.6' 24 | toxpython: 'python3.6' 25 | python_arch: 'x64' 26 | tox_env: 'py36' 27 | os: 'ubuntu-latest' 28 | - name: 'py36 (windows)' 29 | python: '3.6' 30 | toxpython: 'python3.6' 31 | python_arch: 'x64' 32 | tox_env: 'py36' 33 | os: 'windows-latest' 34 | - name: 'py36 (windows)' 35 | python: '3.6' 36 | toxpython: 'python3.6' 37 | python_arch: 'x86' 38 | tox_env: 'py36' 39 | os: 'windows-latest' 40 | - name: 'py36 (macos)' 41 | python: '3.6' 42 | toxpython: 'python3.6' 43 | python_arch: 'x64' 44 | tox_env: 'py36' 45 | os: 'macos-latest' 46 | - name: 'py37 (ubuntu)' 47 | python: '3.7' 48 | toxpython: 'python3.7' 49 | python_arch: 'x64' 50 | tox_env: 'py37' 51 | os: 'ubuntu-latest' 52 | - name: 'py37 (windows)' 53 | python: '3.7' 54 | toxpython: 'python3.7' 55 | python_arch: 'x64' 56 | tox_env: 'py37' 57 | os: 'windows-latest' 58 | - name: 'py37 (windows)' 59 | python: '3.7' 60 | toxpython: 'python3.7' 61 | python_arch: 'x86' 62 | tox_env: 'py37' 63 | os: 'windows-latest' 64 | - name: 'py37 (macos)' 65 | python: '3.7' 66 | toxpython: 'python3.7' 67 | python_arch: 'x64' 68 | tox_env: 'py37' 69 | os: 'macos-latest' 70 | - name: 'py38 (ubuntu)' 71 | python: '3.8' 72 | toxpython: 'python3.8' 73 | python_arch: 'x64' 74 | tox_env: 'py38' 75 | os: 'ubuntu-latest' 76 | - name: 'py38 (windows)' 77 | python: '3.8' 78 | toxpython: 'python3.8' 79 | python_arch: 'x64' 80 | tox_env: 'py38' 81 | os: 'windows-latest' 82 | - name: 'py38 (windows)' 83 | python: '3.8' 84 | toxpython: 'python3.8' 85 | python_arch: 'x86' 86 | tox_env: 'py38' 87 | os: 'windows-latest' 88 | - name: 'py38 (macos)' 89 | python: '3.8' 90 | toxpython: 'python3.8' 91 | python_arch: 'x64' 92 | tox_env: 'py38' 93 | os: 'macos-latest' 94 | - name: 'py39 (ubuntu)' 95 | python: '3.9' 96 | toxpython: 'python3.9' 97 | python_arch: 'x64' 98 | tox_env: 'py39' 99 | os: 'ubuntu-latest' 100 | - name: 'py39 (windows)' 101 | python: '3.9' 102 | toxpython: 'python3.9' 103 | python_arch: 'x64' 104 | tox_env: 'py39' 105 | os: 'windows-latest' 106 | - name: 'py39 (windows)' 107 | python: '3.9' 108 | toxpython: 'python3.9' 109 | python_arch: 'x86' 110 | tox_env: 'py39' 111 | os: 'windows-latest' 112 | - name: 'py39 (macos)' 113 | python: '3.9' 114 | toxpython: 'python3.9' 115 | python_arch: 'x64' 116 | tox_env: 'py39' 117 | os: 'macos-latest' 118 | - name: 'py310 (ubuntu)' 119 | python: '3.10' 120 | toxpython: 'python3.10' 121 | python_arch: 'x64' 122 | tox_env: 'py310' 123 | os: 'ubuntu-latest' 124 | - name: 'py310 (windows)' 125 | python: '3.10' 126 | toxpython: 'python3.10' 127 | python_arch: 'x64' 128 | tox_env: 'py310' 129 | os: 'windows-latest' 130 | - name: 'py310 (windows)' 131 | python: '3.10' 132 | toxpython: 'python3.10' 133 | python_arch: 'x86' 134 | tox_env: 'py310' 135 | os: 'windows-latest' 136 | - name: 'py310 (macos)' 137 | python: '3.10' 138 | toxpython: 'python3.10' 139 | python_arch: 'x64' 140 | tox_env: 'py310' 141 | os: 'macos-latest' 142 | - name: 'pypy37 (ubuntu)' 143 | python: 'pypy-3.7' 144 | toxpython: 'pypy3.7' 145 | python_arch: 'x64' 146 | tox_env: 'pypy37' 147 | os: 'ubuntu-latest' 148 | - name: 'pypy37 (windows)' 149 | python: 'pypy-3.7' 150 | toxpython: 'pypy3.7' 151 | python_arch: 'x64' 152 | tox_env: 'pypy37' 153 | os: 'windows-latest' 154 | - name: 'pypy37 (windows)' 155 | python: 'pypy-3.7' 156 | toxpython: 'pypy3.7' 157 | python_arch: 'x86' 158 | tox_env: 'pypy37' 159 | os: 'windows-latest' 160 | - name: 'pypy37 (macos)' 161 | python: 'pypy-3.7' 162 | toxpython: 'pypy3.7' 163 | python_arch: 'x64' 164 | tox_env: 'pypy37' 165 | os: 'macos-latest' 166 | - name: 'pypy38 (ubuntu)' 167 | python: 'pypy-3.8' 168 | toxpython: 'pypy3.8' 169 | python_arch: 'x64' 170 | tox_env: 'pypy38' 171 | os: 'ubuntu-latest' 172 | - name: 'pypy38 (windows)' 173 | python: 'pypy-3.8' 174 | toxpython: 'pypy3.8' 175 | python_arch: 'x64' 176 | tox_env: 'pypy38' 177 | os: 'windows-latest' 178 | - name: 'pypy38 (windows)' 179 | python: 'pypy-3.8' 180 | toxpython: 'pypy3.8' 181 | python_arch: 'x86' 182 | tox_env: 'pypy38' 183 | os: 'windows-latest' 184 | - name: 'pypy38 (macos)' 185 | python: 'pypy-3.8' 186 | toxpython: 'pypy3.8' 187 | python_arch: 'x64' 188 | tox_env: 'pypy38' 189 | os: 'macos-latest' 190 | steps: 191 | - uses: actions/checkout@v2 192 | with: 193 | fetch-depth: 0 194 | - uses: actions/setup-python@v2 195 | with: 196 | python-version: ${{ matrix.python }} 197 | architecture: ${{ matrix.python_arch }} 198 | - name: install dependencies 199 | run: | 200 | python -mpip install --progress-bar=off -r ci/requirements.txt 201 | virtualenv --version 202 | pip --version 203 | tox --version 204 | pip list --format=freeze 205 | - name: test 206 | env: 207 | TOXPYTHON: '${{ matrix.toxpython }}' 208 | run: > 209 | tox -e ${{ matrix.tox_env }} -v 210 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | .eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | wheelhouse 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | venv*/ 24 | pyvenv*/ 25 | pip-wheel-metadata/ 26 | 27 | # Installer logs 28 | pip-log.txt 29 | 30 | # Unit test / coverage reports 31 | .coverage 32 | .tox 33 | .coverage.* 34 | .pytest_cache/ 35 | nosetests.xml 36 | coverage.xml 37 | htmlcov 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Buildout 43 | .mr.developer.cfg 44 | 45 | # IDE project files 46 | .project 47 | .pydevproject 48 | .idea 49 | .vscode 50 | *.iml 51 | *.komodoproject 52 | 53 | # Complexity 54 | output/*.html 55 | output/*/index.html 56 | 57 | # Sphinx 58 | docs/_build 59 | 60 | .DS_Store 61 | *~ 62 | .*.sw[po] 63 | .build 64 | .ve 65 | .env 66 | .cache 67 | .pytest 68 | .benchmarks 69 | .bootstrap 70 | .appveyor.token 71 | *.bak 72 | 73 | # Mypy Cache 74 | .mypy_cache/ 75 | 76 | presentation.html 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # To install the git pre-commit hook run: 2 | # pre-commit install 3 | # To update the pre-commit hooks run: 4 | # pre-commit install-hooks 5 | exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.0.1 9 | hooks: 10 | - id: trailing-whitespace 11 | - id: end-of-file-fixer 12 | - id: debug-statements 13 | - repo: https://github.com/timothycrosley/isort 14 | rev: 5.10.1 15 | hooks: 16 | - id: isort 17 | - repo: https://gitlab.com/pycqa/flake8 18 | rev: 3.9.2 19 | hooks: 20 | - id: flake8 21 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | version: 2 3 | sphinx: 4 | configuration: docs/conf.py 5 | formats: all 6 | python: 7 | install: 8 | - requirements: docs/requirements.txt 9 | - method: pip 10 | path: . 11 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | Maintainer 5 | ---------- 6 | 7 | Ionel Cristian Mărieș 8 | 9 | History 10 | ------- 11 | 12 | The project was originally named Landslide and was authored by Adam Zapletal (adamzap@gmail.com) and Nicolas Perriault (nperriault@gmail.com) 13 | 14 | Slide code is based on html5-slides. 15 | 16 | Contributors 17 | ------------ 18 | 19 | Brandon Black 20 | 21 | More details: https://github.com/ionelmc/python-darkslide/contributors 22 | 23 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | * Added ``theme_mods`` support to config file 6 | 7 | Darkslide v6.0.0 (2020-07-24) 8 | ============================= 9 | 10 | * Dropped the ``--copy-theme`` option (**backwards incompatible**). 11 | * Added support for theme mods (the ``--mod`` option) and reworked the asset management internals. 12 | * Added a wide 16:9 theme mod. 13 | Contributed by Eric Moyer in `#19 `_. 14 | 15 | Darkslide v5.1.0 (2020-01-13) 16 | ============================= 17 | 18 | * Added support for embedding webfonts. 19 | Contributed by Emmanuel Ohayon in `#17 `_. 20 | * Refactored user css and js path processing code. 21 | * Relativized user css and js paths to the configuration directory (CWD if there's no configuration file). 22 | 23 | Darkslide v5.0.2 (2019-12-07) 24 | ============================= 25 | 26 | * Fixed a small bug in the image embedding feature (image detection in CSS file 27 | could fail). 28 | 29 | Darkslide v5.0.1 (2019-10-01) 30 | ============================= 31 | 32 | * Fixed media for user css to be always be ``all``. Previously it was ``screen, projection`` if embedded. 33 | 34 | Darkslide v5.0.0 (2019-09-29) 35 | ============================= 36 | 37 | * Removed PDF export support. You should just use the PDF export from 38 | Google Chrome (it works way better than the alternatives). 39 | * Fixed transitions in presenter mode. 40 | * Added support for Up/Down arrow navigation. 41 | Contributed by Heiko Schlittermann in `#13 `_. 42 | * Added support for Markdown 3.0+ and Textile 2.3+. 43 | * Changed the broken ``.notes:`` macro to output presenter notes. 44 | 45 | Darkslide v4.0.1 (2017-10-19) 46 | ============================= 47 | 48 | * Fixed print css a bit. 49 | * Fixed missing scrolling to current when changing slides while in overview mode. 50 | 51 | Darkslide v4.0.0 (2017-10-17) 52 | ============================= 53 | 54 | * Dropped MathJax support. Something less to maintain (also, didn't work as expected with ``--embed``). User that need this 55 | should just use the ``user_js`` option. Or a custom theme. 56 | * Changed themes to use a space-adjusted Alegreya Sans as a fallback. 57 | 58 | Darkslide v3.2.0 (2017-10-17) 59 | ============================= 60 | 61 | * Changed themes to use Rosario as a fallback. For better or worse it's smaller and has same width as Candara. 62 | 63 | 64 | Darkslide v3.1.0 (2017-10-17) 65 | ============================= 66 | 67 | * Changed themes to embed a Candara fallback webfont (Alegreya Sans). It's slightly narrower but looks more similar than the other 68 | alternatives better matching Candara's width (Acme, Galdeano). It even has ligatures. 69 | 70 | Darkslide v3.0.1 (2017-10-15) 71 | ============================= 72 | 73 | * Fixed slightly broken slide class changing. 74 | * Made expose mode scroll to current slide. 75 | * Running presenter mode with no target won't break 76 | anymore if target window is gone. 77 | * Fixed display of presenter notes. 78 | 79 | Darkslide v3.0.0 (2017-10-05) 80 | ============================= 81 | 82 | * Removed "expanded mode". It was too buggy and doesn't really have a purpose. 83 | * Changed "show context" to be "show next slide" (so two slides at a time). This is way more useful than showing little 84 | bits of next and prev slides. 85 | * Fixed ``--direct`` on Python 3. 86 | * Fixed glitches when TOC/Help are open. 87 | * Made possible to switch slides when TOC/Help/Overview are open. 88 | 89 | Darkslide v2.3.3 (2016-05-15) 90 | ============================= 91 | 92 | * Fixed height of QR svg elements. 93 | 94 | Darkslide v2.3.2 (2016-04-12) 95 | ============================= 96 | 97 | * Fixed underline occlusion shadows in the footer (for links). 98 | * Fixed missing `presenter_notes` class not being set when notes mode was on. 99 | 100 | Darkslide v2.3.1 (2016-02-08) 101 | ============================= 102 | 103 | * MathJax is loaded on HTTPS. 104 | 105 | Darkslide v2.3.0 (2016-02-07) 106 | ============================= 107 | 108 | * The Darkslide version is shown in the help sidebar. 109 | 110 | Darkslide v2.2.1 (2015-10-06) 111 | ============================= 112 | 113 | * Fixed config file parsing for math_output. 114 | 115 | Darkslide v2.2.0 (2015-10-06) 116 | ============================= 117 | 118 | * Now macro failures abort rendering. Previously they would just log a message that you'd probably woulnd't notice. 119 | * Fixed broken handling where you have css/js in the cfg file. 120 | * Allowed setting the math_output option in the cfg file. 121 | * Fixed encoding issues in the QR macro. 122 | * Added back the old theme with completely black background (as "void"). 123 | * Tweak the faux underlines to look better. 124 | 125 | Darkslide v2.1.0 (2015-10-05) 126 | ============================= 127 | 128 | * Added demo links. 129 | * Fixed options handling. Options from command line now will actually work if a cfg file is used. 130 | * Corrected relative paths handling: 131 | 132 | - paths in sources are now relative to the cfg file (previously they were relative to whatever was cwd). 133 | - relative option now correctly works when destination file is not in cwd. 134 | * Fixed layout of slides with many headering (no more paddings for headings, all root elements are spread out evenly 135 | anyway). 136 | * Fixed bad styling of ToC (and probably other things in the sidebar). 137 | * Fixed ToC links (contributed by Cyrille Pontvieux). 138 | 139 | Darkslide v2.0.4 (2015-09-09) 140 | ============================= 141 | 142 | * Improved handling for filenames that have non-ascii characters in them. 143 | 144 | Darkslide v2.0.3 (2015-09-08) 145 | ============================= 146 | 147 | * Fixed handling for filenames that have non-ascii characters in them. 148 | 149 | Darkslide v2.0.2 (2015-07-20) 150 | ============================= 151 | 152 | - Added color classes in the abyss theme. 153 | - Fixed link underlines in the presenter notes. 154 | 155 | Darkslide v2.0.1 (2015-07-19) 156 | ============================= 157 | 158 | * Don't use Monaco in the ``base.css`` - it's way bigger than Consolas and the other fonts. And Consolas is nice enough. 159 | 160 | Darkslide v2.0.0 (2015-07-17) 161 | ============================= 162 | 163 | - Fix display of RST image target links. 164 | - Add cmd line option to print version. 165 | - Rewrote the default theme (solarized colors) 166 | - Overhauled the abyss theme, improved the coloring. 167 | - Removed all the other themes (they are ugly and broken anyway) (**backwards incompatible**). 168 | - Fixes for print css. 169 | - Added support for two new css files: ``base.css`` and ``theme.css``. This 170 | makes reusing styles acros themes and kinds of display (print/screen) more easy. 171 | - Expanded mode is now activated by default. 172 | - Changed macros to use compiled regexes. 173 | - Added a footnote macro. 174 | - Changed QR macro to use ``qrcode`` library. Now it's rendered to SVG. The size is removed (**backwards incompatible**). 175 | 176 | Darkslide v1.2.2 (2015-05-22) 177 | ============================= 178 | 179 | - Fix the blank page issue when generating pdfs (via Chrome's pdf printer). 180 | 181 | Darkslide v1.2.1 (2015-05-21) 182 | ============================= 183 | 184 | - Couple minor improvements to Abyss theme. 185 | 186 | Darkslide v1.2.0 (2015-05-19) 187 | ============================= 188 | 189 | - Modifier keys flag was not cleared propertly (kb shortcuts were not working anymore after 190 | alt-tab etc); now it's cleared on visibility changes and focus loss. 191 | - Changed expanded mode to automatically hide the context. 192 | - Fixed window resize flickering (for every resize event the expaded flag was toggled). 193 | - Disabled context hiding in presenter view. 194 | - Other small styling improvements. 195 | - Added "abyss" theme. 196 | 197 | Landslide v1.1.3 198 | ================ 199 | 200 | - Identify each slide by a numbered class (#171) (dkg) 201 | - Fix theme image embedding regex to grab all images (#170) 202 | - Fix blockquote font size for rst (#161) 203 | - Fix display of RST image target links (#87) 204 | - Fix relative path generation (#147) 205 | - Add command line option for print version (#135) 206 | - Add use of '---' as a slide separator to textile files (#163) 207 | - README improvements (#88 and #101) 208 | - Improve image path regex and replacement (#177) 209 | 210 | Landslide v1.1.2 211 | ================ 212 | 213 | - Add support for Python 3 214 | - Allow support for copy\_theme argument in CFG files (#139) (syscomet) 215 | - Improve MathJax rendering for Markdown files 216 | - Support math output (#144) (davidedelvento) 217 | - Allow presenter notes in slides with no heading in RST files (#141) 218 | (regebro) 219 | - And more... 220 | 221 | Landslide v1.1.1 222 | ================ 223 | 224 | Fixes 225 | ----- 226 | 227 | - Don't accidentally require watchdog (#134) 228 | 229 | Landslide v1.1.0 230 | ================ 231 | 232 | Major Enhancements 233 | ------------------ 234 | 235 | - Add CHANGELOG 236 | - Add "ribbon" theme from "shower" presentation tool (#129) (durden) 237 | - Add ``-w`` flag for watching/auto-regenerating slideshow (#71, #120) 238 | (jondkoon) 239 | 240 | Minor Enhancements 241 | ------------------ 242 | 243 | - Supress ReST rendering errors 244 | - CSS pre enhancements (#91) (roktas) 245 | - Add an example using presenter notes (#106) (netantho) 246 | - Run macros on headers also, to embed images (#74) (godfat) 247 | - Allow PHP code snippets to not require `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | Darkslide could always use more documentation, whether as part of the 21 | official Darkslide docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests and feedback 25 | ============================= 26 | 27 | The best way to send feedback is to file an issue at https://github.com/ionelmc/python-darkslide/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that code contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `python-darkslide` for local development: 39 | 40 | 1. Fork `python-darkslide `_ 41 | (look for the "Fork" button). 42 | 2. Clone your fork locally:: 43 | 44 | git clone git@github.com:YOURGITHUBNAME/python-darkslide.git 45 | 46 | 3. Create a branch for local development:: 47 | 48 | git checkout -b name-of-your-bugfix-or-feature 49 | 50 | Now you can make your changes locally. 51 | 52 | 4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: 53 | 54 | tox 55 | 56 | 5. Commit your changes and push your branch to GitHub:: 57 | 58 | git add . 59 | git commit -m "Your detailed description of your changes." 60 | git push origin name-of-your-bugfix-or-feature 61 | 62 | 6. Submit a pull request through the GitHub website. 63 | 64 | Pull Request Guidelines 65 | ----------------------- 66 | 67 | If you need some code review or feedback while you're developing the code just make the pull request. 68 | 69 | For merging, you should: 70 | 71 | 1. Include passing tests (run ``tox``). 72 | 2. Update documentation when there's new API, functionality etc. 73 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 74 | 4. Add yourself to ``AUTHORS.rst``. 75 | 76 | 77 | 78 | Tips 79 | ---- 80 | 81 | To run a subset of tests:: 82 | 83 | tox -e envname -- pytest -k test_myfeature 84 | 85 | To run all the test environments in *parallel*:: 86 | 87 | tox -p auto 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft examples 3 | graft src 4 | graft ci 5 | graft tests 6 | 7 | include .bumpversion.cfg 8 | include .cookiecutterrc 9 | include .coveragerc 10 | include .editorconfig 11 | include tox.ini 12 | include .readthedocs.yml 13 | include .pre-commit-config.yaml 14 | include AUTHORS.rst 15 | include CHANGELOG.rst 16 | include CONTRIBUTING.rst 17 | include LICENSE 18 | include README.rst 19 | 20 | 21 | global-exclude *.py[cod] __pycache__/* *.so *.dylib 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | .. start-badges 6 | 7 | .. list-table:: 8 | :stub-columns: 1 9 | 10 | * - docs 11 | - |docs| 12 | * - tests 13 | - | |github-actions| |requires| 14 | | |coveralls| |codecov| 15 | * - package 16 | - | |version| |wheel| |supported-versions| |supported-implementations| 17 | | |commits-since| 18 | .. |docs| image:: https://readthedocs.org/projects/python-darkslide/badge/?style=flat 19 | :target: https://python-darkslide.readthedocs.io/ 20 | :alt: Documentation Status 21 | 22 | .. |github-actions| image:: https://github.com/ionelmc/python-darkslide/actions/workflows/github-actions.yml/badge.svg 23 | :alt: GitHub Actions Build Status 24 | :target: https://github.com/ionelmc/python-darkslide/actions 25 | 26 | .. |requires| image:: https://requires.io/github/ionelmc/python-darkslide/requirements.svg?branch=master 27 | :alt: Requirements Status 28 | :target: https://requires.io/github/ionelmc/python-darkslide/requirements/?branch=master 29 | 30 | .. |coveralls| image:: https://coveralls.io/repos/ionelmc/python-darkslide/badge.svg?branch=master&service=github 31 | :alt: Coverage Status 32 | :target: https://coveralls.io/r/ionelmc/python-darkslide 33 | 34 | .. |codecov| image:: https://codecov.io/gh/ionelmc/python-darkslide/branch/master/graphs/badge.svg?branch=master 35 | :alt: Coverage Status 36 | :target: https://codecov.io/github/ionelmc/python-darkslide 37 | 38 | .. |version| image:: https://img.shields.io/pypi/v/darkslide.svg 39 | :alt: PyPI Package latest release 40 | :target: https://pypi.org/project/darkslide 41 | 42 | .. |wheel| image:: https://img.shields.io/pypi/wheel/darkslide.svg 43 | :alt: PyPI Wheel 44 | :target: https://pypi.org/project/darkslide 45 | 46 | .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/darkslide.svg 47 | :alt: Supported versions 48 | :target: https://pypi.org/project/darkslide 49 | 50 | .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/darkslide.svg 51 | :alt: Supported implementations 52 | :target: https://pypi.org/project/darkslide 53 | 54 | .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-darkslide/v6.0.0.svg 55 | :alt: Commits since latest release 56 | :target: https://github.com/ionelmc/python-darkslide/compare/v6.0.0...master 57 | 58 | 59 | 60 | .. end-badges 61 | 62 | Lightweight markup language (Markdown, ReST, or Textile) slideshow generator. Forked from landslide. 63 | 64 | Demo: http://ionelmc.github.io/python-darkslide/ 65 | 66 | :: 67 | 68 | # Darkslide 69 | 70 | --- 71 | 72 | # Overview 73 | 74 | Generate HTML5 slideshows from markdown, ReST, or textile. 75 | 76 | ![python](http://i.imgur.com/bc2xk.png) 77 | 78 | Darkslide is primarily written in Python, but it's themes use: 79 | 80 | - HTML5 81 | - Javascript 82 | - CSS 83 | 84 | --- 85 | 86 | # Code Sample 87 | 88 | Darkslide supports code snippets 89 | 90 | !python 91 | def log(self, message, level='notice'): 92 | if self.logger and not callable(self.logger): 93 | raise ValueError(u"Invalid logger set, must be a callable") 94 | 95 | if self.verbose and self.logger: 96 | self.logger(message, level) 97 | 98 | Requirements 99 | ============ 100 | 101 | ``python`` and the following modules: 102 | 103 | - ``jinja2`` 104 | - ``pygments`` for code blocks syntax coloration 105 | 106 | Markup Conversion 107 | ----------------- 108 | 109 | - ``markdown`` for `Markdown `__ 110 | - ``docutils`` for `reStructured 111 | Text `__ 112 | - ``textile`` for 113 | `Textile `__ 114 | 115 | Optional 116 | -------- 117 | 118 | - ``watchdog`` for watching/auto-regeneration with the ``-w`` flag 119 | 120 | Installation 121 | ============ 122 | 123 | Install the latest stable version of Darkslide with a python package 124 | manager like ``pip``: 125 | 126 | :: 127 | 128 | $ pip install darkslide 129 | 130 | If you want to stay on the edge: 131 | 132 | :: 133 | 134 | $ git clone https://github.com/ionelmc/python-darkslide.git 135 | $ cd python-darkslide 136 | $ python setup.py build 137 | $ sudo python setup.py install 138 | 139 | Formatting 140 | ========== 141 | 142 | Markdown 143 | -------- 144 | 145 | - Your Markdown source files must be suffixed by ``.md``, ``.markdn``, 146 | ``.mdwn``, ``.mdown`` or ``.markdown`` 147 | - To create a title slide, render a single ``h1`` element (eg. 148 | ``# My Title``) 149 | - Separate your slides with a horizontal rule (``---`` in markdown) 150 | except at the end of md files 151 | - Your other slides should have a heading that renders to an ``h1`` 152 | element 153 | - To highlight blocks of code, put ``!lang`` where ``lang`` is the 154 | pygment supported language identifier as the first indented line 155 | 156 | ReStructuredText 157 | ---------------- 158 | 159 | - Your ReST source files must be suffixed by ``.rst`` or ``.rest`` 160 | (**``.txt`` is not supported**) 161 | - Use headings for slide titles 162 | - Separate your slides using an horizontal rule (``----`` in RST) 163 | except at the end of RST files 164 | 165 | Textile 166 | ------- 167 | 168 | - Separate your slides using ``---``, just like in markdown 169 | 170 | Rendering 171 | ========= 172 | 173 | - Run ``darkslide slides.md`` or ``darkslide slides.rst`` 174 | - Enjoy your newly generated ``presentation.html`` 175 | 176 | Viewing 177 | ======= 178 | 179 | - Press ``h`` to toggle display of help 180 | - Press ``left arrow`` and ``right arrow`` to navigate 181 | - Press ``t`` to toggle a table of contents for your presentation. 182 | Slide titles are links 183 | - Press ``ESC`` to display the presentation overview (Exposé) 184 | - Press ``n`` to toggle slide number visibility 185 | - Press ``b`` to toggle screen blanking 186 | - Press ``c`` to toggle double slide display (current and next 187 | slides) 188 | - Press ``S`` to toggle display of link to the source file for each 189 | slide 190 | - Press '2' to toggle notes in your slides (specify with the .notes 191 | macro) 192 | - Browser zooming is *not* supported 193 | 194 | Commandline Options 195 | =================== 196 | 197 | Usage:: 198 | 199 | darkslide [options] input.md ... 200 | 201 | Options: 202 | --version show program's version number and exit 203 | -h, --help show this help message and exit 204 | -b, --debug Will display any exception trace to stdout. 205 | -d FILE, --destination=FILE 206 | The path to the to the destination html file. Default: 207 | presentation.html. 208 | -e ENCODING, --encoding=ENCODING 209 | The encoding of your files. Default: utf8. 210 | -i, --embed Embed stylesheet and javascript contents, 211 | base64-encoded images and objects in presentation to 212 | make a standalone document. 213 | -l LINENOS, --linenos=LINENOS 214 | How to output linenos in source code. Three options 215 | available: no (no line numbers); inline (inside
216 |                         tag); table (lines numbers in another cell, copy-paste
217 |                         friendly).
218 |   -m LEVEL, --max-toc-level=LEVEL
219 |                         Limits the TOC level generation to a specific level.
220 |   -M, --mod=MOD
221 |                         Comma-separated list of theme modifiers by name.
222 |                         Available: wide16x9, showpagenums.
223 |   -o, --direct-output   Prints the generated HTML code to stdout.
224 |   -P, --no-presenter-notes
225 |                         Don't include presenter notes in the output.
226 |   -q, --quiet           Won't write anything to stdout (silent mode).
227 |   -r, --relative        Make your presentation asset links relative to current
228 |                         working dir; This may be useful if you intend to
229 |                         publish your html presentation online.
230 |   -t THEME, --theme=THEME
231 |                         A theme name, or path to a darkslide theme directory
232 |   -v, --verbose         Write informational messages to stdout (enabled by
233 |                         default).
234 |   -x EXTENSIONS, --extensions=EXTENSIONS
235 |                         Comma-separated list of extensions for Markdown.
236 |   -w, --watch           Watch source directory for changes and regenerate
237 |                         slides.
238 | 
239 | Presentation Configuration
240 | ==========================
241 | 
242 | Darkslide allows to configure your presentation using a ``cfg``
243 | configuration file, therefore easing the aggregation of source
244 | directories and the reuse of them across presentations. Darkslide
245 | configuration files use the ``cfg`` syntax. If you know ``ini`` files,
246 | you get the picture. Below is a sample configuration file:
247 | 
248 | .. code-block:: ini
249 | 
250 |     [darkslide]
251 |     ; the old [landslide] is still supported
252 |     theme  = /path/to/my/beautiful/theme
253 |     source = 0_my_first_slides.md
254 |              a_directory
255 |              another_directory
256 |              now_a_slide.markdown
257 |              another_one.rst
258 |     destination = myWonderfulPresentation.html
259 |     css =    my_first_stylesheet.css
260 |              my_other_stylesheet.css
261 |     js =     jquery.js
262 |              my_fancy_javascript.js
263 |     relative = True
264 |     linenos = inline
265 | 
266 | Don't forget to declare the ``[darkslide]`` section. All configuration
267 | files must end in the .cfg extension.
268 | 
269 | To generate the presentation as configured, just run:
270 | 
271 | ::
272 | 
273 |     $ cd /path/to/my/presentation/sources
274 |     $ darkslide config.cfg
275 | 
276 | Macros
277 | ======
278 | 
279 | You can use macros to enhance your presentation:
280 | 
281 | Notes
282 | -----
283 | 
284 | Add notes to your slides using the ``.notes:`` keyword, eg.:
285 | 
286 | ::
287 | 
288 |     # My Slide Title
289 | 
290 |     .notes: These are my notes, hidden by default
291 | 
292 |     My visible content goes here
293 | 
294 | You can toggle display of notes by pressing the ``2`` key.
295 | 
296 | Some other macros are also available by default: ``.fx: foo bar`` will
297 | add the ``foo`` and ``bar`` classes to the corresponding slide ``
`` 298 | element, easing styling of your presentation using CSS. 299 | 300 | QR Codes 301 | -------- 302 | 303 | Add a QR Code to your presentation by using the ``.qr`` keyword: 304 | 305 | :: 306 | 307 | .qr: https://github.com/ionelmc/python-darkslide 308 | 309 | Footnote 310 | -------- 311 | 312 | Add footnote to the current and all the following presentations 313 | 314 | :: 315 | 316 | .footnote: Slides available at https://blog.ionelmc.ro/presentations/ 317 | 318 | 319 | Presenter Notes 320 | =============== 321 | 322 | You can also add presenter notes to each slide by following the slide 323 | content with a heading entitled "Presenter Notes". Press the 'p' key to 324 | open the presenter view. 325 | 326 | Registering Macros 327 | ================== 328 | 329 | Macros are used to transform the HTML contents of your slide. 330 | 331 | You can register your own macros by creating ``darkslide.macro.Macro`` 332 | derived classes, implementing a ``process(content, source=None)`` method 333 | and returning a tuple containing the modified contents and some css 334 | classes you may be wanting to add to your slide ``
`` element. For 335 | example: 336 | 337 | :: 338 | 339 | !python 340 | import darkslide 341 | 342 | class MyMacro(darkslide.Macro): 343 | def process(self, content, source=None): 344 | return content + '

plop

', ['plopped_slide'] 345 | 346 | g = darkslide.generator.Generator(source='toto.md') 347 | g.register_macro(MyMacro) 348 | print g.render() 349 | 350 | This will render any slide as below: 351 | 352 | :: 353 | 354 | !html 355 |
356 |

foo

357 |
358 |

my slide contents

359 |

plop

360 |
361 |
362 | 363 | Advanced Usage 364 | ============== 365 | 366 | Setting Custom Destination File 367 | ------------------------------- 368 | 369 | :: 370 | 371 | $ darkslide slides.md -d ~/MyPresentations/presentation.html 372 | 373 | Working with Directories 374 | ------------------------ 375 | 376 | :: 377 | 378 | $ darkslide slides/ 379 | 380 | Working with Direct Output 381 | -------------------------- 382 | 383 | :: 384 | 385 | $ darkslide slides.md -o | tidy 386 | 387 | Using an Alternate Darkslide Theme 388 | ---------------------------------- 389 | 390 | :: 391 | 392 | $ darkslide slides.md -t mytheme 393 | $ darkslide slides.md -t /path/to/theme/dir 394 | 395 | Embedding Base-64-Encoded Images 396 | -------------------------------- 397 | 398 | :: 399 | 400 | $ darkslide slides.md -i 401 | 402 | Enabling Markdown Extensions 403 | ---------------------------- 404 | 405 | See documentation on available Markdown extensions 406 | `here `__: 407 | 408 | :: 409 | 410 | $ darkslide slides.md -x abbr 411 | 412 | Theming 413 | ------- 414 | 415 | A Darkslide theme is a directory following this simple structure: 416 | 417 | :: 418 | 419 | mytheme/ 420 | |-- base.html 421 | |-- css 422 | | |-- print.css 423 | | `-- screen.css 424 | `-- js 425 | `-- slides.js 426 | 427 | If a theme does not provide HTML and JS files, those from the default 428 | theme will be used. CSS is not optional. 429 | 430 | Widescreen 16x9 431 | --------------- 432 | 433 | You can create widescreen 16x9 slides using the ``--mod=wide16x9`` option. 434 | 435 | User stylesheets and Javascripts 436 | ================================ 437 | 438 | If you don't want to bother making your own theme, you can include your 439 | own user css and js files to the generated presentation. 440 | 441 | This feature is only available if you use a Darkslide configuration 442 | file, by setting the ``css`` and/or ``js`` flags: 443 | 444 | :: 445 | 446 | [darkslide] 447 | ; the old [landslide] is still supported 448 | theme = /path/to/my/beautiful/theme 449 | source = slides.mdown 450 | css = custom.css 451 | js = jquery.js 452 | powerpoint.js 453 | 454 | These will link the ``custom.css`` stylesheet and both the ``jquery.js`` 455 | and ``powerpoint.js`` files within the ```` section of the 456 | presentation html file. 457 | 458 | **NOTE:** Paths to the css and js files must be relative to the 459 | directory you're running the ``darkslide`` command from. 460 | 461 | Publishing your Presentation Online 462 | =================================== 463 | 464 | For online publishing use the ``--embed`` option to produce a standalone 465 | HTML file with no dependencies:: 466 | 467 | $ darkslide slides.md --embed 468 | 469 | Theme Variables 470 | =============== 471 | 472 | The ``base.html`` must be a `Jinja2 template 473 | file `__ where you can 474 | harness the following template variables: 475 | 476 | - ``css``: the stylesheet contents, available via two keys, ``print`` 477 | and ``screen``, both having: 478 | - a ``path_url`` key storing the url to the asset file path 479 | - a ``contents`` key storing the asset contents 480 | - ``js``: the javascript contents, having: 481 | - a ``path_url`` key storing the url to the asset file path 482 | - a ``contents`` key storing the asset contents 483 | - ``slides``: the slides list, each one having these properties: 484 | - ``header``: the slide title 485 | - ``content``: the slide contents 486 | - ``number``: the slide number 487 | - ``embed``: is the current document a standalone one? 488 | - ``num_slides``: the number of slides in current presentation 489 | - ``toc``: the Table of Contents, listing sections of the document. 490 | Each section has these properties available: 491 | - ``title``: the section title 492 | - ``number``: the slide number of the section 493 | - ``sub``: subsections, if any 494 | 495 | Styles Scope 496 | ============ 497 | 498 | - To change HTML5 presentation styles, tweak the ``css/screen.css`` 499 | stylesheet bundled with the theme you are using 500 | - For printing, modify the ``css/print.css`` 501 | -------------------------------------------------------------------------------- /ci/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import subprocess 9 | import sys 10 | from os.path import abspath 11 | from os.path import dirname 12 | from os.path import exists 13 | from os.path import join 14 | from os.path import relpath 15 | 16 | base_path = dirname(dirname(abspath(__file__))) 17 | templates_path = join(base_path, "ci", "templates") 18 | 19 | 20 | def check_call(args): 21 | print("+", *args) 22 | subprocess.check_call(args) 23 | 24 | 25 | def exec_in_env(): 26 | env_path = join(base_path, ".tox", "bootstrap") 27 | if sys.platform == "win32": 28 | bin_path = join(env_path, "Scripts") 29 | else: 30 | bin_path = join(env_path, "bin") 31 | if not exists(env_path): 32 | import subprocess 33 | 34 | print("Making bootstrap env in: {0} ...".format(env_path)) 35 | try: 36 | check_call([sys.executable, "-m", "venv", env_path]) 37 | except subprocess.CalledProcessError: 38 | try: 39 | check_call([sys.executable, "-m", "virtualenv", env_path]) 40 | except subprocess.CalledProcessError: 41 | check_call(["virtualenv", env_path]) 42 | print("Installing `jinja2` into bootstrap environment...") 43 | check_call([join(bin_path, "pip"), "install", "jinja2", "tox"]) 44 | python_executable = join(bin_path, "python") 45 | if not os.path.exists(python_executable): 46 | python_executable += '.exe' 47 | 48 | print("Re-executing with: {0}".format(python_executable)) 49 | print("+ exec", python_executable, __file__, "--no-env") 50 | os.execv(python_executable, [python_executable, __file__, "--no-env"]) 51 | 52 | 53 | def main(): 54 | import jinja2 55 | 56 | print("Project path: {0}".format(base_path)) 57 | 58 | jinja = jinja2.Environment( 59 | loader=jinja2.FileSystemLoader(templates_path), 60 | trim_blocks=True, 61 | lstrip_blocks=True, 62 | keep_trailing_newline=True 63 | ) 64 | 65 | tox_environments = [ 66 | line.strip() 67 | # 'tox' need not be installed globally, but must be importable 68 | # by the Python that is running this script. 69 | # This uses sys.executable the same way that the call in 70 | # cookiecutter-pylibrary/hooks/post_gen_project.py 71 | # invokes this bootstrap.py itself. 72 | for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines() 73 | ] 74 | tox_environments = [line for line in tox_environments if line.startswith('py')] 75 | 76 | for root, _, files in os.walk(templates_path): 77 | for name in files: 78 | relative = relpath(root, templates_path) 79 | with open(join(base_path, relative, name), "w") as fh: 80 | fh.write(jinja.get_template(join(relative, name)).render(tox_environments=tox_environments)) 81 | print("Wrote {}".format(name)) 82 | print("DONE.") 83 | 84 | 85 | if __name__ == "__main__": 86 | args = sys.argv[1:] 87 | if args == ["--no-env"]: 88 | main() 89 | elif not args: 90 | exec_in_env() 91 | else: 92 | print("Unexpected arguments {0}".format(args), file=sys.stderr) 93 | sys.exit(1) 94 | -------------------------------------------------------------------------------- /ci/requirements.txt: -------------------------------------------------------------------------------- 1 | virtualenv>=16.6.0 2 | pip>=19.1.1 3 | setuptools>=18.0.1 4 | six>=1.14.0 5 | tox 6 | -------------------------------------------------------------------------------- /ci/templates/.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: {{ '${{ matrix.name }}' }} 6 | runs-on: {{ '${{ matrix.os }}' }} 7 | timeout-minutes: 30 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - name: 'check' 13 | python: '3.9' 14 | toxpython: 'python3.9' 15 | tox_env: 'check' 16 | os: 'ubuntu-latest' 17 | - name: 'docs' 18 | python: '3.9' 19 | toxpython: 'python3.9' 20 | tox_env: 'docs' 21 | os: 'ubuntu-latest' 22 | {% for env in tox_environments %} 23 | {% set prefix = env.split('-')[0] -%} 24 | {% if prefix.startswith('pypy') %} 25 | {% set python %}pypy-{{ prefix[4] }}.{{ prefix[5] }}{% endset %} 26 | {% set cpython %}pp{{ prefix[4:5] }}{% endset %} 27 | {% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5] }}{% endset %} 28 | {% else %} 29 | {% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} 30 | {% set cpython %}cp{{ prefix[2:] }}{% endset %} 31 | {% set toxpython %}python{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} 32 | {% endif %} 33 | {% for os, python_arch in [ 34 | ['ubuntu', 'x64'], 35 | ['windows', 'x64'], 36 | ['windows', 'x86'], 37 | ['macos', 'x64'], 38 | ] %} 39 | - name: '{{ env }} ({{ os }})' 40 | python: '{{ python }}' 41 | toxpython: '{{ toxpython }}' 42 | python_arch: '{{ python_arch }}' 43 | tox_env: '{{ env }}{% if 'cover' in env %},codecov{% endif %}' 44 | os: '{{ os }}-latest' 45 | {% endfor %} 46 | {% endfor %} 47 | steps: 48 | - uses: actions/checkout@v2 49 | with: 50 | fetch-depth: 0 51 | - uses: actions/setup-python@v2 52 | with: 53 | python-version: {{ '${{ matrix.python }}' }} 54 | architecture: {{ '${{ matrix.python_arch }}' }} 55 | - name: install dependencies 56 | run: | 57 | python -mpip install --progress-bar=off -r ci/requirements.txt 58 | virtualenv --version 59 | pip --version 60 | tox --version 61 | pip list --format=freeze 62 | - name: test 63 | env: 64 | TOXPYTHON: '{{ '${{ matrix.toxpython }}' }}' 65 | run: > 66 | tox -e {{ '${{ matrix.tox_env }}' }} -v 67 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import sphinx_py3doc_enhanced_theme 5 | 6 | extensions = [ 7 | 'sphinx.ext.autodoc', 8 | 'sphinx.ext.autosummary', 9 | 'sphinx.ext.coverage', 10 | 'sphinx.ext.doctest', 11 | 'sphinx.ext.extlinks', 12 | 'sphinx.ext.ifconfig', 13 | 'sphinx.ext.napoleon', 14 | 'sphinx.ext.todo', 15 | 'sphinx.ext.viewcode', 16 | ] 17 | source_suffix = '.rst' 18 | master_doc = 'index' 19 | project = 'Darkslide' 20 | year = '2015-2021' 21 | author = 'Ionel Cristian Mărieș' 22 | copyright = '{0}, {1}'.format(year, author) 23 | version = release = '6.0.0' 24 | 25 | pygments_style = 'trac' 26 | templates_path = ['.'] 27 | extlinks = { 28 | 'issue': ('https://github.com/ionelmc/python-darkslide/issues/%s', '#'), 29 | 'pr': ('https://github.com/ionelmc/python-darkslide/pull/%s', 'PR #'), 30 | } 31 | html_theme = "sphinx_py3doc_enhanced_theme" 32 | html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] 33 | html_theme_options = { 34 | 'githuburl': 'https://github.com/ionelmc/python-darkslide/' 35 | } 36 | 37 | html_use_smartypants = True 38 | html_last_updated_fmt = '%b %d, %Y' 39 | html_split_index = False 40 | html_sidebars = { 41 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 42 | } 43 | html_short_title = '%s-%s' % (project, version) 44 | 45 | napoleon_use_ivar = True 46 | napoleon_use_rtype = False 47 | napoleon_use_param = False 48 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Contents 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | readme 9 | installation 10 | usage 11 | reference/index 12 | contributing 13 | authors 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | pip install darkslide 8 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/reference/darkslide.rst: -------------------------------------------------------------------------------- 1 | darkslide 2 | ========= 3 | 4 | .. testsetup:: 5 | 6 | from darkslide import * 7 | 8 | .. automodule:: darkslide 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | darkslide* 8 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.3 2 | sphinx-py3doc-enhanced-theme 3 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use Darkslide in a project:: 6 | 7 | import darkslide 8 | -------------------------------------------------------------------------------- /examples/_assets/SourceSansPro.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionelmc/python-darkslide/f28c9adaf88a67701668c3f6c70ad36d0aee2121/examples/_assets/SourceSansPro.woff2 -------------------------------------------------------------------------------- /examples/_assets/landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionelmc/python-darkslide/f28c9adaf88a67701668c3f6c70ad36d0aee2121/examples/_assets/landscape.jpg -------------------------------------------------------------------------------- /examples/config-file/presentation.cfg: -------------------------------------------------------------------------------- 1 | [darkslide] 2 | source = ../markdown 3 | ../restructuredtext 4 | ../textile 5 | ../multiple-files 6 | ../other-features 7 | # use a custom CSS file that embeds an additional font 8 | css = ../user_css/style.css 9 | -------------------------------------------------------------------------------- /examples/markdown/slides.md: -------------------------------------------------------------------------------- 1 | # Title Slide 2 | 3 | .footer: Themes: [default](index.html) | [abyss](abyss.html) | [void](void.html) | [white](white.html) Themes 16x9: [default](default_wide16x9.html) | [abyss](abyss_wide16x9.html) | [void](void_wide16x9.html) | [white](white_wide16x9.html) Project: [github](https://github.com/ionelmc/python-darkslide) 4 | 5 | --- 6 | 7 | # Paragraphs 8 | 9 | .notes: notes 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 12 | tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 13 | scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 14 | elit, vel iaculis lorem facilisis non. 15 | 16 | .notes: more notes 17 | 18 | Vivamus ante mauris, pellentesque et rhoncus ut, condimentum sed ipsum. 19 | Nullam eu molestie sapien. Curabitur imperdiet, ligula id blandit sagittis, 20 | libero quam consectetur felis, ac pharetra metus lectus vitae leo. Mauris 21 | lacinia fermentum augue. 22 | 23 | --- 24 | 25 | # Blockquotes 26 | 27 | > Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 28 | > tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 29 | > scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 30 | > elit, vel iaculis lorem facilisis non. 31 | 32 | --- 33 | 34 | # Subheadings and Emphasis 35 | 36 | ## Italic 37 | 38 | *Lorem ipsum dolor sit amet* 39 | 40 | ## Bold 41 | 42 | **Lorem ipsum dolor sit amet** 43 | 44 | --- 45 | 46 | # First heading: *lorem ipsum* dolor sit 47 | ## Second: *lorem ipsum* dolor sfi amet, consecte 48 | ### Third: *lorem ipsum* dolor sit amet, consectetur adipiscing 49 | 50 | * 1 51 | * 2 52 | * 3 53 | * 4 54 | * 5 55 | * 6 56 | * 7 57 | 58 | --- 59 | 60 | # Lists 61 | 62 | ## Unordered List 63 | 64 | - Markdown 65 | - ReStructured Text 66 | - Textile 67 | 68 | ## Ordered List 69 | 70 | 1. Python 71 | 2. JavaScript 72 | 3. HTML5 73 | 74 | --- 75 | 76 | # Second Title Slide 77 | 78 | --- 79 | 80 | # Code 81 | 82 | ## Single Word 83 | 84 | Hello `World` 85 | 86 | ## Python 87 | 88 | !python 89 | def multiply (x, y): 90 | return x * y 91 | 92 | ## JavaScript 93 | 94 | !javascript 95 | multiply: function (x, y) { 96 | return x * y; 97 | } 98 | 99 | ## HTML 100 | 101 | !html 102 | 103 | 104 | 105 | 106 | 107 | 108 | --- 109 | 110 | # Images 111 | 112 | ![Landscape](../_assets/landscape.jpg) 113 | 114 | --- 115 | 116 | # View Presenter Notes 117 | 118 | This slide has presenter notes. Press `p` to view them. 119 | 120 | # Presenter Notes 121 | 122 | Hello from presenter notes 123 | 124 | --- 125 | 126 | # TOC and subsections 127 | 128 | A presentation supports multiple section levels. Subsections can be used to organize contents. 129 | 130 | The global TOC includes sections and subsections. 131 | 132 | The max-toc-level parameter allows the user to limit the number of subsections included in the global TOC. 133 | 134 | --- 135 | 136 | ## Subsection A 137 | 138 | This slide is a subsection of the section "Toc and subsections" 139 | 140 | --- 141 | 142 | ## Subsection B 143 | 144 | This slide is a subsection of the section "Toc and subsections" 145 | 146 | --- 147 | 148 | # Other features 149 | 150 | View other features in the help sidebar by pressing `h` 151 | -------------------------------------------------------------------------------- /examples/multiple-files/1.md: -------------------------------------------------------------------------------- 1 | # Title Slide 2 | 3 | --- 4 | 5 | # Paragraphs 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 8 | tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 9 | scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 10 | elit, vel iaculis lorem facilisis non. 11 | 12 | Vivamus ante mauris, pellentesque et rhoncus ut, condimentum sed ipsum. 13 | Nullam eu molestie sapien. Curabitur imperdiet, ligula id blandit sagittis, 14 | libero quam consectetur felis, ac pharetra metus lectus vitae leo. Mauris 15 | lacinia fermentum augue. 16 | 17 | --- 18 | 19 | # Blockquotes 20 | 21 | > Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 22 | > tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 23 | > scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 24 | > elit, vel iaculis lorem facilisis non. 25 | 26 | --- 27 | 28 | # Subheadings and Emphasis 29 | 30 | ## Italic 31 | 32 | *Lorem ipsum dolor sit amet* 33 | 34 | ## Bold 35 | 36 | **Lorem ipsum dolor sit amet** 37 | -------------------------------------------------------------------------------- /examples/multiple-files/2.rst: -------------------------------------------------------------------------------- 1 | Lists 2 | ===== 3 | 4 | Unordered List 5 | -------------- 6 | 7 | - Markdown 8 | - ReStructured Text 9 | - Textile 10 | 11 | Ordered List 12 | ------------ 13 | 14 | 1. Python 15 | 2. JavaScript 16 | 3. HTML5 17 | 18 | ---- 19 | 20 | Second Title Slide 21 | ================== 22 | 23 | ---- 24 | 25 | Code 26 | ==== 27 | 28 | Single Word 29 | ----------- 30 | 31 | Hello ``World`` 32 | 33 | Python 34 | ------ 35 | 36 | .. code-block:: python 37 | 38 | def multiply (x, y): 39 | return x * y 40 | 41 | JavaScript 42 | ---------- 43 | 44 | .. code-block:: javascript 45 | 46 | multiply: function (x, y) { 47 | return x * y; 48 | } 49 | 50 | HTML 51 | ---- 52 | 53 | .. code-block:: html 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/multiple-files/3.textile: -------------------------------------------------------------------------------- 1 | h1. Images 2 | 3 | !../_assets/landscape.jpg(Landscape)! 4 | 5 |
6 | 7 | h1. View Presenter Notes 8 | 9 | This slide has presenter notes. Press `p` to view them. 10 | 11 | h1. Presenter Notes 12 | 13 | Hello from presenter notes 14 | 15 |
16 | 17 | h1. Other features 18 | 19 | View other features in the help sidebar by pressing `h` 20 | -------------------------------------------------------------------------------- /examples/other-features/slides.md: -------------------------------------------------------------------------------- 1 | # Other Features: QR 2 | 3 | .qr: https://github.com/ionelmc/python-darkslide 4 | -------------------------------------------------------------------------------- /examples/restructuredtext/slides.rst: -------------------------------------------------------------------------------- 1 | Title Slide 2 | =========== 3 | 4 | ---- 5 | 6 | Paragraphs 7 | ========== 8 | 9 | .notes: notes 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 12 | tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 13 | scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 14 | elit, vel iaculis lorem facilisis non. 15 | 16 | .notes: more notes 17 | 18 | Vivamus ante mauris, pellentesque et rhoncus ut, condimentum sed ipsum. 19 | Nullam eu molestie sapien. Curabitur imperdiet, ligula id blandit sagittis, 20 | libero quam consectetur felis, ac pharetra metus lectus vitae leo. Mauris 21 | lacinia fermentum augue. 22 | 23 | ---- 24 | 25 | Blockquotes 26 | =========== 27 | 28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 29 | tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 30 | scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula 31 | nunc elit, vel iaculis lorem facilisis non. 32 | 33 | ---- 34 | 35 | Subheadings and Emphasis 36 | ======================== 37 | 38 | Italic 39 | ------ 40 | 41 | *Lorem ipsum dolor sit amet* 42 | 43 | Bold 44 | ---- 45 | 46 | **Lorem ipsum dolor sit amet** 47 | 48 | ---- 49 | 50 | First heading: *lorem ipsum* dolor sit 51 | ====================================== 52 | 53 | Second: *lorem ipsum* dolor sfi amet, consecte 54 | ---------------------------------------------- 55 | 56 | Third: *lorem ipsum* dolor sit amet, consectetur adipiscing 57 | ``````````````````````````````````````````````````````````` 58 | 59 | * 1 60 | * 2 61 | * 3 62 | * 4 63 | * 5 64 | * 6 65 | * 7 66 | 67 | ---- 68 | 69 | Lists 70 | ===== 71 | 72 | Unordered List 73 | -------------- 74 | 75 | - Markdown 76 | - ReStructured Text 77 | - Textile 78 | 79 | Ordered List 80 | ------------ 81 | 82 | 1. Python 83 | 2. JavaScript 84 | 3. HTML5 85 | 86 | ---- 87 | 88 | Second Title Slide 89 | ================== 90 | 91 | ---- 92 | 93 | Code 94 | ==== 95 | 96 | Single Word 97 | ----------- 98 | 99 | Hello ``World`` 100 | 101 | Python 102 | ------ 103 | 104 | .. code-block:: python 105 | 106 | def multiply (x, y): 107 | return x * y 108 | 109 | JavaScript 110 | ---------- 111 | 112 | .. code-block:: javascript 113 | 114 | multiply: function (x, y) { 115 | return x * y; 116 | } 117 | 118 | HTML 119 | ---- 120 | 121 | .. code-block:: html 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | ---- 130 | 131 | Images 132 | ====== 133 | 134 | .. image:: ../_assets/landscape.jpg 135 | 136 | ---- 137 | 138 | View Presenter Notes 139 | ==================== 140 | 141 | This slide has presenter notes. Press ``p`` to view them. 142 | 143 | Presenter Notes 144 | =============== 145 | 146 | Hello from presenter notes 147 | 148 | ---- 149 | 150 | TOC and subsections 151 | =================== 152 | 153 | A presentation supports multiple section levels. Subsections can be used to organize contents and generate local table of contents. 154 | 155 | A table of content with depth 1: 156 | 157 | .. contents:: 158 | :local: 159 | :depth: 1 160 | 161 | ---- 162 | 163 | Subsection A 164 | ------------ 165 | 166 | This slide is a subsection of the section "Toc and subsections" 167 | 168 | ---- 169 | 170 | Subsection B 171 | ------------ 172 | 173 | This slide is a subsection of the section "Toc and subsections" 174 | 175 | ---- 176 | 177 | About the global TOC 178 | -------------------- 179 | 180 | The global TOC includes sections and subsections. 181 | 182 | The max-toc-level parameter allows the user to limit the number of subsections included in the global TOC. 183 | 184 | ---- 185 | 186 | Other features 187 | ============== 188 | 189 | View other features in the help sidebar by pressing ``h`` 190 | -------------------------------------------------------------------------------- /examples/textile/slides.textile: -------------------------------------------------------------------------------- 1 | h1. Title Slide 2 | 3 | --- 4 | 5 | h1. Paragraphs 6 | 7 | .notes: notes 8 | 9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 10 | tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 11 | scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 12 | elit, vel iaculis lorem facilisis non. 13 | 14 | .notes: more notes 15 | 16 | Vivamus ante mauris, pellentesque et rhoncus ut, condimentum sed ipsum. 17 | Nullam eu molestie sapien. Curabitur imperdiet, ligula id blandit sagittis, 18 | libero quam consectetur felis, ac pharetra metus lectus vitae leo. Mauris 19 | lacinia fermentum augue. 20 | 21 | --- 22 | 23 | h1. Blockquotes 24 | 25 | bq. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 26 | tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 27 | scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 28 | elit, vel iaculis lorem facilisis non. 29 | 30 | --- 31 | 32 | h1. Subheadings and Emphasis 33 | 34 | h2. Italic 35 | 36 | _Lorem ipsum dolor sit amet_ 37 | 38 | h2. Bold 39 | 40 | *Lorem ipsum dolor sit amet* 41 | 42 | --- 43 | 44 | h1. Lists 45 | 46 | h2. Unordered List 47 | 48 | * Markdown 49 | * ReStructured Text 50 | * Textile 51 | 52 | h2. Ordered List 53 | 54 | # Python 55 | # JavaScript 56 | # HTML5 57 | 58 | --- 59 | 60 | h1. Second Title Slide 61 | 62 | --- 63 | 64 | h1. Code 65 | 66 | h2. Single Word 67 | 68 | Hello @World@ 69 | 70 | h2. Python 71 | 72 | bc. def multiply (x, y): 73 | return x * y 74 | 75 | h2. JavaScript 76 | 77 | bc. multiply: function (x, y) { 78 | return x * y; 79 | } 80 | 81 | h2. HTML 82 | 83 | bc. 84 | 85 | 86 | 87 | 88 | 89 | --- 90 | 91 | h1. Images 92 | 93 | !../_assets/landscape.jpg(Landscape)! 94 | 95 | --- 96 | 97 | h1. View Presenter Notes 98 | 99 | This slide has presenter notes. Press `p` to view them. 100 | 101 | h1. Presenter Notes 102 | 103 | Hello from presenter notes 104 | 105 | --- 106 | 107 | h1. Other features 108 | 109 | View other features in the help sidebar by pressing `h` 110 | -------------------------------------------------------------------------------- /examples/user_css/style.css: -------------------------------------------------------------------------------- 1 | /* latin */ 2 | @font-face { 3 | font-family: 'Source Sans Pro'; 4 | font-style: normal; 5 | font-weight: 400; 6 | font-display: swap; 7 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(../_assets/SourceSansPro.woff2) format('woff2'); 8 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 9 | } 10 | 11 | blockquote { 12 | font-family: 'Source Sans Pro', 'Comic Sans MS'; 13 | } 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 140 6 | exclude = .tox,.eggs,ci/templates,build,dist 7 | 8 | [tool:pytest] 9 | # If a pytest section is found in one of the possible config files 10 | # (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, 11 | # so if you add a pytest config section elsewhere, 12 | # you will need to delete this section from setup.cfg. 13 | norecursedirs = 14 | migrations 15 | 16 | python_files = 17 | test_*.py 18 | *_test.py 19 | tests.py 20 | addopts = 21 | -ra 22 | --strict-markers 23 | --tb=short 24 | testpaths = 25 | tests 26 | 27 | [tool:isort] 28 | force_single_line = True 29 | line_length = 120 30 | known_first_party = darkslide 31 | default_section = THIRDPARTY 32 | forced_separate = test_darkslide 33 | skip = .tox,.eggs,ci/templates,build,dist 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | import io 5 | import re 6 | from glob import glob 7 | from os.path import basename 8 | from os.path import dirname 9 | from os.path import join 10 | from os.path import splitext 11 | 12 | from setuptools import find_packages 13 | from setuptools import setup 14 | 15 | 16 | def read(*names, **kwargs): 17 | with io.open( 18 | join(dirname(__file__), *names), 19 | encoding=kwargs.get('encoding', 'utf8') 20 | ) as fh: 21 | return fh.read() 22 | 23 | 24 | setup( 25 | name='darkslide', 26 | version='6.0.0', 27 | license='Apache-2.0', 28 | description='Lightweight markup language-based html5 slideshow generator. Forked from Landslide.', 29 | long_description='%s\n%s' % ( 30 | re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), 31 | re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) 32 | ), 33 | author='Ionel Cristian Mărieș', 34 | author_email='contact@ionelmc.ro', 35 | url='https://github.com/ionelmc/python-darkslide', 36 | packages=find_packages('src'), 37 | package_dir={'': 'src'}, 38 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 39 | include_package_data=True, 40 | zip_safe=False, 41 | classifiers=[ 42 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 43 | 'Development Status :: 5 - Production/Stable', 44 | 'Intended Audience :: Developers', 45 | 'License :: OSI Approved :: Apache Software License', 46 | 'Operating System :: Unix', 47 | 'Operating System :: POSIX', 48 | 'Operating System :: Microsoft :: Windows', 49 | 'Programming Language :: Python', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python :: 3 :: Only', 52 | 'Programming Language :: Python :: 3.6', 53 | 'Programming Language :: Python :: 3.7', 54 | 'Programming Language :: Python :: 3.8', 55 | 'Programming Language :: Python :: 3.9', 56 | 'Programming Language :: Python :: 3.10', 57 | 'Programming Language :: Python :: Implementation :: CPython', 58 | 'Programming Language :: Python :: Implementation :: PyPy', 59 | # uncomment if you test on these interpreters: 60 | # 'Programming Language :: Python :: Implementation :: IronPython', 61 | # 'Programming Language :: Python :: Implementation :: Jython', 62 | # 'Programming Language :: Python :: Implementation :: Stackless', 63 | 'Topic :: Utilities', 64 | ], 65 | project_urls={ 66 | 'Documentation': 'https://darkslide.readthedocs.io/', 67 | 'Changelog': 'https://darkslide.readthedocs.io/en/latest/changelog.html', 68 | 'Issue Tracker': 'https://github.com/ionelmc/python-darkslide/issues', 69 | }, 70 | keywords=[ 71 | 'markdown', 72 | 'slideshow', 73 | 'presentation', 74 | 'rst', 75 | 'restructuredtext', 76 | 'textile', 77 | 'landslide', 78 | ], 79 | python_requires='>=3.6', 80 | install_requires=[ 81 | 'Jinja2', 82 | 'Markdown', 83 | 'Pygments', 84 | 'docutils', 85 | 'six', 86 | 'qrcode', 87 | ], 88 | extras_require={ 89 | # eg: 'rst': ['docutils>=0.11'], 90 | }, 91 | entry_points={ 92 | 'console_scripts': [ 93 | 'darkslide = darkslide.cli:main', 94 | ] 95 | }, 96 | ) 97 | -------------------------------------------------------------------------------- /src/darkslide/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '6.0.0' 2 | -------------------------------------------------------------------------------- /src/darkslide/__main__.py: -------------------------------------------------------------------------------- 1 | from darkslide.cli import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /src/darkslide/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | from optparse import OptionParser 5 | 6 | from . import __version__ 7 | from . import generator 8 | 9 | 10 | def parse_options(): 11 | """Parses landslide's command line options""" 12 | 13 | parser = OptionParser( 14 | usage="%prog [options] input.md ...", 15 | description="Generates a HTML5 slideshow from Markdown or other formats.", 16 | version="%prog " + __version__) 17 | 18 | parser.add_option( 19 | "-b", "--debug", 20 | action="store_true", 21 | dest="debug", 22 | help="Will display any exception trace to stdout.", 23 | default=False) 24 | 25 | parser.add_option( 26 | "-d", "--destination", 27 | dest="destination_file", 28 | help="The path to the to the destination html file. Default: presentation.html.", 29 | metavar="FILE", 30 | default="presentation.html") 31 | 32 | parser.add_option( 33 | "-e", "--encoding", 34 | dest="encoding", 35 | help="The encoding of your files. Default: utf8.", 36 | metavar="ENCODING", 37 | default="utf8") 38 | 39 | parser.add_option( 40 | "-i", "--embed", 41 | action="store_true", 42 | dest="embed", 43 | help="Embed stylesheet and javascript contents, base64-encoded images and objects in presentation to make a " 44 | "standalone document.", 45 | default=False) 46 | 47 | parser.add_option( 48 | "-l", "--linenos", 49 | type="choice", 50 | choices=generator.VALID_LINENOS, 51 | dest="linenos", 52 | help="How to output linenos in source code. Three options available: " 53 | "no (no line numbers); " 54 | "inline (inside
 tag); "
 55 |              "table (lines numbers in another cell, copy-paste friendly).",
 56 |         default="inline",
 57 |     )
 58 | 
 59 |     parser.add_option(
 60 |         "-m", "--max-toc-level",
 61 |         type="int",
 62 |         dest="maxtoclevel",
 63 |         help="Limits the TOC level generation to a specific level.",
 64 |         default=2)
 65 | 
 66 |     parser.add_option(
 67 |         "-M", "--mod",
 68 |         dest="theme_mod",
 69 |         help="Comma-separated list of theme modifiers by name. "
 70 |         "Available: wide16x9, showpagenums.",
 71 |         default='')
 72 | 
 73 |     parser.add_option(
 74 |         "-o", "--direct-output",
 75 |         action="store_true",
 76 |         dest="direct",
 77 |         help="Prints the generated HTML code to stdout.",
 78 |         default=False)
 79 | 
 80 |     parser.add_option(
 81 |         "-P", "--no-presenter-notes",
 82 |         action="store_false",
 83 |         dest="presenter_notes",
 84 |         help="Don't include presenter notes in the output.",
 85 |         default=True)
 86 | 
 87 |     parser.add_option(
 88 |         "-q", "--quiet",
 89 |         action="store_false",
 90 |         dest="verbose",
 91 |         help="Won't write anything to stdout (silent mode).",
 92 |         default=False)
 93 | 
 94 |     parser.add_option(
 95 |         "-r", "--relative",
 96 |         action="store_true",
 97 |         dest="relative",
 98 |         help="Make your presentation asset links relative to current working dir; "
 99 |              "This may be useful if you intend to publish your html "
100 |              "presentation online.",
101 |         default=False,
102 |     )
103 | 
104 |     parser.add_option(
105 |         "-t", "--theme",
106 |         dest="theme",
107 |         help="A theme name, or path to a landlside theme directory",
108 |         default='default')
109 | 
110 |     parser.add_option(
111 |         "-v", "--verbose",
112 |         action="store_true",
113 |         dest="verbose",
114 |         help="Write informational messages to stdout (enabled by default).",
115 |         default=True)
116 | 
117 |     parser.add_option(
118 |         "-x", "--extensions",
119 |         dest="extensions",
120 |         help="Comma-separated list of extensions for Markdown.",
121 |         default='',
122 |     )
123 | 
124 |     parser.add_option(
125 |         "-w", "--watch",
126 |         action="store_true",
127 |         dest="watch",
128 |         help="Watch source directory for changes and regenerate slides.",
129 |         default=False
130 |     )
131 | 
132 |     options, args = parser.parse_args()
133 | 
134 |     if not args:
135 |         parser.print_help()
136 |         sys.exit(1)
137 | 
138 |     return options, args[0]
139 | 
140 | 
141 | def log(message, type):
142 |     """Log notices to stdout and errors to stderr"""
143 | 
144 |     (sys.stdout if type == 'notice' else sys.stderr).write(message + "\n")
145 | 
146 | 
147 | def run(input_file, options):
148 |     """Runs the Generator using parsed options."""
149 | 
150 |     options.logger = log
151 |     generator.Generator(input_file, **options.__dict__).execute()
152 | 
153 | 
154 | def main():
155 |     """Main program entry point"""
156 | 
157 |     options, input_file = parse_options()
158 | 
159 |     if options.debug:
160 |         run(input_file, options)
161 |     else:
162 |         try:
163 |             run(input_file, options)
164 |         except Exception as e:
165 |             sys.stderr.write("Error: %s\n" % e)
166 |             sys.exit(1)
167 | 


--------------------------------------------------------------------------------
/src/darkslide/generator.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | import codecs
  3 | import inspect
  4 | import os
  5 | import re
  6 | import sys
  7 | 
  8 | import jinja2
  9 | from six import binary_type
 10 | from six import string_types
 11 | from six.moves import configparser
 12 | 
 13 | from . import __version__
 14 | from . import macro as macro_module
 15 | from . import utils
 16 | from .parser import Parser
 17 | from .utils import cached_property
 18 | 
 19 | BASE_DIR = os.path.dirname(__file__)
 20 | THEMES_DIR = os.path.join(BASE_DIR, 'themes')
 21 | MODS_DIR = os.path.join(BASE_DIR, 'mods')
 22 | VALID_LINENOS = ('no', 'inline', 'table')
 23 | 
 24 | 
 25 | class Generator(object):
 26 |     """
 27 |     The Generator class takes and processes presentation source as a file, a
 28 |     folder or a configuration file and provides methods to render them as a
 29 |     presentation.
 30 |     """
 31 |     default_macros = (
 32 |         macro_module.CodeHighlightingMacro,
 33 |         macro_module.EmbedImagesMacro,
 34 |         macro_module.FixImagePathsMacro,
 35 |         macro_module.FxMacro,
 36 |         macro_module.NotesMacro,
 37 |         macro_module.QRMacro,
 38 |         macro_module.FooterMacro,
 39 |     )
 40 | 
 41 |     def __init__(self, source, **kwargs):
 42 |         """
 43 |         Configures this generator. Available ``args`` are:
 44 |             - ``source``: source file or directory path
 45 |             Available ``kwargs`` are:
 46 |             - ``destination_file``: path to html destination file
 47 |             - ``direct``: enables direct rendering presentation to stdout
 48 |             - ``debug``: enables debug mode
 49 |             - ``embed``: generates a standalone document, with embedded assets
 50 |             - ``encoding``: the encoding to use for this presentation
 51 |             - ``extensions``: Comma separated list of markdown extensions
 52 |             - ``logger``: a logger lambda to use for logging
 53 |             - ``maxtoclevel``: the maximum level to include in toc
 54 |             - ``presenter_notes``: enable presenter notes
 55 |             - ``relative``: enable relative asset urls
 56 |             - ``theme``: path to the theme to use for this presentation
 57 |             - ``theme_mod``: modifications to the default theme
 58 |             - ``verbose``: enables verbose output
 59 |         """
 60 |         self.debug = kwargs.get('debug', False)
 61 |         self.destination_file = kwargs.get('destination_file', 'presentation.html')
 62 |         self.direct = kwargs.get('direct', False)
 63 |         self.embed = kwargs.get('embed', False)
 64 |         self.encoding = kwargs.get('encoding', 'utf8')
 65 |         self.extensions = kwargs.get('extensions', None)
 66 |         self.logger = kwargs.get('logger', None)
 67 |         self.maxtoclevel = kwargs.get('maxtoclevel', 2)
 68 |         self.presenter_notes = kwargs.get('presenter_notes', True)
 69 |         self.relative = kwargs.get('relative', False)
 70 |         self.theme = kwargs.get('theme', 'default')
 71 |         self.theme_mods = list(filter(lambda x: x != '', kwargs.get('theme_mod', '').split(',')))
 72 |         self.verbose = kwargs.get('verbose', False)
 73 |         self.linenos = self.linenos_check(kwargs.get('linenos'))
 74 |         self.watch = kwargs.get('watch', False)
 75 |         self.num_slides = 0
 76 |         self.__toc = []
 77 | 
 78 |         if self.direct:
 79 |             # Only output html in direct output mode, not log messages
 80 |             self.verbose = False
 81 | 
 82 |         if not source or not os.path.exists(source):
 83 |             raise RuntimeError("Source file/directory %s does not exist" % source)
 84 | 
 85 |         if source.endswith('.cfg'):
 86 |             self.work_dir = os.path.dirname(source)
 87 |             config = self.parse_config(source)
 88 |             self.source = config.get('source')
 89 |             if not self.source:
 90 |                 raise RuntimeError('unable to fetch a valid source from config')
 91 |             source_abspath = os.path.abspath(self.source[0])
 92 |             self.destination_file = config.get('destination', self.destination_file)
 93 |             self.embed = config.get('embed', self.embed)
 94 |             self.relative = config.get('relative', self.relative)
 95 |             self.extensions = config.get('extensions', self.extensions)
 96 |             self.maxtoclevel = config.get('max-toc-level', self.maxtoclevel)
 97 |             self.theme = config.get('theme', self.theme)
 98 |             self.theme_mods = config.get('theme_mods', self.theme_mods)
 99 |             self.destination_dir = os.path.dirname(self.destination_file)
100 |             self.user_css = config.get('css', [])
101 |             self.user_js = config.get('js', [])
102 |             self.linenos = self.linenos_check(config.get('linenos', self.linenos))
103 |         else:
104 |             self.source = source
105 |             self.work_dir = '.'
106 |             self.destination_dir = os.path.dirname(self.destination_file)
107 |             self.user_css = []
108 |             self.user_js = []
109 | 
110 |             source_abspath = os.path.abspath(source)
111 | 
112 |         if not os.path.isdir(source_abspath):
113 |             source_abspath = os.path.dirname(source_abspath)
114 | 
115 |         self.watch_dir = source_abspath
116 | 
117 |         if os.path.exists(self.destination_file) and not os.path.isfile(self.destination_file):
118 |             raise RuntimeError("Destination %s exists and is not a file" % self.destination_file)
119 | 
120 |         if os.path.exists(self.theme):
121 |             theme_dir = self.theme
122 |         else:
123 |             theme_dir = os.path.join(THEMES_DIR, self.theme)
124 | 
125 |         if not os.path.exists(theme_dir):
126 |             raise RuntimeError("Theme %r doesn't exist" % self.theme)
127 | 
128 |         self.theme_paths = [
129 |             theme_dir,
130 |             os.path.join(THEMES_DIR, 'default'),
131 |         ]
132 |         self.mod_paths = []
133 |         for mod in self.theme_mods:
134 |             theme_mod_dir = os.path.join(MODS_DIR, mod)
135 |             if not os.path.exists(theme_mod_dir):
136 |                 raise RuntimeError("Theme mod %r doesn't exist" % mod)
137 | 
138 |             self.theme_paths.append(theme_mod_dir)
139 |             self.mod_paths.append(theme_mod_dir)
140 | 
141 |         legacy_template = self.lookup_file('base.html', raise_error=False)
142 |         if legacy_template:
143 |             raise RuntimeError("A pre-v6.0 template file has been found at %r. You need to rename it to template.html and use the new"
144 |                                " css_assets/js_assets variables instead." % legacy_template)
145 |         self.template_file = self.lookup_file('template.html')
146 | 
147 |         # macros registering
148 |         self.macros = []
149 |         self.register_macro(*self.default_macros)
150 | 
151 |     def lookup_file(self, name, raise_error=True):
152 |         for path in self.theme_paths:
153 |             absname = os.path.join(path, name)
154 |             if os.path.exists(absname):
155 |                 return absname
156 |         else:
157 |             if raise_error:
158 |                 raise RuntimeError("Could not find %s in %s" % (name, self.theme_paths))
159 | 
160 |     def process_user_files(self, files):
161 |         if isinstance(files, string_types):
162 |             files = [files]
163 |         for path in files:
164 |             if path.startswith(("http://", "https://")):
165 |                 yield {
166 |                     'path_url': path,
167 |                     'contents': '',
168 |                     'dirname': '',
169 |                     'embeddable': False,
170 |                 }
171 |                 self.log("Loaded:  %s (not embeddable)\n" % path)
172 |             else:
173 |                 path = os.path.normpath(os.path.join(self.work_dir, path))
174 |                 if not os.path.exists(path):
175 |                     raise RuntimeError('%s user file not found' % (path,))
176 |                 with codecs.open(path, encoding=self.encoding) as fh:
177 |                     yield {
178 |                         'path_url': utils.get_path_url(path, self.relative and self.destination_dir),
179 |                         'dirname': os.path.dirname(path) or '.',
180 |                         'contents': fh.read(),
181 |                         'embeddable': True,
182 |                     }
183 |                     self.log("Loaded:  %s\n" % path)
184 | 
185 |     def add_toc_entry(self, title, level, slide_number):
186 |         """
187 |         Adds a new entry to current presentation Table of Contents.
188 |         """
189 |         self.__toc.append({'title': title, 'number': slide_number,
190 |                            'level': level})
191 | 
192 |     @property
193 |     def toc(self):
194 |         """
195 |         Smart getter for Table of Content list.
196 |         """
197 |         toc = []
198 |         stack = [toc]
199 |         for entry in self.__toc:
200 |             entry['sub'] = []
201 |             while entry['level'] < len(stack):
202 |                 stack.pop()
203 |             while entry['level'] > len(stack):
204 |                 stack.append(stack[-1][-1]['sub'])
205 |             stack[-1].append(entry)
206 |         return toc
207 | 
208 |     def execute(self):
209 |         """
210 |         Execute this generator regarding its current configuration.
211 |         """
212 |         if self.direct:
213 |             out = getattr(sys.stdout, 'buffer', sys.stdout)
214 |             out.write(self.render().encode(self.encoding))
215 |         else:
216 |             self.write_and_log()
217 | 
218 |             if self.watch:
219 |                 from .watcher import watch
220 | 
221 |                 self.log(u"Watching %s\n" % self.watch_dir)
222 | 
223 |                 watch(self.watch_dir, self.write_and_log)
224 | 
225 |     def write_and_log(self):
226 |         self.num_slides = 0
227 |         self.__toc = []
228 |         self.write()
229 |         self.log(u"Generated file: %s" % self.destination_file)
230 | 
231 |     def fetch_contents(self, source, work_dir):
232 |         """
233 |         Recursively fetches contents from a single file or
234 |         directory containing itself Markdown/RST files.
235 |         """
236 |         slides = []
237 | 
238 |         if type(source) is list:
239 |             for entry in source:
240 |                 slides.extend(self.fetch_contents(entry, work_dir))
241 |         else:
242 |             source = os.path.normpath(os.path.join(work_dir, source))
243 |             if os.path.isdir(source):
244 |                 self.log(u"Entering %r" % source)
245 |                 entries = os.listdir(source)
246 |                 entries.sort()
247 |                 for entry in entries:
248 |                     slides.extend(self.fetch_contents(entry, source))
249 |             else:
250 |                 try:
251 |                     parser = Parser(os.path.splitext(source)[1], self.encoding, self.extensions)
252 |                 except NotImplementedError as exc:
253 |                     self.log(u"Failed   %r: %r" % (source, exc))
254 |                     return slides
255 | 
256 |                 self.log(u"Adding   %r (%s)" % (source, parser.format))
257 | 
258 |                 try:
259 |                     with codecs.open(source, encoding=self.encoding) as file:
260 |                         file_contents = file.read()
261 |                 except UnicodeDecodeError:
262 |                     self.log(u"Unable to decode source %r: skipping" % source,
263 |                              'warning')
264 |                 else:
265 |                     inner_slides = re.split(r'', parser.parse(file_contents))
266 |                     for inner_slide in inner_slides:
267 |                         slides.append(self.get_slide_vars(inner_slide, source))
268 | 
269 |         if not slides:
270 |             self.log(u"Exiting  %r: no contents found" % source, 'notice')
271 | 
272 |         return slides
273 | 
274 |     def read_asset(self, path):
275 |         """
276 |         Load a CSS file from the given path.
277 |         """
278 |         with codecs.open(path, encoding=self.encoding) as asset:
279 |             return {
280 |                 'path_url': utils.get_path_url(path, self.relative and self.destination_dir),
281 |                 'contents': asset.read(),
282 |                 'dirname': os.path.dirname(path),
283 |                 'embeddable': True
284 |             }
285 | 
286 |     @cached_property
287 |     def css_assets(self):
288 |         """ Fetches and returns stylesheet file path or contents, for both
289 |             print and screen contexts, depending if we want a standalone
290 |             presentation or not.
291 |         """
292 |         css = [
293 |             self.read_asset(self.lookup_file(os.path.join('css', 'base.css'))),
294 |             self.read_asset(self.lookup_file(os.path.join('css', 'print.css'))),
295 |             self.read_asset(self.lookup_file(os.path.join('css', 'screen.css'))),
296 |             self.read_asset(self.lookup_file(os.path.join('css', 'theme.css'))),
297 |         ]
298 |         for mod_path in self.mod_paths:
299 |             css.append(self.read_asset(os.path.join(mod_path, 'css', 'mod.css')))
300 |         css.extend(self.process_user_files(self.user_css))
301 |         return css
302 | 
303 |     @cached_property
304 |     def js_assets(self):
305 |         """
306 |         Fetches and returns javascript file path or contents, depending if
307 |         we want a standalone presentation or not.
308 |         """
309 |         js = [self.read_asset(self.lookup_file(os.path.join('js', 'slides.js')))]
310 |         js.extend(self.process_user_files(self.user_js))
311 |         return js
312 | 
313 |     def get_slide_vars(self, slide_src, source,
314 |                        _presenter_notes_re=re.compile(r']*>presenter notes',
315 |                                                       re.DOTALL | re.UNICODE | re.IGNORECASE),
316 |                        _slide_title_re=re.compile(r'((.+?))\s?(.+)?', re.DOTALL | re.UNICODE)):
317 |         """
318 |         Computes a single slide template vars from its html source code.
319 |         Also extracts slide information for the table of contents.
320 |         """
321 |         presenter_notes = ''
322 | 
323 |         find = _presenter_notes_re.search(slide_src)
324 | 
325 |         if find:
326 |             if self.presenter_notes:
327 |                 presenter_notes = slide_src[find.end():].strip()
328 | 
329 |             slide_src = slide_src[:find.start()]
330 | 
331 |         find = _slide_title_re.search(slide_src)
332 | 
333 |         if not find:
334 |             header = level = title = None
335 |             content = slide_src.strip()
336 |         else:
337 |             header = find.group(1)
338 |             level = int(find.group(2))
339 |             title = find.group(3)
340 |             content = find.group(4).strip() if find.group(4) else find.group(4)
341 | 
342 |         slide_classes = []
343 |         context = {}
344 | 
345 |         if header:
346 |             header, _ = self.process_macros(header, source, context)
347 | 
348 |         if content:
349 |             content, slide_classes = self.process_macros(content, source, context)
350 | 
351 |         source_dict = {}
352 | 
353 |         if source:
354 |             source_dict = {
355 |                 'rel_path': source.decode(sys.getfilesystemencoding(), 'ignore') if isinstance(source,
356 |                                                                                                binary_type) else source,
357 |                 'abs_path': os.path.abspath(source)
358 |             }
359 | 
360 |         if header or content:
361 |             context.update(
362 |                 content=content,
363 |                 classes=slide_classes,
364 |                 header=header,
365 |                 level=level,
366 |                 source=source_dict,
367 |                 title=title,
368 |             )
369 |             context.setdefault('presenter_notes', '')
370 |             context['presenter_notes'] += presenter_notes
371 |             if not context['presenter_notes']:
372 |                 context['presenter_notes'] = None
373 |             return context
374 | 
375 |     def get_template_vars(self, slides):
376 |         """
377 |         Computes template vars from slides html source code.
378 |         """
379 |         try:
380 |             head_title = slides[0]['title']
381 |         except (IndexError, TypeError):
382 |             head_title = "Untitled Presentation"
383 | 
384 |         for slide_index, slide_vars in enumerate(slides):
385 |             if not slide_vars:
386 |                 continue
387 |             self.num_slides += 1
388 |             slide_number = slide_vars['number'] = self.num_slides
389 |             if slide_vars['level'] and slide_vars['level'] <= self.maxtoclevel:
390 |                 # only show slides that have a title and lever is not too deep
391 |                 self.add_toc_entry(slide_vars['title'], slide_vars['level'], slide_number)
392 | 
393 |         return {
394 |             'head_title': head_title,
395 |             'num_slides': str(self.num_slides),
396 |             'slides': slides,
397 |             'toc': self.toc,
398 |             'embed': self.embed,
399 |             'css_assets': self.css_assets,
400 |             'js_assets': self.js_assets,
401 |             'version': __version__
402 |         }
403 | 
404 |     def linenos_check(self, value):
405 |         """
406 |         Checks and returns a valid value for the ``linenos`` option.
407 |         """
408 |         return value if value in VALID_LINENOS else 'inline'
409 | 
410 |     def log(self, message, type='notice'):
411 |         """
412 |         Logs a message (eventually, override to do something more clever).
413 |         """
414 |         if self.logger and not callable(self.logger):
415 |             raise ValueError(u"Invalid logger set, must be a callable")
416 |         if self.verbose and self.logger:
417 |             self.logger(message, type)
418 | 
419 |     def parse_config(self, config_source):
420 |         """
421 |         Parses a landslide configuration file and returns a normalized
422 |         python dict.
423 |         """
424 |         self.log(u"Reading config: %s" % config_source)
425 |         try:
426 |             raw_config = configparser.RawConfigParser()
427 |             raw_config.read(config_source)
428 |         except Exception as e:
429 |             raise RuntimeError(u"Invalid configuration file: %s" % e)
430 |         section_name = 'landslide' if raw_config.has_section('landslide') else 'darkslide'
431 |         config = {
432 |             'source': raw_config.get(section_name, 'source').replace('\r', '').split('\n')
433 |         }
434 |         if raw_config.has_option(section_name, 'theme'):
435 |             config['theme'] = raw_config.get(section_name, 'theme')
436 |             self.log(u"Using configured theme: %s" % config['theme'])
437 |         if raw_config.has_option(section_name, 'destination'):
438 |             config['destination'] = raw_config.get(section_name, 'destination')
439 |         if raw_config.has_option(section_name, 'linenos'):
440 |             config['linenos'] = raw_config.get(section_name, 'linenos')
441 |         if raw_config.has_option(section_name, 'max-toc-level'):
442 |             config['max-toc-level'] = int(raw_config.get(section_name, 'max-toc-level'))
443 |         for boolopt in ('embed', 'relative'):
444 |             if raw_config.has_option(section_name, boolopt):
445 |                 config[boolopt] = raw_config.getboolean(section_name, boolopt)
446 |         if raw_config.has_option(section_name, 'extensions'):
447 |             config['extensions'] = ",".join(raw_config.get(section_name, 'extensions').replace('\r', '').split('\n'))
448 |         for listopt in ('theme_mods', 'css', 'js'):
449 |             if raw_config.has_option(section_name, listopt):
450 |                 config[listopt] = raw_config.get(section_name, listopt).replace('\r', '').split('\n')
451 |         return config
452 | 
453 |     def process_macros(self, content, source, context):
454 |         """
455 |         Process all macros.
456 |         """
457 |         classes = []
458 |         for macro in self.macros:
459 |             content, add_classes = macro.process(content, source, context)
460 |             if add_classes:
461 |                 classes += add_classes
462 |         return content, classes
463 | 
464 |     def register_macro(self, *macros):
465 |         """
466 |         Registers macro classes passed a method arguments.
467 |         """
468 |         macro_options = {'relative': self.relative, 'linenos': self.linenos, 'destination_dir': self.destination_dir}
469 |         for m in macros:
470 |             if inspect.isclass(m) and issubclass(m, macro_module.Macro):
471 |                 self.macros.append(m(logger=self.logger, embed=self.embed, options=macro_options))
472 |             else:
473 |                 raise TypeError("Couldn't register macro; a macro must inherit"
474 |                                 " from macro.Macro")
475 | 
476 |     def embed_url_data(self, context, html):
477 |         """
478 |         Find all image and fonts referenced in CSS with an ``url()`` function
479 |         and embed them in base64. Images from the user (i.e. included in its
480 |         source code, *not* in its CSS) are embedded by the macro `EmbedImagesMacro`.
481 |         """
482 |         all_urls = re.findall(r'url\([\"\']?(.*?)[\"\']?\)', html,
483 |                               re.DOTALL | re.UNICODE)
484 |         embed_exts = ('.jpg', '.jpeg', '.png', '.gif', '.svg', '.woff2',
485 |                       '.woff')
486 |         embed_urls = (url for url in all_urls if url.endswith(embed_exts))
487 | 
488 |         css_dirs = [asset['dirname'] for asset in self.css_assets]
489 | 
490 |         for embed_url in embed_urls:
491 |             embed_url = embed_url.replace('"', '').replace("'", '')
492 | 
493 |             directory, encoded_url = None, None
494 |             for directory in css_dirs:
495 |                 encoded_url = utils.encode_data_from_url(embed_url, directory)
496 |                 if encoded_url:
497 |                     break
498 | 
499 |             if encoded_url:
500 |                 html = html.replace(embed_url, encoded_url, 1)
501 |                 self.log("Embedded theme file %s from directory %s"
502 |                          % (embed_url, directory))
503 |             else:
504 |                 self.log(u"Failed to embed theme file %s" % embed_url)
505 | 
506 |         return html
507 | 
508 |     def render(self):
509 |         """
510 |         Returns generated html code.
511 |         """
512 |         with codecs.open(self.template_file, encoding=self.encoding) as template_src:
513 |             template = jinja2.Template(template_src.read())
514 |         slides = self.fetch_contents(self.source, self.work_dir)
515 |         context = self.get_template_vars(slides)
516 | 
517 |         html = template.render(context)
518 | 
519 |         if self.embed:
520 |             html = self.embed_url_data(context, html)
521 | 
522 |         return html
523 | 
524 |     def write(self):
525 |         """
526 |         Writes generated presentation code into the destination file.
527 |         """
528 |         html = self.render()
529 |         dirname = os.path.dirname(self.destination_file)
530 |         if dirname and not os.path.exists(dirname):
531 |             os.makedirs(dirname)
532 |         with codecs.open(self.destination_file, 'w',
533 |                          encoding='utf_8') as outfile:
534 |             outfile.write(html)
535 | 


--------------------------------------------------------------------------------
/src/darkslide/macro.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | import os
  3 | import re
  4 | import sys
  5 | 
  6 | import pygments
  7 | import qrcode
  8 | from pygments.formatters import HtmlFormatter
  9 | from pygments.lexers import get_lexer_by_name
 10 | from qrcode.image.svg import SvgPathImage
 11 | from six.moves import html_entities
 12 | 
 13 | from . import utils
 14 | 
 15 | try:
 16 |     from io import BytesIO as StringIO
 17 | except ImportError:
 18 |     from StringIO import StringIO
 19 | 
 20 | 
 21 | class Macro(object):
 22 |     """Base class for altering slide HTML during presentation generation"""
 23 | 
 24 |     def __init__(self, logger=sys.stdout.write, embed=False, options=None):
 25 |         self.logger = logger
 26 |         self.embed = embed
 27 |         if options:
 28 |             if not isinstance(options, dict):
 29 |                 raise ValueError(u'Macro options must be a dict instance')
 30 |             self.options = options
 31 |         else:
 32 |             self.options = {}
 33 | 
 34 |     def process(self, content, source=None, context=None):
 35 |         """Generic processor (does actually nothing)"""
 36 |         return content, []
 37 | 
 38 | 
 39 | class CodeHighlightingMacro(Macro):
 40 |     """Performs syntax coloration in slide code blocks using Pygments"""
 41 | 
 42 |     macro_re = re.compile(
 43 |         r'(()?\s?!(\S+?)\n(.*?)()?
)', 44 | re.UNICODE | re.MULTILINE | re.DOTALL) 45 | 46 | html_entity_re = re.compile(r'&(\w+?);') 47 | 48 | def descape(self, string, defs=None): 49 | """Decodes html entities from a given string""" 50 | if defs is None: 51 | defs = html_entities.entitydefs 52 | 53 | def replacer(m): 54 | if len(m.groups()) > 0: 55 | return defs[m.group(1)] 56 | else: 57 | return m.group(0) 58 | 59 | return self.html_entity_re.sub(replacer, string) 60 | 61 | def process(self, content, source=None, context=None): 62 | code_blocks = self.macro_re.findall(content) 63 | if not code_blocks: 64 | return content, [] 65 | 66 | classes = [] 67 | for block, void1, lang, code, void2 in code_blocks: 68 | try: 69 | lexer = get_lexer_by_name(lang, startinline=True) 70 | except Exception: 71 | self.logger(u"Unknown pygment lexer \"%s\", skipping" 72 | % lang, 'warning') 73 | return content, classes 74 | 75 | if 'linenos' not in self.options or self.options['linenos'] == 'no': 76 | self.options['linenos'] = False 77 | 78 | formatter = HtmlFormatter(linenos=self.options['linenos'], 79 | nobackground=True) 80 | pretty_code = pygments.highlight(self.descape(code), lexer, 81 | formatter) 82 | content = content.replace(block, pretty_code, 1) 83 | 84 | return content, [u'has_code'] 85 | 86 | 87 | class EmbedImagesMacro(Macro): 88 | """Encodes images in base64 for embedding in image:data""" 89 | macro_re = re.compile( 90 | r'|]+?data="(.*?)"[^<>]+?type="image/svg\+xml"', 91 | re.DOTALL | re.UNICODE) 92 | 93 | def process(self, content, source=None, context=None): 94 | classes = [] 95 | 96 | if not self.embed: 97 | return content, classes 98 | 99 | images = self.macro_re.findall(content) 100 | 101 | source_dir = os.path.dirname(source) 102 | 103 | for image_url, data_url in images: 104 | encoded_url = utils.encode_data_from_url(image_url or data_url, source_dir) 105 | 106 | if not encoded_url: 107 | self.logger(u"Failed to embed image \"%s\"" % image_url, 'warning') 108 | return content, classes 109 | 110 | if image_url: 111 | content = content.replace(u"src=\"" + image_url, 112 | u"src=\"" + encoded_url, 1) 113 | else: 114 | content = content.replace(u"data=\"" + data_url, 115 | u"data=\"" + encoded_url, 1) 116 | 117 | self.logger(u"Embedded image %r" % (image_url or data_url), 'notice') 118 | 119 | return content, classes 120 | 121 | 122 | class FixImagePathsMacro(Macro): 123 | """Replaces html image paths with fully qualified absolute urls""" 124 | 125 | macro_re = re.compile( 126 | r']+?data="(?!http://)(.*?)"[^<>]+?type="image/svg\+xml"', 128 | re.DOTALL | re.UNICODE 129 | ) 130 | 131 | def process(self, content, source=None, context=None): 132 | classes = [] 133 | 134 | if self.embed: 135 | return content, classes 136 | 137 | base_path = utils.get_path_url(source, self.options['relative'] and self.options['destination_dir']) 138 | base_url = os.path.split(base_path)[0] 139 | 140 | images = self.macro_re.findall(content) 141 | 142 | for matches in images: 143 | for image in matches: 144 | if image: 145 | full_path = '"%s"' % os.path.join(base_url, image) 146 | image = '"%s"' % image 147 | content = content.replace(image, full_path) 148 | 149 | return content, classes 150 | 151 | 152 | class FxMacro(Macro): 153 | """Adds custom CSS class to slides""" 154 | macro_re = re.compile(r'(

\.fx:\s?(.*?)

\n?)', 155 | re.DOTALL | re.UNICODE) 156 | 157 | def process(self, content, source=None, context=None): 158 | classes = [] 159 | 160 | fx_match = self.macro_re.search(content) 161 | if fx_match: 162 | classes = fx_match.group(2).split(u' ') 163 | content = content.replace(fx_match.group(1), '', 1) 164 | 165 | return content, classes 166 | 167 | 168 | class NotesMacro(Macro): 169 | """Adds toggleable notes to slides""" 170 | macro_re = re.compile(r'

\.notes:\s?(.*?)

') 171 | 172 | def process(self, content, source=None, context=None): 173 | notes = [] 174 | 175 | def repl(match): 176 | notes.append("

%s

" % match.group(1)) 177 | return '' 178 | new_content = self.macro_re.sub(repl, content) 179 | 180 | if context is not None: 181 | context['presenter_notes'] = ''.join(notes) 182 | 183 | return new_content, [] 184 | 185 | 186 | class QRMacro(Macro): 187 | """Generates a QR code in a slide""" 188 | macro_re = re.compile(r'

\.qr:\s?(.*?)

') 189 | 190 | def process(self, content, source=None, context=None): 191 | classes = [] 192 | 193 | def encoder(match): 194 | qr = qrcode.QRCode(1, error_correction=qrcode.ERROR_CORRECT_L, box_size=40) 195 | qr.add_data(match.group(1)) 196 | buff = StringIO() 197 | qr.make_image(image_factory=SvgPathImage).save(buff) 198 | return '

%s

' % buff.getvalue().decode('utf-8') 199 | 200 | new_content = self.macro_re.sub(encoder, content) 201 | 202 | if content != new_content: 203 | classes.append(u'has_qr') 204 | 205 | return new_content, classes 206 | 207 | 208 | class FooterMacro(Macro): 209 | """Add footer in slides""" 210 | footer = '' 211 | macro_re = re.compile(r'

\.footer:\s?(.*?)

') 212 | 213 | def process(self, content, source=None, context=None): 214 | classes = [] 215 | 216 | def save(match): 217 | self.footer = match.group(1) 218 | return '' 219 | 220 | content = self.macro_re.sub(save, content) 221 | 222 | if self.footer: 223 | classes.append(u'has_footer') 224 | context['footer'] = self.footer 225 | 226 | return content, classes 227 | -------------------------------------------------------------------------------- /src/darkslide/mods/showpagenums/README.md: -------------------------------------------------------------------------------- 1 | # Darkslide `showpagenums` mod 2 | 3 | The purpose of this darkslide mod is to automatically show the page 4 | number on each slide by default. (The page numbers can still be 5 | toggled on and off by hitting `n` during the presentation) 6 | -------------------------------------------------------------------------------- /src/darkslide/mods/showpagenums/css/mod.css: -------------------------------------------------------------------------------- 1 | aside.page_number { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /src/darkslide/mods/wide16x9/css/mod.css: -------------------------------------------------------------------------------- 1 | .slide { 2 | width: 1333px; 3 | } 4 | .slide header:only-child h1 { 5 | width: 1333px; 6 | } 7 | @media screen { 8 | .slide { 9 | margin-left: -667px; 10 | } 11 | .slide.next { 12 | margin-left: 677px; 13 | } 14 | .slide.next_2 { 15 | margin-left: 1368px; 16 | } 17 | .slide.next_1 { 18 | margin-left: 2020px; 19 | } 20 | .show_next .slide.prev { 21 | margin-left: -2681px; 22 | } 23 | .show_next .slide.current { 24 | margin-left: -1338px; 25 | } 26 | .show_next .slide.next { 27 | margin-left: 5px; 28 | } 29 | .show_next .slide.next_1 { 30 | margin-left: 1348px; 31 | } 32 | .presenter_view .slide.prev { 33 | margin-left: -1348px; 34 | } 35 | .presenter_view .slide.current { 36 | margin-left: -667px; 37 | } 38 | .presenter_view .slide.next_1 { 39 | margin-left: 692px; 40 | } 41 | .expose .slide-wrapper { 42 | width: 414px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/darkslide/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | SUPPORTED_FORMATS = { 5 | 'markdown': ['.mdown', '.markdown', '.markdn', '.md', '.mdn', '.mdwn'], 6 | 'restructuredtext': ['.rst', '.rest'], 7 | 'textile': ['.textile'], 8 | } 9 | 10 | 11 | class Parser(object): 12 | """This class generates the HTML code depending on which syntax is used in 13 | the souce document. 14 | 15 | The Parser currently supports both Markdown and restructuredText 16 | syntaxes. 17 | """ 18 | RST_REPLACEMENTS = [ 19 | (r'', r'', re.UNICODE), 20 | (r'
', r'', re.UNICODE), 21 | (r'

.*?

', r'', re.UNICODE), 22 | (r'Document or section may not begin with a transition\.', 23 | r'', re.UNICODE), 24 | (r'', r'', re.DOTALL | re.UNICODE), 25 | (r'\n', r'
\n', re.DOTALL | re.UNICODE), 26 | (r'(.+)<\/a>', r'\1', re.UNICODE), 27 | ] 28 | 29 | md_extensions = '' 30 | 31 | def __init__(self, extension, encoding='utf8', md_extensions=''): 32 | """Configures this parser. 33 | """ 34 | self.encoding = encoding 35 | self.format = None 36 | 37 | for supp_format, supp_extensions in SUPPORTED_FORMATS.items(): 38 | for supp_extension in supp_extensions: 39 | if supp_extension == extension: 40 | self.format = supp_format 41 | 42 | if not self.format: 43 | raise NotImplementedError(u"Unsupported format %s" % extension) 44 | 45 | if md_extensions: 46 | exts = (value.strip() for value in md_extensions.split(',')) 47 | self.md_extensions = filter(None, exts) 48 | 49 | def parse(self, text): 50 | """Parses and renders a text as HTML regarding current format. 51 | """ 52 | if self.format == 'markdown': 53 | try: 54 | import markdown 55 | except ImportError: 56 | raise RuntimeError(u"Looks like markdown is not installed") 57 | 58 | if text.startswith(u'\ufeff'): # check for unicode BOM 59 | text = text[1:] 60 | 61 | return markdown.markdown(text, extensions=self.md_extensions) 62 | elif self.format == 'restructuredtext': 63 | try: 64 | from .rst import html_body 65 | except ImportError: 66 | raise RuntimeError(u"Looks like docutils are not installed") 67 | 68 | html = html_body(text, input_encoding=self.encoding) 69 | 70 | # RST generates pretty much markup to be removed in our case 71 | for (pattern, replacement, mode) in self.RST_REPLACEMENTS: 72 | html = re.sub(re.compile(pattern, mode), replacement, html, 0) 73 | 74 | return html.strip() 75 | elif self.format == 'textile': 76 | try: 77 | import textile 78 | except ImportError: 79 | raise RuntimeError(u"Looks like textile is not installed") 80 | 81 | text = text.replace('\n---\n', '\n
\n') 82 | 83 | return textile.textile(text) 84 | else: 85 | raise NotImplementedError(u"Unsupported format %s, cannot parse" 86 | % self.format) 87 | -------------------------------------------------------------------------------- /src/darkslide/rst.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from docutils import core 3 | from docutils import nodes 4 | from docutils.parsers.rst import Directive 5 | from docutils.parsers.rst import directives 6 | from pygments import highlight 7 | from pygments.formatters import HtmlFormatter 8 | from pygments.lexers import TextLexer 9 | from pygments.lexers import get_lexer_by_name 10 | 11 | 12 | class Pygments(Directive): 13 | """ Source code syntax hightlighting for ReST syntax.""" 14 | required_arguments = 1 15 | optional_arguments = 0 16 | final_argument_whitespace = True 17 | option_spec = { 18 | 'linenos': directives.flag, 19 | 'emphasize-lines': directives.unchanged_required, 20 | } 21 | has_content = True 22 | 23 | def run(self): 24 | self.assert_has_content() 25 | try: 26 | lexer = get_lexer_by_name(self.arguments[0]) 27 | except ValueError: 28 | # no lexer found - use the text one instead of an exception 29 | lexer = TextLexer() 30 | args = {'noclasses': False} 31 | if 'linenos' in self.options: 32 | args['linenos'] = 'table' 33 | if 'emphasize-lines' in self.options: 34 | args['hl_lines'] = self.options['emphasize-lines'].split(',') 35 | formatter = HtmlFormatter(**args) 36 | parsed = highlight(u'\n'.join(self.content), lexer, formatter) 37 | return [nodes.raw('', parsed, format='html')] 38 | 39 | 40 | directives.register_directive('sourcecode', Pygments) 41 | directives.register_directive('code-block', Pygments) 42 | 43 | 44 | def html_parts(input_string, source_path=None, destination_path=None, 45 | input_encoding='unicode', doctitle=1, initial_header_level=1): 46 | """ 47 | Given an input string, returns a dictionary of HTML document parts. 48 | 49 | Dictionary keys are the names of parts, and values are Unicode strings; 50 | encoding is up to the client. 51 | 52 | Parameters: 53 | 54 | - `input_string`: A multi-line text string; required. 55 | - `source_path`: Path to the source file or object. Optional, but useful 56 | for diagnostic output (system messages). 57 | - `destination_path`: Path to the file or object which will receive the 58 | output; optional. Used for determining relative paths (stylesheets, 59 | source links, etc.). 60 | - `input_encoding`: The encoding of `input_string`. If it is an encoded 61 | 8-bit string, provide the correct encoding. If it is a Unicode string, 62 | use "unicode", the default. 63 | - `doctitle`: Disable the promotion of a lone top-level section title to 64 | document title (and subsequent section title to document subtitle 65 | promotion); enabled by default. 66 | - `initial_header_level`: The initial level for header elements (e.g. 1 67 | for "

"). 68 | """ 69 | overrides = { 70 | 'input_encoding': input_encoding, 71 | 'doctitle_xform': doctitle, 72 | 'initial_header_level': initial_header_level, 73 | 'report_level': 5 74 | } 75 | 76 | parts = core.publish_parts( 77 | source=input_string, source_path=source_path, 78 | destination_path=destination_path, 79 | writer_name='html', settings_overrides=overrides) 80 | return parts 81 | 82 | 83 | def html_body(input_string, source_path=None, destination_path=None, 84 | input_encoding='unicode', doctitle=1, initial_header_level=1): 85 | """ 86 | Given an input string, returns an HTML fragment as a string. 87 | 88 | The return value is the contents of the element. 89 | 90 | Parameters (see `html_parts()` for the remainder): 91 | 92 | - `output_encoding`: The desired encoding of the output. If a Unicode 93 | string is desired, use the default value of "unicode" . 94 | """ 95 | parts = html_parts( 96 | input_string=input_string, source_path=source_path, 97 | destination_path=destination_path, 98 | input_encoding=input_encoding, doctitle=doctitle, 99 | initial_header_level=initial_header_level) 100 | fragment = parts['html_body'] 101 | return fragment 102 | -------------------------------------------------------------------------------- /src/darkslide/themes/default/css/base.css: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4, h5, h6, 2 | p, body, header { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | .slide { 8 | position: relative; 9 | overflow: hidden; 10 | width: 1000px; 11 | height: 750px; 12 | border-radius: 20px; 13 | -moz-border-radius: 20px; 14 | -webkit-border-radius: 20px; 15 | } 16 | 17 | p.hr { 18 | height: 0px; 19 | font-size: 0px !important; 20 | display: block; 21 | border-bottom: 1px solid #fdf6e3; 22 | } 23 | header:not(:only-child) { 24 | font-size: 58px; 25 | position: absolute; 26 | left: 30px; 27 | top: 25px; 28 | margin: 0; 29 | padding: 0; 30 | } 31 | header h1, header h2, header h3, header h4, header h5, header h6 { 32 | display: inline; 33 | font-size: 110%; 34 | font-weight: bold; 35 | padding: 0; 36 | margin: 0; 37 | } 38 | header h2:first-child { 39 | margin-top: 0; 40 | } 41 | footer { 42 | position: absolute; 43 | bottom: 0; 44 | left: 0; 45 | right: 0; 46 | padding: 5px; 47 | text-align: center; 48 | } 49 | section, .slide header:only-child h1 { 50 | margin-left: 30px; 51 | margin-right: 30px; 52 | margin-top: 100px; 53 | display: block; 54 | overflow: hidden; 55 | } 56 | section { 57 | font-size: 32px; 58 | position: absolute; 59 | margin: 0; 60 | left: 30px; 61 | right: 30px; 62 | top: 110px; 63 | bottom: 30px; 64 | overflow: auto; 65 | display: flex; 66 | flex-direction: column; 67 | justify-content: space-around; 68 | } 69 | section > * { 70 | overflow: hidden; 71 | } 72 | section img.align-center { 73 | display: block; 74 | margin-left: auto; 75 | margin-right: auto; 76 | } 77 | section img.align-right { 78 | display: block; 79 | margin-left: auto; 80 | margin-right: 0; 81 | } 82 | section img.align-left { 83 | display: block; 84 | margin-right: auto; 85 | margin-left: 0; 86 | } 87 | a { 88 | text-decoration: none; 89 | line-height: 110%; 90 | } 91 | ul { 92 | margin: 0; 93 | } 94 | pre, code, tt { 95 | font-family: Consolas, 'Bitstream Vera Sans Mono', 'Lucida Console', FreeMono, Courier, monospace; 96 | } 97 | pre, .gist .gist-file .gist-data { 98 | font-size: 22px; 99 | max-height: 485px; 100 | padding: 0 0.5em !important; 101 | margin: 0; 102 | overflow: auto; 103 | } 104 | li { 105 | padding: 10px 0; 106 | } 107 | li pre { 108 | margin: 0; 109 | } 110 | .slide header:only-child h1 { 111 | line-height: 180%; 112 | text-align: center; 113 | display: table-cell; 114 | vertical-align: middle; 115 | height: 750px; 116 | width: 1000px; 117 | font-size: 68px; 118 | margin-top: 100px; 119 | margin-bottom: 100px; 120 | } 121 | aside { 122 | display: none; 123 | } 124 | aside.source { 125 | position: absolute; 126 | bottom: 6px; 127 | left: 10px; 128 | } 129 | aside.page_number { 130 | position: absolute; 131 | bottom: 6px; 132 | right: 10px; 133 | text-indent: 10px; 134 | } 135 | .slide p.notes { 136 | font-size: 90%; 137 | } 138 | img { 139 | display: block; 140 | margin: 0 auto; 141 | } 142 | .center { 143 | align-self: center; 144 | display: inline-block; 145 | } 146 | .large { 147 | font-size: 120%; 148 | } 149 | .huge { 150 | font-size: 150%; 151 | } 152 | .qr svg { 153 | background: white; 154 | max-height: 100%; 155 | } 156 | .qr { 157 | text-align: center; 158 | } 159 | -------------------------------------------------------------------------------- /src/darkslide/themes/default/css/print.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | #toc, 3 | #help, 4 | .slide aside, 5 | .slide .notes, 6 | .presenter_notes, 7 | #current_presenter_notes, 8 | #presenter_note { 9 | display: none; 10 | } 11 | 12 | @page { 13 | margin: 0; 14 | size: 1031px 781px; 15 | } 16 | 17 | .slide, 18 | .slide.current, 19 | .slides, 20 | body, 21 | .presentation { 22 | position: relative; 23 | margin: 0; 24 | box-shadow: none !important; 25 | } 26 | 27 | .inner { 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | right: 0; 32 | bottom: 0; 33 | } 34 | 35 | .slide { 36 | margin: 0 !important; 37 | page-break-after: avoid; 38 | } 39 | 40 | .slide-wrapper { 41 | page-break-after: always; 42 | position: relative; 43 | margin: 0; 44 | padding: 15px; 45 | } 46 | 47 | * { 48 | -webkit-print-color-adjust: exact; 49 | color-adjust: exact; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/darkslide/themes/default/css/screen.css: -------------------------------------------------------------------------------- 1 | @media screen { 2 | html, body { 3 | overflow: hidden; 4 | position: relative; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | .slides .slide { 10 | opacity: 0; 11 | } 12 | 13 | .presenter_view .slide, 14 | .expose .slide, 15 | .slide.current, 16 | .show_next .slide.next { 17 | opacity: 1; 18 | } 19 | 20 | .presentation, 21 | .presenter_view #current_presenter_notes section, 22 | .slides, 23 | .expose .slides, 24 | .presenter_notes { 25 | top: 0; 26 | left: 0; 27 | right: 0; 28 | bottom: 0; 29 | position: absolute; 30 | display: block; 31 | background: inherit; 32 | } 33 | 34 | .slide { 35 | position: absolute; 36 | display: none; 37 | left: 50%; 38 | top: 50%; 39 | margin-top: -375px; 40 | margin-left: -500px; 41 | -webkit-transition: margin 0.5s ease-in, opacity 0.5s ease-in; 42 | -moz-transition: margin 0.5s ease-in, opacity 0.5s ease-in; 43 | -o-transition: margin 0.5s ease-in, opacity 0.5s ease-in; 44 | } 45 | 46 | .slide.current, 47 | .slide.prev, 48 | .slide.next, 49 | .slide.next_1, 50 | .slide.next_2 { 51 | display: block; 52 | } 53 | 54 | .slide.prev_1, 55 | .slide.next_2 { 56 | opacity: 0; 57 | } 58 | 59 | .slide.prev_1 { 60 | margin-left: -1525px; 61 | } 62 | 63 | .slide.prev { 64 | margin-left: -1510px; 65 | } 66 | 67 | .slide.next { 68 | margin-left: 510px; 69 | } 70 | 71 | .slide.next_2 { 72 | margin-left: 1035px; 73 | } 74 | 75 | .slide.next_1 { 76 | margin-left: 1520px; 77 | } 78 | 79 | .show_next .slide.prev { 80 | margin-left: -2015px; 81 | } 82 | 83 | .show_next .slide.current { 84 | margin-left: -1005px; 85 | } 86 | 87 | .show_next .slide.next { 88 | margin-left: 5px; 89 | } 90 | 91 | .show_next .slide.next_1 { 92 | margin-left: 1015px; 93 | } 94 | 95 | /* Content */ 96 | /* render a nice scrollbar in overflowed pre area's */ 97 | ::-webkit-scrollbar-thumb { 98 | background: -webkit-gradient(linear, left bottom, left top, from(#eee), to(#fefefe)); 99 | border: 1px solid #888; 100 | -webkit-border-radius: 1ex; 101 | } 102 | 103 | ::-webkit-scrollbar-corner { 104 | background: #dedede; 105 | } 106 | 107 | ::-webkit-scrollbar { 108 | height: 8px; 109 | width: 8px; 110 | background: #888; 111 | border-radius: 5px; 112 | } 113 | 114 | .sidebar { 115 | background: white; 116 | color: black; 117 | border-right: 5px solid #ccc; 118 | z-index: 9999999; 119 | height: 100%; 120 | overflow: hidden; 121 | top: 0; 122 | position: absolute; 123 | display: block; 124 | margin: 0; 125 | margin-left: -400px; 126 | padding: 10px 16px; 127 | overflow: auto; 128 | -webkit-transition: margin 0.2s ease-in; 129 | -moz-transition: margin 0.2s ease-in; 130 | -o-transition: margin 0.2s ease-in; 131 | } 132 | 133 | .sidebar h2 { 134 | text-shadow: rgba(0, 0, 0, 0.2) 0 2px 5px; 135 | margin: 0 0 16px; 136 | padding: 0; 137 | } 138 | 139 | .sidebar table { 140 | width: 100%; 141 | margin: 0; 142 | padding: 0; 143 | border-collapse: collapse; 144 | } 145 | 146 | .sidebar table caption { 147 | display: none; 148 | } 149 | 150 | .sidebar tr { 151 | margin: 2px 0; 152 | border-bottom: 1px solid #ccc; 153 | } 154 | 155 | .sidebar tr:last-of-type { 156 | border-bottom: none; 157 | } 158 | 159 | .sidebar th { 160 | text-align: left; 161 | font-weight: normal; 162 | max-width: 300px; 163 | overflow: hidden; 164 | } 165 | 166 | .sidebar tr.sub th { 167 | text-indent: 20px; 168 | } 169 | 170 | .sidebar td { 171 | text-align: right; 172 | min-width: 20px; 173 | } 174 | 175 | .sidebar a, .sidebar a:hover { 176 | display: block; 177 | text-decoration: none !important; 178 | text-shadow: none !important; 179 | background: none !important; 180 | border-bottom: none; 181 | padding: 4px 0; 182 | } 183 | 184 | .sidebar tr.active { 185 | background: #ff0; 186 | } 187 | 188 | .notes { 189 | display: none; 190 | padding: 10px; 191 | background: #ccc; 192 | border-radius: 10px; 193 | -moz-border-radius: 10px; 194 | -webkit-border-radius: 10px; 195 | } 196 | 197 | /* Expose */ 198 | .expose .slides { 199 | overflow: auto; 200 | } 201 | 202 | .expose .slide { 203 | display: block; 204 | opacity: 1; 205 | float: left; 206 | position: relative; 207 | left: auto !important; 208 | top: auto !important; 209 | margin: 10px !important; 210 | -webkit-transition: none; 211 | -moz-transition: none; 212 | -o-transition: none; 213 | -moz-transform: scale(.30, .30); 214 | -moz-transform-origin: 0 0; 215 | -webkit-transform: scale(.30, .30); 216 | -webkit-transform-origin: 0 0; 217 | -o-transform: scale(.30, .30); 218 | -o-transform-origin: 0 0; 219 | -webkit-transition: none; 220 | -moz-transition: none; 221 | -o-transition: none; 222 | cursor: pointer; 223 | } 224 | 225 | .expose .slide-wrapper { 226 | float: left; 227 | position: relative; 228 | margin: 2%; 229 | width: 300px; 230 | height: 233px; 231 | } 232 | 233 | .expose .slide footer { 234 | } 235 | 236 | .expose .slide .inner { 237 | } 238 | 239 | .expose .slide.next, 240 | .expose .slide.next_1 { 241 | margin-left: 0; 242 | } 243 | 244 | /* Presenter Mode */ 245 | .presenter_view .slide { 246 | display: inline; 247 | position: absolute; 248 | overflow: hidden; 249 | -moz-transform: scale(.5, .5); 250 | -moz-transform-origin: 0 0; 251 | -webkit-transform: scale(.5, .5); 252 | -webkit-transform-origin: 0 0; 253 | -o-transform: scale(.5, .5); 254 | -o-transform-origin: 0 0; 255 | margin-top: -375px; 256 | } 257 | 258 | .presenter_view .slide.prev { 259 | display: block; 260 | margin-left: -1015px; 261 | } 262 | 263 | .presenter_view .slide.current { 264 | display: block; 265 | margin-left: -500px; 266 | box-shadow: 0 0 0 15px maroon; 267 | z-index: 2; 268 | } 269 | 270 | .presenter_view .slide.next { 271 | display: block; 272 | margin-left: 15px; 273 | z-index: 1; 274 | } 275 | 276 | .presenter_view .slide.next_1 { 277 | display: block; 278 | margin-left: 525px; 279 | } 280 | 281 | .presenter_view .slide.none { 282 | display: none; 283 | } 284 | 285 | .presenter_view #current_presenter_notes { 286 | visibility: visible; 287 | display: block; 288 | position: fixed; 289 | overflow: auto; 290 | vertical-align: middle; 291 | left: 50%; 292 | top: 50%; 293 | bottom: 0; 294 | margin-left: -475px; 295 | margin-top: 20px; 296 | z-index: 2; 297 | width: 950px; 298 | border-radius: 10px; 299 | margin-bottom: 20px; 300 | } 301 | 302 | .presenter_view #current_presenter_notes section { 303 | display: block; 304 | overflow: visible; 305 | margin: 60px 30px 0 30px; 306 | font-size: 22px; 307 | } 308 | 309 | .presenter_view #current_presenter_notes section p { 310 | margin: 0; 311 | } 312 | 313 | .presenter_view #current_presenter_notes h1 { 314 | font-size: 50%; 315 | display: block; 316 | } 317 | 318 | #current_presenter_notes { 319 | display: none; 320 | } 321 | 322 | .slide .presenter_notes { 323 | display: none; 324 | } 325 | 326 | #blank { 327 | position: absolute; 328 | top: 0; 329 | left: 0; 330 | background-color: black; 331 | width: 100%; 332 | height: 100%; 333 | z-index: 64; 334 | display: none; 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/darkslide/themes/default/js/slides.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | // Since we don't have the fallback of attachEvent and 3 | // other IE only stuff we won't try to run JS for IE. 4 | // It will run though when using Google Chrome Frame 5 | if (document.all) { 6 | return; 7 | } 8 | 9 | var currentSlideNo; 10 | var notesOn = false; 11 | var hiddenNext = false; 12 | var blanked = false; 13 | var slides = document.getElementsByClassName('slide'); 14 | var touchStartX = 0; 15 | var spaces = /\s+/, a1 = ['']; 16 | var tocOpened = false; 17 | var helpOpened = false; 18 | var overviewActive = false; 19 | var modifierKeyDown = false; 20 | var scale = 1; 21 | var showingPresenterView = false; 22 | var presenterViewWin = null; 23 | var isPresenterView = false; 24 | 25 | var str2array = function (s) { 26 | if (typeof s == 'string' || s instanceof String) { 27 | if (s.indexOf(' ') < 0) { 28 | a1[0] = s; 29 | return a1; 30 | } else { 31 | return s.split(spaces); 32 | } 33 | } 34 | return s; 35 | }; 36 | 37 | var trim = function (str) { 38 | return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); 39 | }; 40 | 41 | var addClass = function (node, classStr) { 42 | classStr = str2array(classStr); 43 | var cls = ' ' + node.className + ' '; 44 | for (var i = 0, len = classStr.length, c; i < len; ++i) { 45 | c = classStr[i]; 46 | if (c && cls.indexOf(' ' + c + ' ') < 0) { 47 | cls += c + ' '; 48 | } 49 | } 50 | node.className = trim(cls); 51 | }; 52 | 53 | var removeClass = function (node, classStr) { 54 | var cls; 55 | if (!node) { 56 | throw 'no node provided'; 57 | } 58 | if (classStr !== undefined) { 59 | classStr = str2array(classStr); 60 | cls = ' ' + node.className + ' '; 61 | for (var i = 0, len = classStr.length; i < len; ++i) { 62 | cls = cls.replace(' ' + classStr[i] + ' ', ' '); 63 | } 64 | cls = trim(cls); 65 | } else { 66 | cls = ''; 67 | } 68 | if (node.className != cls) { 69 | node.className = cls; 70 | } 71 | }; 72 | 73 | var getSlideEl = function (slideNo) { 74 | if (slideNo > 0) { 75 | return slides[slideNo - 1]; 76 | } else { 77 | return null; 78 | } 79 | }; 80 | 81 | var getSlideTitle = function (slideNo) { 82 | var el = getSlideEl(slideNo); 83 | if (el) { 84 | var headers = el.getElementsByTagName('header'); 85 | if (headers.length > 0) { 86 | return el.getElementsByTagName('header')[0].innerText; 87 | } 88 | } 89 | return null; 90 | }; 91 | 92 | var getSlidePresenterNote = function (slideNo) { 93 | var el = getSlideEl(slideNo); 94 | if (el) { 95 | var n = el.getElementsByClassName('presenter_notes'); 96 | if (n.length > 0) { 97 | return n[0]; 98 | } 99 | } 100 | return null; 101 | }; 102 | 103 | var changeSlideElClass = function (slideNo, className) { 104 | var el = getSlideEl(slideNo); 105 | if (el) { 106 | removeClass(el, 'prev prev_1 current next next_1 next_2 none'); 107 | addClass(el, className); 108 | } 109 | }; 110 | 111 | var updateSlideClasses = function (updateOther) { 112 | window.location.hash = (isPresenterView ? "presenter:" : "slide:") + currentSlideNo; 113 | 114 | for (i = 1; i < currentSlideNo - 1; i++) changeSlideElClass(i, 'none'); 115 | changeSlideElClass(currentSlideNo - 2, 'prev_1'); 116 | changeSlideElClass(currentSlideNo - 1, 'prev'); 117 | changeSlideElClass(currentSlideNo, 'current'); 118 | changeSlideElClass(currentSlideNo + 1, 'next'); 119 | changeSlideElClass(currentSlideNo + 2, 'next_1'); 120 | changeSlideElClass(currentSlideNo + 3, 'next_2'); 121 | for (i = currentSlideNo + 4; i < slides.length + 1; i++) changeSlideElClass(i, 'none'); 122 | 123 | highlightCurrentTocLink(); 124 | 125 | document.getElementsByTagName('title')[0].innerText = getSlideTitle(currentSlideNo); 126 | 127 | updatePresenterNotes(); 128 | 129 | if (updateOther) updateOtherPage(); 130 | if (overviewActive) scrollToCurrent(); 131 | }; 132 | 133 | var updatePresenterNotes = function () { 134 | if (!isPresenterView) { 135 | return; 136 | } 137 | 138 | var existingNote = document.getElementById('current_presenter_notes'); 139 | var currentNote = getSlidePresenterNote(currentSlideNo).cloneNode(true); 140 | currentNote.setAttribute('id', 'presenter_note'); 141 | 142 | existingNote.replaceChild(currentNote, document.getElementById('presenter_note')); 143 | }; 144 | 145 | var highlightCurrentTocLink = function () { 146 | var toc = document.getElementById('toc'); 147 | 148 | if (toc) { 149 | var tocRows = toc.getElementsByTagName('tr'); 150 | for (var i = 0; i < tocRows.length; i++) { 151 | removeClass(tocRows.item(i), 'active'); 152 | } 153 | 154 | var currentTocRow = document.getElementById('toc-row-' + currentSlideNo); 155 | if (currentTocRow) { 156 | addClass(currentTocRow, 'active'); 157 | } 158 | } 159 | }; 160 | 161 | var updateOtherPage = function () { 162 | if (!showingPresenterView) { 163 | return; 164 | } 165 | 166 | var w = isPresenterView ? window.opener : presenterViewWin; 167 | if (w) w.postMessage('slide#' + currentSlideNo, '*'); 168 | }; 169 | 170 | var nextSlide = function () { 171 | if (currentSlideNo < slides.length) { 172 | currentSlideNo++; 173 | } 174 | updateSlideClasses(true); 175 | }; 176 | 177 | var prevSlide = function () { 178 | if (currentSlideNo > 1) { 179 | currentSlideNo--; 180 | } 181 | updateSlideClasses(true); 182 | }; 183 | 184 | var firstSlide = function () { 185 | currentSlideNo = 1; 186 | updateSlideClasses(true); 187 | }; 188 | 189 | var lastSlide = function () { 190 | currentSlideNo = slides.length; 191 | updateSlideClasses(true); 192 | }; 193 | 194 | var showNotes = function () { 195 | var slide = getSlideEl(currentSlideNo), 196 | notes = slide.getElementsByClassName('presenter_notes'); 197 | for (var i = 0, len = notes.length; i < len; i++) { 198 | notes.item(i).style.display = (notesOn) ? 'none' : 'block'; 199 | } 200 | notesOn = !notesOn; 201 | if (notesOn) { 202 | addClass(slide, 'presenter_notes'); 203 | } else { 204 | removeClass(slide, 'presenter_notes'); 205 | } 206 | }; 207 | 208 | var showSlideNumbers = function () { 209 | var asides = document.getElementsByClassName('page_number'); 210 | var hidden = asides[0].style.display != 'block'; 211 | for (var i = 0; i < asides.length; i++) { 212 | asides.item(i).style.display = hidden ? 'block' : 'none'; 213 | } 214 | }; 215 | 216 | var showSlideSources = function () { 217 | var asides = document.getElementsByClassName('source'); 218 | var hidden = asides[0].style.display != 'block'; 219 | for (var i = 0; i < asides.length; i++) { 220 | asides.item(i).style.display = hidden ? 'block' : 'none'; 221 | } 222 | }; 223 | 224 | var showToc = function () { 225 | if (helpOpened) { 226 | showHelp(); 227 | } 228 | var toc = document.getElementById('toc'); 229 | if (toc) { 230 | toc.style.marginLeft = tocOpened ? '-' + (toc.clientWidth + 20) + 'px' : '0px'; 231 | tocOpened = !tocOpened; 232 | } 233 | updateOverview(); 234 | }; 235 | 236 | var showHelp = function () { 237 | if (tocOpened) { 238 | showToc(); 239 | } 240 | 241 | var help = document.getElementById('help'); 242 | 243 | if (help) { 244 | help.style.marginLeft = helpOpened ? '-' + (help.clientWidth + 20) + 'px' : '0px'; 245 | helpOpened = !helpOpened; 246 | } 247 | }; 248 | 249 | var showPresenterView = function () { 250 | if (isPresenterView) { 251 | return; 252 | } 253 | 254 | if (showingPresenterView) { 255 | if (presenterViewWin) 256 | presenterViewWin.close(); 257 | presenterViewWin = null; 258 | showingPresenterView = false; 259 | } else { 260 | presenterViewWin = open(window.location.pathname + "#presenter:" + currentSlideNo, 'presenter_notes', 261 | 'directories=no,location=no,toolbar=no,menubar=no,copyhistory=no'); 262 | showingPresenterView = true; 263 | } 264 | }; 265 | 266 | var toggleOverview = function () { 267 | if (helpOpened) { 268 | showHelp(); 269 | return 270 | } 271 | if (tocOpened) { 272 | showToc(); 273 | return; 274 | } 275 | if (!overviewActive) { 276 | addClass(document.body, 'expose'); 277 | overviewActive = true; 278 | scrollToCurrent(); 279 | } else { 280 | removeClass(document.body, 'expose'); 281 | overviewActive = false; 282 | } 283 | setScale(); 284 | updateOverview(); 285 | }; 286 | var scrollToCurrent = function () { 287 | setTimeout(function () { 288 | document.querySelector('.slide.current').parentElement.scrollIntoView(true); 289 | }); 290 | } 291 | 292 | var updateOverview = function () { 293 | try { 294 | var presentation = document.getElementsByClassName('presentation')[0]; 295 | } catch (e) { 296 | return; 297 | } 298 | 299 | if (isPresenterView) { 300 | var action = overviewActive ? removeClass : addClass; 301 | action(document.body, 'presenter_view'); 302 | } 303 | 304 | var toc = document.getElementById('toc'); 305 | 306 | if (!toc) { 307 | return; 308 | } 309 | 310 | if (!tocOpened || !overviewActive) { 311 | presentation.style.marginLeft = '0px'; 312 | presentation.style.width = '100%'; 313 | } else { 314 | presentation.style.marginLeft = toc.clientWidth + 'px'; 315 | presentation.style.width = (presentation.clientWidth - toc.clientWidth) + 'px'; 316 | } 317 | }; 318 | 319 | var computeScale = function () { 320 | var cSlide = document.getElementsByClassName('current')[0]; 321 | var sx = (cSlide.clientWidth + 20) * (hiddenNext ? 2 : 1) / window.innerWidth; 322 | var sy = (cSlide.clientHeight + 20) / window.innerHeight; 323 | return 1 / Math.max(sy, sx); 324 | }; 325 | 326 | var setScale = function () { 327 | var presentation = document.getElementsByClassName('slides')[0]; 328 | var transform = 'scale(' + (overviewActive ? 1 : computeScale()) + ')'; 329 | presentation.style.MozTransform = transform; 330 | presentation.style.WebkitTransform = transform; 331 | presentation.style.OTransform = transform; 332 | presentation.style.msTransform = transform; 333 | presentation.style.transform = transform; 334 | }; 335 | 336 | var showNext = function () { 337 | var presentation = document.getElementsByClassName('slides')[0]; 338 | addClass(presentation, 'show_next'); 339 | hiddenNext = true; 340 | setScale(); 341 | }; 342 | 343 | var hideNext = function () { 344 | if (isPresenterView) { 345 | return; 346 | } 347 | var presentation = document.getElementsByClassName('slides')[0]; 348 | removeClass(presentation, 'show_next'); 349 | hiddenNext = false; 350 | setScale(); 351 | }; 352 | 353 | var toggleNext = function () { 354 | if (hiddenNext) { 355 | hideNext(); 356 | } else { 357 | showNext(); 358 | } 359 | }; 360 | 361 | var toggleBlank = function () { 362 | blank_elem = document.getElementById('blank'); 363 | 364 | blank_elem.style.display = blanked ? 'none' : 'block'; 365 | 366 | blanked = !blanked; 367 | }; 368 | 369 | var isModifierKey = function (keyCode) { 370 | switch (keyCode) { 371 | case 16: // shift 372 | case 17: // ctrl 373 | case 18: // alt 374 | case 91: // command 375 | return true; 376 | break; 377 | default: 378 | return false; 379 | break; 380 | } 381 | }; 382 | 383 | var checkModifierKeyUp = function (event) { 384 | if (isModifierKey(event.keyCode)) { 385 | modifierKeyDown = false; 386 | } 387 | }; 388 | 389 | var checkModifierKeyDown = function (event) { 390 | if (isModifierKey(event.keyCode)) { 391 | modifierKeyDown = true; 392 | } 393 | }; 394 | 395 | var handleBodyKeyDown = function (event) { 396 | if (modifierKeyDown) { 397 | return 398 | } 399 | switch (event.keyCode) { 400 | case 13: // Enter 401 | if (overviewActive) { 402 | toggleOverview(); 403 | } 404 | break; 405 | case 27: // ESC 406 | toggleOverview(); 407 | break; 408 | case 37: // left arrow 409 | case 38: // arrow up 410 | case 33: // page up 411 | event.preventDefault(); 412 | prevSlide(); 413 | break; 414 | case 39: // right arrow 415 | case 40: // arrow down 416 | case 32: // space 417 | case 34: // page down 418 | event.preventDefault(); 419 | nextSlide(); 420 | break; 421 | case 35: // end 422 | lastSlide(); 423 | break; 424 | case 36: // home 425 | firstSlide(); 426 | break; 427 | case 50: // 2 428 | showNotes(); 429 | break; 430 | case 190: // . 431 | case 48: // 0 432 | case 66: // b 433 | if (!overviewActive) { 434 | toggleBlank(); 435 | } 436 | break; 437 | case 67: // c 438 | if (!overviewActive) { 439 | toggleNext(); 440 | } 441 | break; 442 | case 72: // h 443 | showHelp(); 444 | break; 445 | case 78: // n 446 | if (!overviewActive) { 447 | showSlideNumbers(); 448 | } 449 | break; 450 | case 80: // p 451 | if (!overviewActive) { 452 | showPresenterView(); 453 | } 454 | break; 455 | case 83: // s 456 | if (!overviewActive) { 457 | showSlideSources(); 458 | } 459 | break; 460 | case 84: // t 461 | showToc(); 462 | break; 463 | } 464 | }; 465 | 466 | var handleWheel = function (event) { 467 | if (tocOpened || helpOpened || overviewActive) { 468 | return; 469 | } 470 | 471 | var delta = 0; 472 | 473 | if (!event) { 474 | event = window.event; 475 | } 476 | 477 | if (event.wheelDelta) { 478 | delta = event.wheelDelta / 120; 479 | if (window.opera) delta = -delta; 480 | } else if (event.detail) { 481 | delta = -event.detail / 3; 482 | } 483 | 484 | if (delta && delta < 0) { 485 | nextSlide(); 486 | } else if (delta) { 487 | prevSlide(); 488 | } 489 | }; 490 | 491 | var addSlideClickListeners = function () { 492 | for (var i = 0; i < slides.length; i++) { 493 | var slide = slides.item(i); 494 | slide.num = i + 1; 495 | slide.addEventListener('click', function (e) { 496 | if (overviewActive) { 497 | currentSlideNo = this.num; 498 | toggleOverview(); 499 | updateSlideClasses(true); 500 | e.preventDefault(); 501 | } 502 | return false; 503 | }, true); 504 | } 505 | }; 506 | 507 | var addRemoteWindowControls = function () { 508 | window.addEventListener("message", function (e) { 509 | if (e.data.indexOf("slide#") != -1) { 510 | currentSlideNo = Number(e.data.replace('slide#', '')); 511 | updateSlideClasses(false); 512 | } 513 | }, false); 514 | }; 515 | 516 | var addTouchListeners = function () { 517 | document.addEventListener('touchstart', function (e) { 518 | touchStartX = e.touches[0].pageX; 519 | }, false); 520 | document.addEventListener('touchend', function (e) { 521 | var pixelsMoved = touchStartX - e.changedTouches[0].pageX; 522 | var SWIPE_SIZE = 150; 523 | if (pixelsMoved > SWIPE_SIZE) { 524 | nextSlide(); 525 | } 526 | else if (pixelsMoved < -SWIPE_SIZE) { 527 | prevSlide(); 528 | } 529 | }, false); 530 | }; 531 | 532 | var addTocLinksListeners = function () { 533 | var toc = document.getElementById('toc'); 534 | if (toc) { 535 | var tocLinks = toc.getElementsByTagName('a'); 536 | for (var i = 0; i < tocLinks.length; i++) { 537 | tocLinks.item(i).addEventListener('click', function (e) { 538 | currentSlideNo = Number(this.attributes['href'].value.replace('#slide:', '')); 539 | updateSlideClasses(true); 540 | e.preventDefault(); 541 | }, true); 542 | } 543 | } 544 | }; 545 | // initialize 546 | 547 | (function () { 548 | if (window.location.hash == "") { 549 | currentSlideNo = 1; 550 | } else if (window.location.hash.indexOf("#presenter:") != -1) { 551 | currentSlideNo = Number(window.location.hash.replace('#presenter:', '')); 552 | isPresenterView = true; 553 | showingPresenterView = true; 554 | presenterViewWin = window; 555 | addClass(document.body, 'presenter_view'); 556 | } else { 557 | currentSlideNo = Number(window.location.hash.replace('#slide:', '')) || 1; 558 | } 559 | 560 | document.addEventListener('keyup', checkModifierKeyUp); 561 | document.addEventListener('keydown', handleBodyKeyDown); 562 | document.addEventListener('keydown', checkModifierKeyDown); 563 | document.addEventListener('visibilitychange', function (event) { 564 | if (document.hidden) { 565 | modifierKeyDown = false; 566 | } 567 | }, false); 568 | setInterval(function () { 569 | if (!document.hasFocus()) { 570 | modifierKeyDown = false; 571 | } 572 | }, 100); 573 | 574 | window.addEventListener("mousewheel", handleWheel); 575 | window.addEventListener("DOMMouseScroll", handleWheel); 576 | window.addEventListener("DOMContentLoaded", function () { 577 | setScale(); 578 | hideNext(); 579 | }); 580 | window.onresize = function () { 581 | setScale(); 582 | } 583 | 584 | for (var i = 0, el; el = slides[i]; i++) { 585 | addClass(el, 'slide'); 586 | } 587 | updateSlideClasses(false); 588 | 589 | // add support for finger events (filter it by property detection?) 590 | addTouchListeners(); 591 | addTocLinksListeners(); 592 | addSlideClickListeners(); 593 | addRemoteWindowControls(); 594 | })(); 595 | } 596 | -------------------------------------------------------------------------------- /src/darkslide/themes/default/template.html: -------------------------------------------------------------------------------- 1 | 2 | 24 | 25 | 26 | 27 | 28 | 29 | {{ head_title }} 30 | {% for css in css_assets %} 31 | {% if embed and css.embeddable %} 32 | 33 | {% else %} 34 | 35 | {% endif %} 36 | {% endfor %} 37 | {% for js in js_assets %} 38 | {% if embed %} 39 | 40 | {% else %} 41 | 42 | {% endif %} 43 | {% endfor %} 44 | 45 | 46 |
47 |
90 | {% if toc %} 91 | 127 | {% endif %} 128 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/darkslide/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import mimetypes 4 | import os 5 | 6 | # add woff2 font type: not here by default... 7 | mimetypes.add_type('font/woff2', '.woff2') 8 | 9 | 10 | def get_path_url(path, relative=False): 11 | """ Returns an absolute or relative path url given a path 12 | """ 13 | if relative is False: 14 | return 'file://%s' % os.path.abspath(path) 15 | else: 16 | return os.path.relpath(path, relative) 17 | 18 | 19 | def encode_data_from_url(url, source_path): 20 | if not url or url.startswith('data:') or url.startswith('file://'): 21 | return False 22 | 23 | if url.startswith('http://') or url.startswith('https://'): 24 | return False 25 | 26 | real_path = url if os.path.isabs(url) else os.path.join(source_path, url) 27 | 28 | if not os.path.exists(real_path): 29 | return False 30 | 31 | mime_type, encoding = mimetypes.guess_type(real_path) 32 | 33 | if not mime_type: 34 | return False 35 | 36 | try: 37 | with open(real_path, 'rb') as image_file: 38 | image_contents = image_file.read() 39 | encoded_image = base64.b64encode(image_contents) 40 | except IOError: 41 | return False 42 | 43 | return u"data:%s;base64,%s" % (mime_type, encoded_image.decode()) 44 | 45 | 46 | class cached_property(object): 47 | def __init__(self, func): 48 | self.func = func 49 | 50 | def __get__(self, obj, cls): 51 | if obj is None: 52 | return self 53 | value = obj.__dict__[self.func.__name__] = self.func(obj) 54 | return value 55 | -------------------------------------------------------------------------------- /src/darkslide/watcher.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | try: 5 | from watchdog.events import DirModifiedEvent 6 | from watchdog.events import FileSystemEventHandler 7 | from watchdog.observers import Observer 8 | except ImportError: 9 | print('Error: The watchdog module must be installed to use the -w option') 10 | print('Exiting...') 11 | sys.exit(1) 12 | 13 | 14 | def watch(watch_dir, generate_func): 15 | event_handler = LandslideEventHandler(generate_func) 16 | observer = Observer() 17 | 18 | observer.schedule(event_handler, path=watch_dir, recursive=True) 19 | observer.start() 20 | 21 | try: 22 | while True: 23 | time.sleep(1) 24 | except KeyboardInterrupt: 25 | observer.stop() 26 | 27 | observer.join() 28 | 29 | 30 | class LandslideEventHandler(FileSystemEventHandler): 31 | def __init__(self, generate_func): 32 | super(LandslideEventHandler, self).__init__() 33 | 34 | self.generate_func = generate_func 35 | 36 | def on_modified(self, event): 37 | if isinstance(event, DirModifiedEvent): 38 | self.generate_func() 39 | -------------------------------------------------------------------------------- /tests/test-data/căcăneață.rst: -------------------------------------------------------------------------------- 1 | La țară mere 2 | ============ 3 | 4 | Pam pam. 5 | -------------------------------------------------------------------------------- /tests/test-data/encoding.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionelmc/python-darkslide/f28c9adaf88a67701668c3f6c70ad36d0aee2121/tests/test-data/encoding.rst -------------------------------------------------------------------------------- /tests/test-data/full.cfg: -------------------------------------------------------------------------------- 1 | [darkslide] 2 | source = source1 3 | source2 4 | destination = config_destination 5 | embed = True 6 | relative = True 7 | extensions = ext1 8 | ext2 9 | max-toc-level = 100 10 | theme = abyss 11 | theme_mods = wide16x9 12 | css = config_user_css 13 | js = config_user_js 14 | linenos = table 15 | -------------------------------------------------------------------------------- /tests/test-data/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionelmc/python-darkslide/f28c9adaf88a67701668c3f6c70ad36d0aee2121/tests/test-data/img.png -------------------------------------------------------------------------------- /tests/test-data/test.css: -------------------------------------------------------------------------------- 1 | * {color: red;} 2 | -------------------------------------------------------------------------------- /tests/test-data/test.js: -------------------------------------------------------------------------------- 1 | alert('foo'); 2 | -------------------------------------------------------------------------------- /tests/test-data/test.md: -------------------------------------------------------------------------------- 1 | # Title Slide 2 | 3 | --- 4 | 5 | # Paragraphs 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 8 | tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 9 | scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 10 | elit, vel iaculis lorem facilisis non. 11 | 12 | Vivamus ante mauris, pellentesque et rhoncus ut, condimentum sed ipsum. 13 | Nullam eu molestie sapien. Curabitur imperdiet, ligula id blandit sagittis, 14 | libero quam consectetur felis, ac pharetra metus lectus vitae leo. Mauris 15 | lacinia fermentum augue. 16 | 17 | --- 18 | 19 | # Blockquotes 20 | 21 | > Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ultricies 22 | > tempus ultricies. Ut porta scelerisque viverra. Pellentesque aliquam metus 23 | > scelerisque dui ultricies, auctor dictum erat aliquet. Integer vehicula nunc 24 | > elit, vel iaculis lorem facilisis non. 25 | 26 | --- 27 | 28 | # Subheadings and Emphasis 29 | 30 | ## Italic 31 | 32 | *Lorem ipsum dolor sit amet* 33 | 34 | ## Bold 35 | 36 | **Lorem ipsum dolor sit amet** 37 | 38 | --- 39 | 40 | # Lists 41 | 42 | ## Unordered List 43 | 44 | - Markdown 45 | - ReStructured Text 46 | - Textile 47 | 48 | ## Ordered List 49 | 50 | 1. Python 51 | 2. JavaScript 52 | 3. HTML5 53 | 54 | --- 55 | 56 | # Second Title Slide 57 | 58 | --- 59 | 60 | # Code 61 | 62 | ## Single Word 63 | 64 | Hello `World` 65 | 66 | ## Python 67 | 68 | !python 69 | def multiply (x, y): 70 | return x * y 71 | 72 | ## JavaScript 73 | 74 | !javascript 75 | multiply: function (x, y) { 76 | return x * y; 77 | } 78 | 79 | ## HTML 80 | 81 | !html 82 | 83 | 84 | 85 | 86 | 87 | 88 | --- 89 | 90 | # Images 91 | 92 | ![img](img.png) 93 | 94 | --- 95 | 96 | # View Presenter Notes 97 | 98 | This slide has presenter notes. Press `p` to view them. 99 | 100 | # Presenter Notes 101 | 102 | Hello from presenter notes 103 | 104 | --- 105 | 106 | # Other features 107 | 108 | View other features in the help sidebar by pressing `h` 109 | 110 | --- 111 | 112 | # Unicode 113 | 114 | This is ünicô∂e 115 | -------------------------------------------------------------------------------- /tests/test_landslide.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import codecs 4 | import os 5 | import re 6 | 7 | from pytest import raises 8 | 9 | from darkslide import macro 10 | from darkslide.generator import Generator 11 | from darkslide.parser import Parser 12 | 13 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'test-data') 14 | 15 | if not os.path.exists(DATA_DIR): 16 | raise IOError('Test data not found, cannot run tests') 17 | 18 | 19 | def logtest(message, type='notice'): 20 | if type == 'warning': 21 | raise WarningMessage(message) 22 | elif type == 'error': 23 | raise ErrorMessage(message) 24 | 25 | 26 | def test_generator__init__(): 27 | raises(RuntimeError, Generator, None) 28 | raises(RuntimeError, Generator, 'foo.md') 29 | 30 | 31 | def test_add_user_assets(): 32 | base_dir = os.path.join(DATA_DIR, 'test.md') 33 | g = Generator(base_dir, logger=logtest) 34 | g.user_css.extend(g.process_user_files(os.path.join(DATA_DIR, 'test.css'))) 35 | g.user_js.extend(g.process_user_files(os.path.join(DATA_DIR, 'test.js'))) 36 | assert g.user_css[0]['contents'].strip() == '* {color: red;}' 37 | assert g.user_js[0]['contents'].strip() == "alert('foo');" 38 | 39 | 40 | def test_get_toc(): 41 | base_dir = os.path.join(DATA_DIR, 'test.md') 42 | g = Generator(base_dir, logger=logtest) 43 | g.add_toc_entry('Section 1', 1, 1) 44 | g.add_toc_entry('Section 1.1', 2, 2) 45 | g.add_toc_entry('Section 1.2', 2, 3) 46 | g.add_toc_entry('Section 2', 1, 4) 47 | g.add_toc_entry('Section 2.1', 2, 5) 48 | g.add_toc_entry('Section 3', 1, 6) 49 | toc = g.toc 50 | assert len(toc) == 3 51 | assert toc[0]['title'] == 'Section 1' 52 | assert len(toc[0]['sub']) == 2 53 | assert toc[0]['sub'][1]['title'] == 'Section 1.2' 54 | assert toc[1]['title'] == 'Section 2' 55 | assert len(toc[1]['sub']) == 1 56 | assert toc[2]['title'] == 'Section 3' 57 | assert len(toc[2]['sub']) == 0 58 | 59 | 60 | def test_get_slide_vars(): 61 | g = Generator(os.path.join(DATA_DIR, 'test.md')) 62 | svars = g.get_slide_vars("

heading

\n

foo

\n

bar

\n", '') 63 | assert svars['title'] == 'heading' 64 | assert svars['level'] == 1 65 | assert svars['header'] == '

heading

' 66 | assert svars['content'] == '

foo

\n

bar

' 67 | assert svars['source'] == {} 68 | assert svars['classes'] == [] 69 | 70 | 71 | def test_unicode(): 72 | g = Generator(os.path.join(DATA_DIR, 'test.md')) 73 | g.execute() 74 | s = g.render() 75 | assert s.find('
') != -1
 76 |     assert len(re.findall('
foo

\n

.notes: bar

\n

baz

', '', ctx) 142 | assert r[0].find('bar') == -1 143 | assert r[1] == [] 144 | assert ctx == {'presenter_notes': '

bar

'} 145 | # FXs 146 | content = '

foo

\n

.fx: blah blob

\n

baz

' 147 | r = g.process_macros(content, '', {}) 148 | assert r[0] == '

foo

\n

baz

' 149 | assert r[1][0] == 'blah' 150 | assert r[1][1] == 'blob' 151 | 152 | 153 | def test_register_macro(): 154 | g = Generator(os.path.join(DATA_DIR, 'test.md')) 155 | 156 | class SampleMacro(macro.Macro): 157 | pass 158 | 159 | g.register_macro(SampleMacro) 160 | assert any(isinstance(i, SampleMacro) for i in g.macros) 161 | 162 | def plop(foo): 163 | pass 164 | 165 | raises(TypeError, g.register_macro, plop) 166 | 167 | 168 | def test_presenter_notes(): 169 | g = Generator(os.path.join(DATA_DIR, 'test.md')) 170 | svars = g.get_slide_vars("

heading

\n

foo

\n" 171 | "

Presenter Notes

\n

bar

\n", '') 172 | assert svars['presenter_notes'] == "

bar

" 173 | 174 | # Check that presenter notes work even if the slide has no heading. 175 | # For example, if it is only an image: 176 | 177 | g = Generator(os.path.join(DATA_DIR, 'test.md')) 178 | svars = g.get_slide_vars("

foo

\n" 179 | "

Presenter Notes

\n

bar

\n", '') 180 | 181 | 182 | def test_skip_presenter_notes(): 183 | g = Generator(os.path.join(DATA_DIR, 'test.md'), 184 | presenter_notes=False) 185 | svars = g.get_slide_vars("

heading

\n

foo

\n" 186 | "

Presenter Notes

\n

bar

\n", '') 187 | assert svars['presenter_notes'] is None 188 | 189 | 190 | SAMPLE_HTML = '''

Let me give you this snippet:

191 |
192 | !python
193 | def foo():
194 |     "just a test"
195 |     print bar
196 | 
197 |

Then this one:

198 |
199 | !php
200 | 
203 | 
204 |

Then this other one:

205 |
206 | !xml
207 | 
208 |     baz
209 | 
210 | 
211 |

End here.

''' 212 | 213 | 214 | def test_macro_parsing_code_blocks(): 215 | m = macro.CodeHighlightingMacro(logtest) 216 | blocks = m.macro_re.findall(SAMPLE_HTML) 217 | assert len(blocks) == 3 218 | assert blocks[0][2] == 'python' 219 | assert blocks[0][3].startswith('def foo():') 220 | assert blocks[1][2] == 'php' 221 | assert blocks[1][3].startswith('') 224 | 225 | 226 | def test_macro_descape(): 227 | m = macro.CodeHighlightingMacro(logtest) 228 | assert m.descape('foo') == 'foo' 229 | assert m.descape('>') == '>' 230 | assert m.descape('<') == '<' 231 | assert m.descape('&lt;') == '<' 232 | assert m.descape('<span>') == '' 233 | assert m.descape('<spam&eggs>') == '' 234 | 235 | 236 | def test_macro_process(): 237 | m = macro.CodeHighlightingMacro(logtest) 238 | hl = m.process("
!php\n$foo;
") 239 | assert hl[0].startswith('
Let me give you this') 250 | assert hl[0].find('

Then this one') > 0 251 | assert hl[0].find('

Then this other one') > 0 252 | assert hl[0].find('

0 253 | assert hl[1][0] == u'has_code' 254 | 255 | 256 | def test_embed_images_macro_process(): 257 | base_dir = os.path.join(DATA_DIR, 'test.md') 258 | m = macro.EmbedImagesMacro(logtest, True) 259 | raises(WarningMessage, m.process, 260 | '', '.') 261 | content, classes = m.process('', base_dir) 262 | match = re.search(r'', 263 | content) 264 | assert base64.b64decode(match.group(1)) 265 | 266 | 267 | def test_fix_image_paths_macro_process(): 268 | base_dir = os.path.join(DATA_DIR, 'test.md') 269 | m = macro.FixImagePathsMacro(logtest, False, options={"relative": False}) 270 | content, classes = m.process('', base_dir) 271 | assert re.match(r'', content), content 272 | 273 | 274 | def test_fx_macro_process(): 275 | m = macro.FxMacro(logtest) 276 | content = '

foo

\n

.fx: blah blob

\n

baz

' 277 | r = m.process(content) 278 | assert r[0] == '

foo

\n

baz

' 279 | assert r[1][0] == 'blah' 280 | assert r[1][1] == 'blob' 281 | 282 | 283 | def test_notes_macro_process(): 284 | m = macro.NotesMacro(logtest) 285 | r = m.process('

foo

\n

.notes: bar

\n

baz

') 286 | assert r[0].find('bar') == -1 287 | assert r[1] == [] 288 | 289 | 290 | def test_parser__init__(): 291 | assert Parser('.md').format == 'markdown' 292 | assert Parser('.markdown').format == 'markdown' 293 | assert Parser('.rst').format == 'restructuredtext' 294 | raises(NotImplementedError, Parser, '.txt') 295 | 296 | 297 | class WarningMessage(Exception): 298 | pass 299 | 300 | 301 | class ErrorMessage(Exception): 302 | pass 303 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv:bootstrap] 2 | deps = 3 | jinja2 4 | tox 5 | skip_install = true 6 | commands = 7 | python ci/bootstrap.py --no-env 8 | passenv = 9 | * 10 | ; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist 11 | 12 | [tox] 13 | envlist = 14 | clean, 15 | check, 16 | docs, 17 | {py36,py37,py38,py39,py310,pypy37,pypy38}, 18 | report 19 | ignore_basepython_conflict = true 20 | 21 | [testenv] 22 | basepython = 23 | pypy37: {env:TOXPYTHON:pypy3.7} 24 | pypy38: {env:TOXPYTHON:pypy3.8} 25 | py36: {env:TOXPYTHON:python3.6} 26 | py37: {env:TOXPYTHON:python3.7} 27 | py38: {env:TOXPYTHON:python3.8} 28 | py39: {env:TOXPYTHON:python3.9} 29 | {bootstrap,clean,check,report,docs,examples,codecov,coveralls}: {env:TOXPYTHON:python3} 30 | setenv = 31 | PYTHONPATH={toxinidir}/tests 32 | PYTHONUNBUFFERED=yes 33 | passenv = 34 | * 35 | usedevelop = false 36 | deps = 37 | pytest 38 | pytest-cov 39 | 40 | Jinja2==3.0.3 41 | Markdown==3.3.6 42 | Pygments==2.10.0 43 | docutils==0.18.1 44 | textile==4.0.2 45 | qrcode==7.3.1 46 | watchdog==2.1.6 47 | 48 | commands = 49 | {py36,py37,py38,py39,py310,pypy37,pypy38}: {posargs:py.test --cov=darkslide --cov-report=term-missing -vv tests} 50 | examples: darkslide --verbose --debug {posargs:--embed} examples/config-file/presentation.cfg --destination=dist/examples/index.html 51 | examples: darkslide --verbose --debug {posargs:--embed} --theme=void examples/config-file/presentation.cfg --destination=dist/examples/void.html 52 | examples: darkslide --verbose --debug {posargs:--embed} --theme=abyss examples/config-file/presentation.cfg --destination=dist/examples/abyss.html 53 | examples: darkslide --verbose --debug {posargs:--embed} --theme=white examples/config-file/presentation.cfg --destination=dist/examples/white.html 54 | examples: darkslide --verbose --debug {posargs:--embed} --mod=wide16x9 examples/config-file/presentation.cfg --destination=dist/examples/default_wide16x9.html 55 | examples: darkslide --verbose --debug {posargs:--embed} --mod=wide16x9 --theme=void examples/config-file/presentation.cfg --destination=dist/examples/void_wide16x9.html 56 | examples: darkslide --verbose --debug {posargs:--embed} --mod=wide16x9 --theme=abyss examples/config-file/presentation.cfg --destination=dist/examples/abyss_wide16x9.html 57 | examples: darkslide --verbose --debug {posargs:--embed} --mod=wide16x9 --theme=white examples/config-file/presentation.cfg --destination=dist/examples/white_wide16x9.html 58 | 59 | [testenv:examples] 60 | usedevelop = true 61 | 62 | [testenv:check] 63 | deps = 64 | docutils 65 | check-manifest 66 | flake8 67 | readme-renderer 68 | pygments 69 | isort 70 | skip_install = true 71 | commands = 72 | python setup.py check --strict --metadata --restructuredtext 73 | check-manifest {toxinidir} 74 | flake8 75 | isort --verbose --check-only --diff --filter-files . 76 | 77 | [testenv:docs] 78 | usedevelop = true 79 | deps = 80 | -r{toxinidir}/docs/requirements.txt 81 | commands = 82 | sphinx-build {posargs:-E} -b html docs dist/docs 83 | sphinx-build -b linkcheck docs dist/docs 84 | 85 | [testenv:coveralls] 86 | deps = 87 | coveralls 88 | skip_install = true 89 | commands = 90 | coveralls [] 91 | 92 | [testenv:codecov] 93 | deps = 94 | codecov 95 | skip_install = true 96 | commands = 97 | codecov [] 98 | 99 | [testenv:report] 100 | deps = 101 | coverage 102 | skip_install = true 103 | commands = 104 | coverage report 105 | coverage html 106 | 107 | [testenv:clean] 108 | commands = coverage erase 109 | skip_install = true 110 | deps = 111 | coverage 112 | --------------------------------------------------------------------------------