Compare commits

...

31 Commits

Author SHA1 Message Date
57ab592d36 chore(deps): bump version from 5.2.0 to 5.3.0 2025-01-10 11:57:16 +01:00
840d1d80d9 fix(convert): avoid micro-stuttering with --one-file in HTML presentation (#508)
* fix: Fix micro-stuttering with --one-file in HTML presentation

* Update template.html

* Update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* Update CHANGELOG.md

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2025-01-09 14:13:57 +01:00
c15cd95565 chore(docs): Manim Slides survey 2025-01-08 11:34:37 +01:00
478552c528 fix(docs): dead links (#507) 2025-01-03 14:20:05 +01:00
1189f37cf3 feat(convert/html): inline CSS and JS with convert --one_file --offline (#505)
* feat: Inline CSS and JS by default with --offline

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore: Add test

* Add one_file parameter

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix lint

* Fix typo

* Fix typo

* Fix IPython magic doc

* Update manim_slides/convert.py

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* Add test for one_file=true

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update manim_slides/convert.py

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* Update manim_slides/convert.py

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* Update docs/source/reference/sharing.md

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* Update manim_slides/convert.py

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add changelog and tests

* Fix IPython magic

* Update docs/source/faq.md

* Update CHANGELOG.md

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2025-01-03 13:49:05 +01:00
e50271b0b2 feat(cli): document config options (#485)
* feat(cli): document config options

* chpre(docs): add changelog entry

* fixes

* chore(deps): remove restriction

* fix(deps): PySide6 issue

See https://github.com/astral-sh/uv/issues/10056

* chore(docs): document config options

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fox(docs): deps

* chore(tests): remove old test

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-03 09:19:41 +01:00
98955bee5c fix(deps): bump ManimGL>=1.7.2 (#506)
* fix(deps): bump ManimGL>=1.7.2

This bumps and fixes issues related to latest ManimGL version, as it includes breaking changes.

* fix(docs): PR number

* fix(deps): PySide6 issue

See https://github.com/astral-sh/uv/issues/10056

* fix(ci): headers
2025-01-02 17:39:27 +01:00
2169938df7 chore(deps): bump version from 5.1.10 to 5.2.0 2024-12-24 11:12:02 +01:00
a207248deb chore(deps): update lock 2024-12-24 11:10:58 +01:00
1c859991b8 chore(deps): pre-commit autoupdate (#504)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.3 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4)
- [github.com/pre-commit/mirrors-mypy: v1.13.0 → v1.14.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.13.0...v1.14.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-24 11:10:04 +01:00
a24915665d chore(deps): pre-commit autoupdate (#501)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.2 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.2...v0.8.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-18 10:44:08 +01:00
5cce117050 chore(deps): bump astral-sh/setup-uv from 3 to 4 (#492)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 4.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v4)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-11 16:10:01 +01:00
bd76fbdfd9 chore(deps): pre-commit autoupdate (#486)
* chore(deps): pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.1 → v0.8.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.1...v0.8.2)

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-11 15:55:29 +01:00
5b026919ea chore(deps): bump ManimGL to 1.7.1 (#499)
* chore(deps): bump ManimGL to 1.7.1

Bump ManimGL's minimal version, so relax constraints on other deps and remove compatibility issues with Manim

* fix(docs): correct PR number

* fix(lib): update ManimGL's init

See https://github.com/3b1b/manim/issues/2261

* fix(lib): force float

* chore(tests): correctly ignore warning

* fix(tests)

* fix(tests): add skips

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(tests)

* oops

* fix on 3.12

* fix(lib): correctly patch ManimGL

* fix(deps): pyrr issue

* fix: version

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-11 15:54:59 +01:00
05c1a16ca3 feat(cli): smarter info window hiding logic (#482)
* Adds features from #327

This changes the ``--hide-info-window-`` command to accept three options: auto, always, and never. Auto will hide the info window if there is only one screen. This also fixes the --full-screen option by moving the info window to monitor 1 and the presentation to monitor 0 by default.

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Let Qt decide the screens

* Fix full screen bug

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(cli): improve logic

* fix

* Revert fixes and clean code

* Update changelog

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-11 09:16:50 +01:00
3bd8c386b1 chore(deps): bump codecov/codecov-action from 4 to 5 (#491)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-06 18:05:18 +01:00
628c8da832 fix(convert): blank web page when converting multiple slides into HTML (#497)
* fix(convert): Blank web page when converting multiple slides into HTML

* chore(docs): add changelog

* Fix typo
2024-12-04 12:25:34 +01:00
3b62e6b788 chore(deps): bump version from 5.1.9 to 5.1.10 2024-12-03 12:57:36 +01:00
ff9aac49d7 chore(deps): pin av<14 to avoid breaking changes (#495)
* chore(deps): Pin `av<14` to avoid breaking changes

Closes #494

* chore(docs): syntax
2024-12-03 12:56:16 +01:00
16d9d32d33 fix(ci): actually run tests on multiple Python versions (#493) 2024-12-03 12:17:32 +01:00
988011ff7d feat(cli): allow multiple reverses (#488)
* Permit multiple reverses

* chore(docs): add changelog entry

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-11-12 12:09:35 +01:00
3dbe12b480 fix(ci): typo 2024-11-06 10:25:18 +01:00
6ba657c0d5 fix(ci): feature_request.yml 2024-11-06 10:24:50 +01:00
6c8ab61f9d chore(deps): pre-commit autoupdate (#480)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.6.8 → v0.7.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.8...v0.7.1)
- [github.com/pre-commit/mirrors-mypy: v1.11.2 → v1.13.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.2...v1.13.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-11-01 10:01:53 +01:00
91e6e139e3 fix(docs): some typos (#484) 2024-11-01 10:01:28 +01:00
d8acbae165 feat(convert): allow fully offline HTML presentation (#440)
* feat(cli): allow offline HTML presentations

* feat(convert): allow fully offline HTML presentation

TODO: check if this is really the case, especially for nested dependencies?

Closes #438

* fix(cli): typo

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(tests): add tests

* fix(cli): use full path

* fix(tests): typo

* chore(ci): avoid specific kernel name

* fix ?

* chore(lib): simplify logic

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-01 10:00:48 +01:00
75af26e601 chore(docker): drop legacy syntax for env variables 2024-10-15 17:58:20 +02:00
a8903b809d chore(deps): bump version from 5.1.8 to 5.1.9 2024-10-15 17:24:48 +02:00
d813aaf313 chore(docs): add changelog entry 2024-10-15 17:24:37 +02:00
d5679924b9 fix(ci): docker image (#481)
* fix(ci): docker image

* fix(ci): test build docker

* fix(ci): typo :-(

* fix(ci): don't use `--system`
2024-10-15 17:22:03 +02:00
fb562d88ac chore(deps): bump version from 5.1.7 to 5.1.8 2024-10-02 19:56:23 +02:00
35 changed files with 2805 additions and 1518 deletions

View File

@ -29,7 +29,7 @@ body:
options:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Abug+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- label: Checked the [frequently qsked questions]](https://manim-slides.eertmans.be/latest/faq.html);
- label: Checked the [frequently asked questions]](https://manim-slides.eertmans.be/latest/faq.html);
required: true
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
required: true

View File

@ -18,7 +18,7 @@ body:
label: Terms
description: 'By submitting this issue, I have:'
options:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Adocumentation+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Aenhancement+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- type: textarea

View File

@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
@ -36,7 +36,6 @@ jobs:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
steps:
- name: Checkout
uses: actions/checkout@v4
@ -67,7 +66,7 @@ jobs:
with:
platforms: linux/arm64,linux/amd64
file: docker/Dockerfile
push: true
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }}
tags: |
ghcr.io/jeertmans/manim-slides:latest
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}

View File

@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os: [macos-13, ubuntu-latest, windows-latest]
pyversion: ['3.9', '3.10', '3.11', '3.12']
extras: [pyside6-full, manimgl]
exclude:
@ -41,7 +41,9 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get install build-essential python${{ matrix.pyversion }}-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
@ -70,13 +72,10 @@ jobs:
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Setup Python ${{ matrix.pyversion }}
run: uv python install ${{ matrix.pyversion }}
- name: Install manim dependencies on MacOS
if: matrix.os == 'macos-latest'
run: brew install ffmpeg py3cairo pango pkg-config scipy
@ -96,14 +95,11 @@ jobs:
if: matrix.os == 'windows-latest'
uses: ssciwr/setup-mesa-dist-win@v2
- name: Install Manim Slides
run: uv sync --locked --extra tests
- name: Run pytest
run: uv run pytest
run: uv run --python ${{ matrix.pyversion }} --frozen --extra tests pytest
- name: Upload to codecov.io
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:

View File

@ -6,7 +6,7 @@ ci:
autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-yaml
- id: check-toml
@ -21,13 +21,13 @@ repos:
exclude: poetry.lock
args: [--autofix, --trailing-commas]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.8
rev: v0.8.4
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2
rev: v1.14.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]

View File

@ -6,9 +6,6 @@ build:
apt_packages:
- libpango1.0-dev
- ffmpeg
jobs:
post_install:
- ipython kernel install --name "manim-slides" --user
sphinx:
builder: html
configuration: docs/source/conf.py

View File

@ -8,9 +8,95 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- start changelog -->
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.7...HEAD)
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.3.0...HEAD)
(unreleased-added)=
(v5.3.0)=
## [v5.3.0](https://github.com/jeertmans/manim-slides/compare/v5.2.0...v5.3.0)
(v5.3.0-added)=
### Added
- Added CSS and JS inline for `manim-slides convert` if `--offline`
and `--one-file` (`-cone_file`) are used for HTML output.
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
(v5.3.0-changed)=
### Changed
- Deprecate `-cdata_uri` in favor of `-cone_file` for `manim-slides convert`.
[@Rapsssito](https://github.com/Rapsssito) [#505](https://github.com/jeertmans/manim-slides/pull/505)
- Changed template to avoid micro-stuttering with `--one-file` in HTML presentation.
[@Rapsssito](https://github.com/Rapsssito) [#508](https://github.com/jeertmans/manim-slides/pull/508)
(v5.2.0)=
## [v5.2.0](https://github.com/jeertmans/manim-slides/compare/v5.1.10...v5.2.0)
(v5.2.0-changed)=
### Changed
- The info window is now only shown in presentations when there
are multiple monitors. However, the `--show-info-window` option
was added to `manim-slides present` to force the info window.
When there are multiple monitors, the info window will no longer
be on the same monitor as the main window, unless overridden.
[@PeculiarProgrammer](https://github.com/PeculiarProgrammer)
[#482](https://github.com/jeertmans/manim-slides/pull/482)
(v5.2.0-chore)=
### Chore
- Bump ManimGL to `>=1.7.1`, to remove conflicting dependencies
with Manim's.
[#499](https://github.com/jeertmans/manim-slides/pull/499)
- Bump ManimGL to `>=1.7.2`, to remove `pyrr` from dependencies,
and to avoid complex code for supporting both `1.7.1` and `>=1.7.2`,
as the latter includes many breaking changes.
[#506](https://github.com/jeertmans/manim-slides/pull/506)
(v5.1.10)=
## [v5.1.10](https://github.com/jeertmans/manim-slides/compare/v5.1.9...v5.1.10)
(v5.1.10-added)=
### Added
- Added `--offline` option to `manim-slides convert` for offline
HTML presentations.
[#440](https://github.com/jeertmans/manim-slides/pull/440)
- Added documentation to config option to `manim-slides convert`
when using `--show-config`.
[#485](https://github.com/jeertmans/manim-slides/pull/485)
(v5.1.10-changed)=
### Changed
- Allow multiple slide reverses by going backward [@PeculiarProgrammer](https://github.com/PeculiarProgrammer).
[#488](https://github.com/jeertmans/manim-slides/pull/488)
(v5.1.10-fixed)=
### Fixed
- Fixed PyAV issue by pinning its version to `<14`.
A future release will contain a fix that supports both `av>=14`
and `av<14`, as their syntax differ, but the former doesn't
provide binary wheels for Python 3.9.
[#494](https://github.com/jeertmans/manim-slides/pull/494)
- Fixed blank web page when converting multiple slides into HTML.
[#497](https://github.com/jeertmans/manim-slides/pull/497)
(v5.1.9)=
## [v5.1.9](https://github.com/jeertmans/manim-slides/compare/v5.1.8...v5.1.9)
(v5.1.9-fixed)=
## Chore
- Fixed failing docker builds.
[#481](https://github.com/jeertmans/manim-slides/pull/481)
(v5.1.8)=
## [v5.1.8](https://github.com/jeertmans/manim-slides/compare/v5.1.7...v5.1.8)
(v5.1.8-added)=
### Added
- Added `manim-slides checkhealth` command to easily obtain important information
@ -23,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
and asset files.
[#470](https://github.com/jeertmans/manim-slides/pull/470)
(unreleased-chore)=
(v5.1.8-chore)=
### Chore
- Pin `rtoml==0.9.0` on Windows platforms,
@ -49,7 +135,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed project manager from Rye to uv.
[#476](https://github.com/jeertmans/manim-slides/pull/476)
(unreleased-fixed)=
(v5.1.8-fixed)=
### Fixed
- Fix combining assets from multiple scenes to avoid filename collision.
@ -60,7 +146,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
[#465](https://github.com/jeertmans/manim-slides/pull/465)
(unreleased-removed)=
(v5.1.8-removed)=
### Removed
- Removed `full-gl` extra, because it does not make sense to ship both

View File

@ -26,7 +26,7 @@ keywords:
- PowerPoint
- Python
license: MIT
version: v5.1.7
version: v5.3.0
preferred-citation:
publisher:
name: The Open Journal

View File

@ -1,3 +1,9 @@
> [!IMPORTANT]
> Take the [**Manim Slides Survey**](https://forms.gle/i4scrwPQghbTQwQs5)
> to help improve this tool! Thanks in advance to all the people taking the time
> to answer this short survey! The form is open until **January 31st 2025**,
> and results will be communicated in the GitHub discussions.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">

View File

@ -1,2 +1,2 @@
style:
camera:
background_color: '#000000'

View File

@ -1,9 +1,8 @@
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/68bd79093e1ebc1ed9f8051942ffe6e72a9e66a7/docker/Dockerfile
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/v0.18.1/docker/Dockerfile
FROM python:3.11-slim
RUN apt-get update -qq \
&& apt-get install --no-install-recommends -y \
ffmpeg \
build-essential \
gcc \
cmake \
@ -24,21 +23,23 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tl
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
&& tlmgr install \
amsmath babel-english cbfonts-fd cm-super ctex doublestroke dvisvgm everysel \
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
mathastext microtype ms physics preview ragged2e relsize rsfs \
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
# clone and build manim-slides
COPY . /opt/manim-slides
WORKDIR /opt/manim-slides
RUN pip install --no-cache manim[jupyterlab] .[sphinx-directive]
ENV UV_PYTHON=/usr/local/bin/python
RUN pip install --no-cache-dir uv
RUN uv pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
ARG NB_USER=manimslidesuser
ARG NB_UID=1000
ENV USER ${NB_USER}
ENV NB_UID ${NB_UID}
ENV HOME /manim-slides
ENV USER=${NB_USER}
ENV NB_UID=${NB_UID}
ENV HOME=/manim-slides
RUN adduser --disabled-password \
--gecos "Default user" \

View File

@ -21,7 +21,7 @@
{%- for presentation_config in presentation_configs -%}
{% set outer_loop = loop %}
{%- for slide_config in presentation_config.slides -%}
{%- if data_uri -%}
{%- if one_file -%}
{% set file = file_to_data_uri(slide_config.file) %}
{%- else -%}
{% set file = assets_dir / slide_config.file.name %}
@ -315,16 +315,16 @@
hideCursorTime: {{ hide_cursor_time }}
});
{% if data_uri %}
// Fix found by @t-fritsch on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
{% if one_file %}
// Fix found by @t-fritsch and @Rapsssito on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
function fixBase64VideoBackground(event) {
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
if (event.currentSlide.getAttribute('data-background-video')) {
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
video = background.querySelector('video'),
sources = video.querySelectorAll('source');
// Analyze all slides backgrounds
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
// Get the slide video and its sources for each background
const video = slide.querySelector('video');
const sources = video.querySelectorAll('source');
// Update the source of the video
sources.forEach((source, i) => {
const src = source.getAttribute('src');
if(src.match(/^data:video.*;base64$/)) {
@ -334,8 +334,8 @@
});
}
}
// Setup base64 videos
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
{% endif %}
</script>

View File

@ -30,9 +30,11 @@ extensions = [
# Additional
"nbsphinx",
"myst_parser",
"sphinxcontrib.programoutput",
"sphinxext.opengraph",
"sphinx_click",
"sphinx_copybutton",
"sphinx_design",
# Custom
"manim_slides.docs.manim_slides_directive",
]

View File

@ -29,13 +29,8 @@ ManimGL support is only guaranteed to work
on a very minimal set of versions, because it differs quite a lot from ManimCE,
and its development is not very active.
The typical issues are that (1) ManimGL needs an outdated NumPy version
and (2) ManimGL **should not** be installed from the GitHub repository,
at least not from the `main` branch, but from a version released to PyPI.
To solve the NumPy issue, you can safely downgrade NumPy to a version supported
by ManimGL,
while ignoring the possible *conflicting dependencies* messages from `pip` (or else).
The typical issue is that ManimGL `<1.7.1` needs an outdated NumPy version, but
can be resolved by manually downgrading NumPy, or upgrading ManimGL (**recommended**).
### Presenting
@ -107,7 +102,7 @@ Questions related to `manim-slides convert [SCENES]... output.html`.
### I moved my `.html` file and it stopped working
If you did not specify `-cdata_uri=true` when converting,
If you did not specify `--one-file` (or `-cone_file=true`) when converting,
then Manim Slides generated a folder containing all
the video files, in the same folder as the HTML
output. As the path to video files is a relative path,

View File

@ -23,10 +23,10 @@ using Manim Slides presentations.
Daniel publishes his presentations on *Cosmology, String Theory and related*
topics on his
[personal website](https://panopepino.github.io/Web_Page/main_page/slides.html). https://panopepino.github.io/Web_Page/main_page/slides.html
[personal website](https://panopepino.github.io/web_page/main_page/slides.html). https://panopepino.github.io/web_page/main_page/slides.html
For example, below are the slides of a seminar he gave titled
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html).
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html).
<div style="position:relative;padding-bottom:56.25%;">
<iframe
@ -37,12 +37,12 @@ For example, below are the slides of a seminar he gave titled
height="100%"
allowfullscreen
allow="autoplay"
src="https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html">
src="https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html">
</iframe>
</div>
He also shares his code on a public
[GitHub repository](https://github.com/PanoPepino/Manim_Theoretical).
[GitHub repository](https://github.com/PanoPepino/mtheoretical).
### Jérome Eertmans

View File

@ -29,19 +29,6 @@ please refer to their specific installation guidelines:
- [Manim](https://docs.manim.community/en/stable/installation.html)
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
:::{warning}
If you install Manim from its git repository, as suggested by ManimGL,
make sure to first check out a supported version (e.g., `git checkout tags/v1.6.1`
for ManimGL), otherwise it might install an unsupported version of Manim!
See [#314](https://github.com/jeertmans/manim-slides/issues/314).
Also, note that ManimGL uses outdated dependencies, and may
not work out-of-the-box. One example is NumPy: ManimGL
does not specify any restriction on this package, but
only `numpy<1.25` will work, see
[#2053](https://github.com/3b1b/manim/issues/2053).
:::
<!-- end deps -->
## Pip Install
@ -137,12 +124,12 @@ Manim Slides is distributed under Nixpkgs >=24.05.
If you are using Nix or NixOS, you can find Manim Slides under:
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
includes pyqt6 (see above);
includes PyQt6 (see above);
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
module (for notebook magics), and includes IPython but not does not include
any Qt bindings.
module (for notebook magics), and includes IPython but does not include
any Qt binding.
You can try out the Manim Slides package with
You can try out the Manim Slides package with:
```sh
nix-shell -p manim ffmpeg manim-slides
@ -160,7 +147,7 @@ nix-shell -p manim ffmpeg "python3.withPackages(ps: with ps; [ manim-slides, ...
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
:::{note}
Nix current does not support `manimgl`.
Nix does not currently support `manimgl`.
:::
## When you need a Qt backend

View File

@ -8,3 +8,30 @@ This page contains an exhaustive list of all the commands available with `manim-
:prog: manim-slides
:nested: full
```
## All config options
Each converter has its own configuration options, which are listed below.
::::{dropdown} HTML
```{program-output} manim-slides convert --to=html --show-config
```
::::
::::{dropdown} Zip
:::{note}
The Zip converter inherits from the HTML converter.
:::
```{program-output} manim-slides convert --to=zip --show-config
```
::::
::::{dropdown} PDF
```{program-output} manim-slides convert --to=pdf --show-config
```
::::
::::{dropdown} HTML
```{program-output} manim-slides convert --to=pdf --show-config
```
::::

View File

@ -78,9 +78,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "manim-slides",
"display_name": ".venv",
"language": "python",
"name": "manim-slides"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
@ -92,7 +92,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.11.8"
}
},
"nbformat": 4,

View File

@ -137,9 +137,10 @@ and it there to preserve the original aspect ratio (16:9).
### Sharing ONE HTML file
If you set the `data_uri` option to `true` (with `-cdata_uri=true`),
all animations will be data URI encoded, making the HTML a self-contained
presentation file that can be shared on its own.
If you set the `--one-file` flag, all animations will be data URI encoded,
making the HTML a self-contained presentation file that can be shared
on its own. If you also set the `--offline` flag, the JS and CSS files will
be included in the HTML file as well.
### Over the internet

View File

@ -1 +1 @@
__version__ = "5.1.7"
__version__ = "5.3.0"

View File

@ -4,6 +4,8 @@ import platform
import shutil
import subprocess
import tempfile
import textwrap
import warnings
import webbrowser
from base64 import b64encode
from collections import deque
@ -15,6 +17,8 @@ from typing import Any, Callable, Optional, Union
import av
import click
import pptx
import requests
from bs4 import BeautifulSoup
from click import Context, Parameter
from jinja2 import Template
from lxml import etree
@ -28,7 +32,6 @@ from pydantic import (
PositiveFloat,
PositiveInt,
ValidationError,
conlist,
)
from pydantic_core import CoreSchema, core_schema
from pydantic_extra_types.color import Color
@ -100,9 +103,12 @@ def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
class Converter(BaseModel): # type: ignore
presentation_configs: conlist(PresentationConfig, min_length=1) # type: ignore[valid-type]
assets_dir: str = "{basename}_assets"
template: Optional[Path] = None
presentation_configs: list[PresentationConfig]
assets_dir: str = Field(
"{basename}_assets",
description="Assets folder.\nThis is a template string that accepts 'dirname', 'basename', and 'ext' as variables.\nThose variables are obtained from the output filename.",
)
template: Optional[Path] = Field(None, description="Custom template file to use.")
def convert_to(self, dest: Path) -> None:
"""Convert self, i.e., a list of presentations, into a given format."""
@ -287,49 +293,153 @@ class RevealTheme(str, StrEnum):
class RevealJS(Converter):
# Export option: use data-uri
data_uri: bool = False
"""
RevealJS options.
Please check out https://revealjs.com/config/ for more details.
"""
# Export option:
one_file: bool = Field(
False, description="Embed all assets (e.g., animations) inside the HTML."
)
offline: bool = Field(
False, description="Download remote assets for offline presentation."
)
# Presentation size options from RevealJS
width: Union[Str, int] = Str("100%")
height: Union[Str, int] = Str("100%")
margin: float = 0.04
min_scale: float = 0.2
max_scale: float = 2.0
width: Union[Str, int] = Field(
Str("100%"), description="Width of the presentation."
)
height: Union[Str, int] = Field(
Str("100%"), description="Height of the presentation."
)
margin: float = Field(0.04, description="Margin to use around the content.")
min_scale: float = Field(
0.2, description="Bound for smallest possible scale to apply to content."
)
max_scale: float = Field(
2.0, description="Bound for large possible scale to apply to content."
)
# Configuration options from RevealJS
controls: JsBool = JsBool.false
controls_tutorial: JsBool = JsBool.true
controls_layout: ControlsLayout = ControlsLayout.bottom_right
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
progress: JsBool = JsBool.false
slide_number: SlideNumber = SlideNumber.false
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
hash_one_based_index: JsBool = JsBool.false
hash: JsBool = JsBool.false
respond_to_hash_changes: JsBool = JsBool.false
history: JsBool = JsBool.false
keyboard: JsBool = JsBool.true
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
disable_layout: JsBool = JsBool.false
overview: JsBool = JsBool.true
center: JsBool = JsBool.true
touch: JsBool = JsBool.true
loop: JsBool = JsBool.false
rtl: JsBool = JsBool.false
navigation_mode: NavigationMode = NavigationMode.default
shuffle: JsBool = JsBool.false
fragments: JsBool = JsBool.true
fragment_in_url: JsBool = JsBool.true
embedded: JsBool = JsBool.false
help: JsBool = JsBool.true
pause: JsBool = JsBool.true
show_notes: JsBool = JsBool.false
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
preload_iframes: PreloadIframes = PreloadIframes.null
auto_animate: JsBool = JsBool.true
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
auto_animate_duration: float = 1.0
auto_animate_unmatched: JsBool = JsBool.true
controls: JsBool = Field(
JsBool.false, description="Display presentation control arrows."
)
controls_tutorial: JsBool = Field(
JsBool.true, description="Help the user learn the controls by providing hints."
)
controls_layout: ControlsLayout = Field(
ControlsLayout.bottom_right, description="Determine where controls appear."
)
controls_back_arrows: ControlsBackArrows = Field(
ControlsBackArrows.faded,
description="Visibility rule for backwards navigation arrows.",
)
progress: JsBool = Field(
JsBool.false, description="Display a presentation progress bar."
)
slide_number: SlideNumber = Field(
SlideNumber.false, description="Display the page number of the current slide."
)
show_slide_number: Union[ShowSlideNumber, Function] = Field(
ShowSlideNumber.all,
description="Can be used to limit the contexts in which the slide number appears.",
)
hash_one_based_index: JsBool = Field(
JsBool.false,
description="Use 1 based indexing for # links to match slide number (default is zero based).",
)
hash: JsBool = Field(
JsBool.false,
description="Add the current slide number to the URL hash so that reloading the page/copying the URL will return you to the same slide.",
)
respond_to_hash_changes: JsBool = Field(
JsBool.false,
description="Flags if we should monitor the hash and change slides accordingly.",
)
jump_to_slide: JsBool = Field(
JsBool.true,
description="Enable support for jump-to-slide navigation shortcuts.",
)
history: JsBool = Field(
JsBool.false,
description="Push each slide change to the browser history. Implies `hash: true`.",
)
keyboard: JsBool = Field(
JsBool.true, description="Enable keyboard shortcuts for navigation."
)
keyboard_condition: Union[KeyboardCondition, Function] = Field(
KeyboardCondition.null,
description="Optional function that blocks keyboard events when retuning false.",
)
disable_layout: JsBool = Field(
JsBool.false,
description="Disable the default reveal.js slide layout (scaling and centering) so that you can use custom CSS layout.",
)
overview: JsBool = Field(JsBool.true, description="Enable the slide overview mode.")
center: JsBool = Field(JsBool.true, description="Vertical centering of slides.")
touch: JsBool = Field(
JsBool.true, description="Enable touch navigation on devices with touch input."
)
loop: JsBool = Field(JsBool.false, description="Loop the presentation.")
rtl: JsBool = Field(
JsBool.false, description="Change the presentation direction to be RTL."
)
navigation_mode: NavigationMode = Field(
NavigationMode.default,
description="Change the behavior of our navigation directions.",
)
shuffle: JsBool = Field(
JsBool.false,
description="Randomize the order of slides each time the presentation loads.",
)
fragments: JsBool = Field(
JsBool.true, description="Turns fragment on and off globally."
)
fragment_in_url: JsBool = Field(
JsBool.true,
description="Flag whether to include the current fragment in the URL, so that reloading brings you to the same fragment position.",
)
embedded: JsBool = Field(
JsBool.false,
description="Flag if the presentation is running in an embedded mode, i.e. contained within a limited portion of the screen.",
)
help: JsBool = Field(
JsBool.true,
description="Flag if we should show a help overlay when the question-mark key is pressed.",
)
pause: JsBool = Field(
JsBool.true,
description="Flag if it should be possible to pause the presentation (blackout).",
)
show_notes: JsBool = Field(
JsBool.false,
description="Flag if speaker notes should be visible to all viewers.",
)
auto_play_media: AutoPlayMedia = Field(
AutoPlayMedia.null,
description="Global override for autolaying embedded media (video/audio/iframe).",
)
preload_iframes: PreloadIframes = Field(
PreloadIframes.null,
description="Global override for preloading lazy-loaded iframes.",
)
auto_animate: JsBool = Field(
JsBool.true, description="Can be used to globally disable auto-animation."
)
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = Field(
AutoAnimateMatcher.null,
description="Optionally provide a custom element matcher that will be used to dictate which elements we can animate between.",
)
auto_animate_easing: AutoAnimateEasing = Field(
AutoAnimateEasing.ease,
description="Default settings for our auto-animate transitions, can be overridden per-slide or per-element via data arguments.",
)
auto_animate_duration: float = Field(
1.0, description="See 'auto_animate_easing' documentation."
)
auto_animate_unmatched: JsBool = Field(
JsBool.true, description="See 'auto_animate_easing' documentation."
)
auto_animate_styles: list[str] = Field(
default_factory=lambda: [
"opacity",
@ -344,34 +454,88 @@ class RevealJS(Converter):
"border-radius",
"outline",
"outline-offset",
]
],
description="CSS properties that can be auto-animated.",
)
auto_slide: AutoSlide = Field(
0, description="Control automatic progression to the next slide."
)
auto_slide_stoppable: JsBool = Field(
JsBool.true, description="Stop auto-sliding after user input."
)
auto_slide_method: Union[AutoSlideMethod, Function] = Field(
AutoSlideMethod.null,
description="Use this method for navigation when auto-sliding (defaults to navigateNext).",
)
default_timing: Union[JsNull, int] = Field(
JsNull.null,
description="Specify the average time in seconds that you think you will spend presenting each slide.",
)
mouse_wheel: JsBool = Field(
JsBool.false, description="Enable slide navigation via mouse wheel."
)
preview_links: JsBool = Field(
JsBool.false, description="Open links in an iframe preview overlay."
)
post_message: JsBool = Field(
JsBool.true, description="Expose the reveal.js API through window.postMessage."
)
post_message_events: JsBool = Field(
JsBool.false,
description="Dispatch all reveal.js events to the parent window through postMessage.",
)
focus_body_on_page_visibility_change: JsBool = Field(
JsBool.true,
description="Focus body when page changes visibility to ensure keyboard shortcuts work.",
)
transition: Transition = Field(Transition.none, description="Transition style.")
transition_speed: TransitionSpeed = Field(
TransitionSpeed.default, description="Transition speed."
)
background_size: BackgroundSize = Field(
BackgroundSize.contain, description="Background size attribute for each video."
) # Not in RevealJS
background_transition: BackgroundTransition = Field(
BackgroundTransition.none,
description="Transition style for full page slide backgrounds.",
)
pdf_max_pages_per_slide: Union[int, str] = Field(
"Number.POSITIVE_INFINITY",
description="The maximum number of pages a single slide can expand onto when printing to PDF, unlimited by default.",
)
pdf_separate_fragments: JsBool = Field(
JsBool.true, description="Print each fragment on a separate slide."
)
pdf_page_height_offset: int = Field(
-1,
description="Offset used to reduce the height of content within exported PDF pages.",
)
view_distance: int = Field(
3, description="Number of slides away from the current that are visible."
)
mobile_view_distance: int = Field(
2,
description="Number of slides away from the current that are visible on mobile devices.",
)
display: Display = Field(
Display.block, description="The display mode that will be used to show slides."
)
hide_inactive_cursor: JsBool = Field(
JsBool.true, description="Hide cursor if inactive."
)
hide_cursor_time: int = Field(
5000, description="Time before the cursor is hidden (in ms)."
)
auto_slide: AutoSlide = 0
auto_slide_stoppable: JsBool = JsBool.true
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
default_timing: Union[JsNull, int] = JsNull.null
mouse_wheel: JsBool = JsBool.false
preview_links: JsBool = JsBool.false
post_message: JsBool = JsBool.true
post_message_events: JsBool = JsBool.false
focus_body_on_page_visibility_change: JsBool = JsBool.true
transition: Transition = Transition.none
transition_speed: TransitionSpeed = TransitionSpeed.default
background_size: BackgroundSize = BackgroundSize.contain # Not in RevealJS
background_transition: BackgroundTransition = BackgroundTransition.none
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
pdf_separate_fragments: JsBool = JsBool.true
pdf_page_height_offset: int = -1
view_distance: int = 3
mobile_view_distance: int = 2
display: Display = Display.block
hide_inactive_cursor: JsBool = JsBool.true
hide_cursor_time: int = 5000
# Appearance options from RevealJS
background_color: Color = "black"
reveal_version: str = "5.1.0"
reveal_theme: RevealTheme = RevealTheme.black
title: str = "Manim Slides"
background_color: Color = Field(
"black",
description="Background color used in slides, not relevant if videos fill the whole area.",
)
reveal_version: str = Field("5.1.0", description="RevealJS version.")
reveal_theme: RevealTheme = Field(
RevealTheme.black, description="RevealJS version."
)
title: str = Field("Manim Slides", description="Presentation title.")
# Pydantic options
model_config = ConfigDict(use_enum_values=True, extra="forbid")
@ -385,27 +549,24 @@ class RevealJS(Converter):
def open(self, file: Path) -> None:
webbrowser.open(file.absolute().as_uri())
def convert_to(self, dest: Path) -> None:
def convert_to(self, dest: Path) -> None: # noqa: C901
"""
Convert this configuration into a RevealJS HTML presentation, saved to
DEST.
"""
if self.data_uri:
assets_dir = Path("") # Actually we won't care.
else:
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = dirname / assets_dir
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = dirname / assets_dir
if not self.one_file or self.offline:
logger.debug(f"Assets will be saved to: {full_assets_dir}")
full_assets_dir.mkdir(parents=True, exist_ok=True)
if not self.one_file:
num_presentation_configs = len(self.presentation_configs)
if num_presentation_configs > 1:
@ -424,6 +585,7 @@ class RevealJS(Converter):
def prefix(i: int) -> str:
return ""
full_assets_dir.mkdir(parents=True, exist_ok=True)
for i, presentation_config in enumerate(self.presentation_configs):
presentation_config.copy_to(
full_assets_dir, include_reversed=False, prefix=prefix(i)
@ -435,7 +597,9 @@ class RevealJS(Converter):
revealjs_template = Template(self.load_template())
options = self.model_dump()
options["assets_dir"] = assets_dir
if assets_dir is not None:
options["assets_dir"] = assets_dir
has_notes = any(
slide_config.notes != ""
@ -448,9 +612,50 @@ class RevealJS(Converter):
get_duration_ms=get_duration_ms,
has_notes=has_notes,
env=os.environ,
prefix=prefix if not self.one_file else None,
**options,
)
# If not offline, write the content to the file
if not self.offline:
f.write(content)
return
# If offline, download remote assets and store them in the assets folder
soup = BeautifulSoup(content, "html.parser")
session = requests.Session()
for tag, inner in [("link", "href"), ("script", "src")]:
for item in soup.find_all(tag):
if item.has_attr(inner) and (link := item[inner]).startswith(
"http"
):
asset_name = link.rsplit("/", 1)[1]
asset = session.get(link)
if self.one_file:
# If it is a CSS file, inline it
if tag == "link" and "stylesheet" in item["rel"]:
item.decompose()
style = soup.new_tag("style")
style.string = asset.text
soup.head.append(style)
# If it is a JS file, inline it
elif tag == "script":
item.decompose()
script = soup.new_tag("script")
script.string = asset.text
soup.head.append(script)
else:
raise ValueError(
f"Unable to inline {tag} asset: {link}"
)
else:
full_assets_dir.mkdir(parents=True, exist_ok=True)
with open(full_assets_dir / asset_name, "wb") as asset_file:
asset_file.write(asset.content)
item[inner] = str(assets_dir / asset_name)
content = str(soup)
f.write(content)
@ -476,10 +681,18 @@ class FrameIndex(str, Enum):
first = "first"
last = "last"
def __repr__(self) -> str:
return self.value
class PDF(Converter):
frame_index: FrameIndex = FrameIndex.last
resolution: PositiveFloat = 100.0
frame_index: FrameIndex = Field(
FrameIndex.last,
description="What frame (first or last) is used to represent each slide.",
)
resolution: PositiveFloat = Field(
100.0, description="Image resolution use for saving frames."
)
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def convert_to(self, dest: Path) -> None:
@ -508,12 +721,28 @@ class PDF(Converter):
class PowerPoint(Converter):
left: PositiveInt = 0
top: PositiveInt = 0
width: PositiveInt = 1280
height: PositiveInt = 720
auto_play_media: bool = True
poster_frame_image: Optional[FilePath] = None
left: PositiveInt = Field(
0, description="Horizontal offset where the video is placed from left border."
)
top: PositiveInt = Field(
0, description="Vertical offset where the video is placed from top border."
)
width: PositiveInt = Field(
1280,
description="Width of the slides.\nThis should match the resolution of the presentation.",
)
height: PositiveInt = Field(
720,
description="Height of the slides.\nThis should match the resolution of the presentation.",
)
auto_play_media: bool = Field(
True, description="Automatically play animations when changing slide."
)
poster_frame_image: Optional[FilePath] = Field(
None,
description="Optional image to use when animations are not playing.\n"
"By default, the first frame of each animation is used.\nThis is important to avoid blinking effects between slides.",
)
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def convert_to(self, dest: Path) -> None:
@ -590,22 +819,39 @@ class PowerPoint(Converter):
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a `--show-config` option."""
"""Wrap a function to add a '--show-config' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
def callback(ctx: Context, _param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
to = ctx.params.get("to", "html")
if "to" in ctx.params:
to = ctx.params["to"]
cls = Converter.from_string(to)
elif "dest" in ctx.params:
dest = Path(ctx.params["dest"])
fmt = dest.suffix[1:].lower()
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warning(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = RevealJS
converter = Converter.from_string(to)
if doc := getattr(cls, "__doc__", ""):
click.echo(textwrap.dedent(doc))
for key, field in converter.model_fields.items():
for key, field in cls.model_fields.items():
if field.is_required():
continue
default = field.get_default(call_default_factory=True)
click.echo(f"{key}: {default}")
click.echo(click.style(key, bold=True) + f": {default}")
if description := field.description:
click.secho(textwrap.indent(description, prefix="# "), dim=True)
ctx.exit()
@ -621,18 +867,31 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a `--show-template` option."""
"""Wrap a function to add a '--show-template' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
to = ctx.params.get("to", "html")
if "to" in ctx.params:
to = ctx.params["to"]
cls = Converter.from_string(to)
elif "dest" in ctx.params:
dest = Path(ctx.params["dest"])
fmt = dest.suffix[1:].lower()
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warning(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = RevealJS
template = ctx.params.get("template", None)
converter = Converter.from_string(to)(
presentation_configs=[PresentationConfig()], template=template
)
converter = cls(presentation_configs=[], template=template)
click.echo(converter.load_template())
ctx.exit()
@ -666,7 +925,6 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
is_flag=True,
help="Open the newly created file using the appropriate application.",
)
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
@click.option(
"-c",
"--config",
@ -674,7 +932,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. "
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
"E.g., pass '-cslide_number=true' to display slide numbers.",
)
@click.option(
"--use-template",
@ -682,7 +940,19 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
metavar="FILE",
type=click.Path(exists=True, dir_okay=False, path_type=Path),
help="Use the template given by FILE instead of default one. "
"To echo the default template, use ``--show-template``.",
"To echo the default template, use '--show-template'.",
)
@click.option(
"--one-file",
is_flag=True,
help="Embed all local assets (e.g., video files) in the output file. "
"The is a convenient alias to '-cone_file=true'.",
)
@click.option(
"--offline",
is_flag=True,
help="Download any remote content and store it in the assets folder. "
"The is a convenient alias to '-coffline=true'.",
)
@show_template_option
@show_config_options
@ -693,9 +963,10 @@ def convert(
dest: Path,
to: str,
open_result: bool,
force: bool,
config_options: dict[str, str],
template: Optional[Path],
offline: bool,
one_file: bool,
) -> None:
"""Convert SCENE(s) into a given format and writes the result in DEST."""
presentation_configs = get_scenes_presentation_config(scenes, folder)
@ -713,6 +984,35 @@ def convert(
else:
cls = Converter.from_string(to)
if (
one_file
and issubclass(cls, (RevealJS, HtmlZip))
and "one_file" not in config_options
):
config_options["one_file"] = "true"
# Change data_uri to one_file and print a warning if present
if "data_uri" in config_options:
warnings.warn(
"The 'data_uri' configuration option is deprecated and will be "
"removed in the next major version. "
"Use 'one_file' instead.",
DeprecationWarning,
stacklevel=2,
)
config_options["one_file"] = (
config_options["one_file"]
if "one_file" in config_options
else config_options.pop("data_uri")
)
if (
offline
and issubclass(cls, (RevealJS, HtmlZip))
and "offline" not in config_options
):
config_options["offline"] = "true"
converter = cls(
presentation_configs=presentation_configs,
template=template,

View File

@ -125,7 +125,7 @@ class ManimSlidesMagic(Magics): # type: ignore
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
could look as follows::
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true data_uri=true
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true one_file=true
class MySlide(Slide):
def construct(self):
@ -222,17 +222,29 @@ class ManimSlidesMagic(Magics): # type: ignore
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
if embed: # Embedding implies data-uri
kwargs["data_uri"] = "true"
# If data_uri is set, raise a warning
if "data_uri" in kwargs:
logger.warning(
"'data_uri' configuration option is deprecated and will be removed in a future release. "
"Please use 'one_file' instead."
)
kwargs["one_file"] = (
kwargs["one_file"]
if "one_file" in kwargs
else kwargs.pop("data_uri")
)
if embed: # Embedding implies one_file
kwargs["one_file"] = "true"
# TODO: FIXME
# Seems like files are blocked so date-uri is the only working option...
if kwargs.get("data_uri", "false").lower().strip() == "false":
# Seems like files are blocked so one_file is the only working option...
if kwargs.get("one_file", "false").lower().strip() == "false":
logger.warning(
"data_uri option is currently automatically enabled, "
"one_file option is currently automatically enabled, "
"because using local video files does not seem to work properly."
)
kwargs["data_uri"] = "true"
kwargs["one_file"] = "true"
presentation_configs = get_scenes_presentation_config(
[clsname], Path("./slides")

View File

@ -1,7 +1,7 @@
import signal
import sys
from pathlib import Path
from typing import Optional
from typing import Literal, Optional
import click
from click import Context, Parameter
@ -222,8 +222,14 @@ def start_at_callback(
)
@click.option(
"--hide-info-window",
is_flag=True,
help="Hide info window.",
flag_value="always",
help="Hide info window. By default, hide the info window if there is only one screen.",
)
@click.option(
"--show-info-window",
"hide_info_window",
flag_value="never",
help="Force to show info window.",
)
@click.option(
"--info-window-screen",
@ -231,11 +237,13 @@ def start_at_callback(
metavar="NUMBER",
type=int,
default=None,
help="Put info window on the given screen (a.k.a. display).",
help="Put info window on the given screen (a.k.a. display). "
"If there is more than one screen, it will by default put the info window "
"on a different screen than the main player.",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
def present( # noqa: C901
scenes: list[str],
config_path: Path,
folder: Path,
@ -251,7 +259,7 @@ def present(
screen_number: Optional[int],
playback_rate: float,
next_terminates_loop: bool,
hide_info_window: bool,
hide_info_window: Optional[Literal["always", "never"]],
info_window_screen_number: Optional[int],
) -> None:
"""
@ -294,22 +302,36 @@ def present(
app = qapp()
app.setApplicationName("Manim Slides")
screens = app.screens()
def get_screen(number: int) -> Optional[QScreen]:
try:
return app.screens()[number]
return screens[number]
except IndexError:
logger.error(
f"Invalid screen number {number}, "
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
f"allowed values are from 0 to {len(screens)-1} (incl.)"
)
return None
should_hide_info_window = False
if hide_info_window is None:
should_hide_info_window = len(screens) == 1
elif hide_info_window == "always":
should_hide_info_window = True
if should_hide_info_window and info_window_screen_number is not None:
logger.warning(
f"Ignoring `--info-window-screen` because `--hide-info-window` is set to `{hide_info_window}`."
)
if screen_number is not None:
screen = get_screen(screen_number)
else:
screen = None
if info_window_screen_number is not None:
if info_window_screen_number is not None and not should_hide_info_window:
info_window_screen = get_screen(info_window_screen_number)
else:
info_window_screen = None
@ -333,11 +355,11 @@ def present(
screen=screen,
playback_rate=playback_rate,
next_terminates_loop=next_terminates_loop,
hide_info_window=hide_info_window,
hide_info_window=should_hide_info_window,
info_window_screen=info_window_screen,
)
player.show()
player.show(screens)
signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit(app.exec())

View File

@ -28,7 +28,6 @@ class Info(QWidget): # type: ignore[misc]
def __init__(
self,
*,
full_screen: bool,
aspect_ratio_mode: Qt.AspectRatioMode,
screen: Optional[QScreen],
) -> None:
@ -38,9 +37,6 @@ class Info(QWidget): # type: ignore[misc]
self.setScreen(screen)
self.move(screen.geometry().topLeft())
if full_screen:
self.setWindowState(Qt.WindowFullScreen)
layout = QHBoxLayout()
# Current slide view
@ -243,7 +239,6 @@ class Player(QMainWindow): # type: ignore[misc]
self.slide_changed.connect(self.slide_changed_callback)
self.info = Info(
full_screen=full_screen,
aspect_ratio_mode=aspect_ratio_mode,
screen=info_window_screen,
)
@ -484,11 +479,28 @@ class Player(QMainWindow): # type: ignore[misc]
self.info.next_media_player.setSource(url)
self.info.next_media_player.play()
def show(self) -> None:
def show(self, screens: list[QScreen]) -> None:
"""Screens is necessary to prevent the info window from being shown on the same screen as the main window (especially in full screen mode)."""
super().show()
if not self.hide_info_window:
self.info.show()
if len(screens) > 1 and self.isFullScreen():
self.ensure_different_screens(screens)
if self.isFullScreen():
self.info.showFullScreen()
else:
self.info.show()
if (
len(screens) > 1 and self.info.screen() == self.screen()
): # It is better when Qt assigns the location, but if it fails to, this is a fallback
self.ensure_different_screens(screens)
def ensure_different_screens(self, screens: list[QScreen]) -> None:
target_screen = screens[1] if self.screen() == screens[0] else screens[0]
self.info.setScreen(target_screen)
self.info.move(target_screen.geometry().topLeft())
@Slot()
def close(self) -> None:
@ -515,6 +527,9 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def reverse(self) -> None:
if self.playing_reversed_slide and self.current_slide_index >= 1:
self.current_slide_index -= 1
self.load_reversed_slide()
self.preview_next_slide()
@ -535,8 +550,10 @@ class Player(QMainWindow): # type: ignore[misc]
def full_screen(self) -> None:
if self.windowState() == Qt.WindowFullScreen:
self.setWindowState(Qt.WindowNoState)
self.info.setWindowState(Qt.WindowNoState)
else:
self.setWindowState(Qt.WindowFullScreen)
self.info.setWindowState(Qt.WindowFullScreen)
@Slot()
def hide_mouse(self) -> None:

View File

@ -1,7 +1,7 @@
"""
Alias command to either
``manim render [OPTIONS] [ARGS]...`` or
``manimgl [OPTIONS] [ARGS]...``.
``manimgl -w [OPTIONS] [ARGS]...``.
This is especially useful for two reasons:
@ -48,6 +48,6 @@ def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
if ce and gl:
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
if gl:
subprocess.run([sys.executable, "-m", "manimlib", *args])
subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
else:
subprocess.run([sys.executable, "-m", "manim", "render", *args])

View File

@ -1,7 +1,7 @@
__all__ = [
"API_NAME",
"MANIM",
"MANIMGL",
"API_NAME",
"Slide",
"ThreeDSlide",
]

View File

@ -2,7 +2,6 @@ from pathlib import Path
from typing import Any, ClassVar, Optional
from manimlib import Scene, ThreeDCamera
from manimlib.utils.file_ops import get_sorted_integer_files
from .base import BaseSlide
@ -10,42 +9,40 @@ from .base import BaseSlide
class Slide(BaseSlide, Scene): # type: ignore[misc]
def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs.setdefault("file_writer_config", {}).update(
skip_animations=True,
break_into_partial_movies=True,
write_to_movie=True,
subdivide_output=True,
)
kwargs["preview"] = False # Avoid opening a preview window
super().__init__(*args, **kwargs)
@property
def _frame_height(self) -> float:
return self.camera.frame.get_height() # type: ignore
return float(self.camera.get_frame_height())
@property
def _frame_width(self) -> float:
return self.camera.frame.get_width() # type: ignore
return float(self.camera.get_frame_width())
@property
def _background_color(self) -> str:
return self.camera_config["background_color"].hex # type: ignore
rgba = self.camera.background_rgba
r = int(255 * rgba[0])
g = int(255 * rgba[1])
b = int(255 * rgba[2])
if rgba[3] == 1.0:
return f"#{r:02x}{g:02x}{b:02x}"
a = int(255 * rgba[3])
return f"#{r:02x}{g:02x}{b:02x}{a:02x}"
@property
def _resolution(self) -> tuple[int, int]:
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
return self.camera.get_pixel_width(), self.camera.get_pixel_height()
@property
def _partial_movie_files(self) -> list[Path]:
kwargs = {
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return [
Path(file)
for file in get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
]
partial_movie_directory = self.file_writer.partial_movie_directory
extension = self.file_writer.movie_file_extension
return sorted(partial_movie_directory.glob(f"*{extension}"))
@property
def _show_progress_bar(self) -> bool:

View File

@ -19,31 +19,32 @@
<body>
<div class="reveal">
<div class="slides">
{%- for presentation_config in presentation_configs -%}
{% for presentation_config in presentation_configs -%}
{% set outer_loop = loop %}
{%- for slide_config in presentation_config.slides -%}
{%- if data_uri -%}
{%- if one_file -%}
{% set file = file_to_data_uri(slide_config.file) %}
{%- else -%}
{% set file = assets_dir / slide_config.file.name %}
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
{%- endif -%}
<section
data-background-size={{ background_size }}
data-background-color="{{ presentation_config.background_color }}"
data-background-video="{{ file }}"
{% if loop.index == 1 and outer_loop.index == 1 -%}
data-background-video-muted
{%- endif %}
{% if slide_config.loop -%}
data-background-video-loop
{%- endif -%}
{% if slide_config.auto_next -%}
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif -%}>
{% if slide_config.notes != "" -%}
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
</section>
<section
data-background-size={{ background_size }}
data-background-color="{{ presentation_config.background_color }}"
data-background-video="{{ file }}"
{% if loop.index == 1 and outer_loop.index == 1 -%}
data-background-video-muted
{%- endif -%}
{% if slide_config.loop -%}
data-background-video-loop
{%- endif -%}
{% if slide_config.auto_next -%}
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif %}
>
{%- if slide_config.notes != "" -%}
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
</section>
{%- endfor -%}
{%- endfor -%}
</div>
@ -128,6 +129,9 @@
// Flags if we should monitor the hash and change slides accordingly
respondToHashChanges: {{ respond_to_hash_changes }},
// Enable support for jump-to-slide navigation shortcuts
jumpToSlide: {{ jump_to_slide }},
// Push each slide change to the browser history. Implies `hash: true`
history: {{ history }},
@ -316,16 +320,16 @@
hideCursorTime: {{ hide_cursor_time }}
});
{% if data_uri -%}
// Fix found by @t-fritsch on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
{% if one_file -%}
// Fix found by @t-fritsch and @Rapsssito on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
function fixBase64VideoBackground(event) {
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
if (event.currentSlide.getAttribute('data-background-video')) {
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
video = background.querySelector('video'),
sources = video.querySelectorAll('source');
// Analyze all slides backgrounds
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
// Get the slide video and its sources for each background
const video = slide.querySelector('video');
const sources = video.querySelectorAll('source');
// Update the source of the video
sources.forEach((source, i) => {
const src = source.getAttribute('src');
if(src.match(/^data:video.*;base64$/)) {
@ -335,8 +339,8 @@
});
}
}
// Setup base64 videos
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
{%- endif %}
</script>

View File

@ -17,7 +17,8 @@ classifiers = [
"Topic :: Scientific/Engineering",
]
dependencies = [
"av>=9.0.0",
"av>=9.0.0,<14",
"beautifulsoup4>=4.12.3",
"click>=8.1.3",
"click-default-group>=1.2.2",
"jinja2>=3.1.2",
@ -49,6 +50,8 @@ docs = [
"nbsphinx>=0.9.2",
"pandoc>=2.3",
"sphinx>=7.0.1",
"sphinxcontrib-programoutput>=0.18",
"sphinx-design>=0.6.1",
"sphinx-click>=4.4.0",
"sphinx-copybutton>=0.5.1",
"sphinxext-opengraph>=0.7.5",
@ -58,10 +61,10 @@ full = [
]
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.18.0"]
manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
manimgl = ["manimgl>=1.7.2"]
pyqt6 = ["pyqt6>=6.7.0"]
pyqt6-full = ["manim-slides[full,pyqt6]"]
pyside6 = ["pyside6>=6.6.1"]
pyside6 = ["pyside6>=6.6.1,!=6.8.1.1"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
tests = [
@ -87,7 +90,7 @@ Repository = "https://github.com/jeertmans/manim-slides"
allow_dirty = false
commit = true
commit_args = ""
current_version = "5.1.7"
current_version = "5.3.0"
ignore_missing_version = false
message = "chore(deps): bump version from {current_version} to {new_version}"
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
@ -189,7 +192,8 @@ filterwarnings = [
'''ignore:'audioop' is deprecated:DeprecationWarning''',
'ignore:pkg_resources is deprecated as an API:DeprecationWarning',
'ignore::DeprecationWarning:pkg_resources.*:',
'ignore::DeprecationWarning:pydub.*:',
'ignore:invalid escape sequence.*:DeprecationWarning',
'ignore:invalid escape sequence.*:SyntaxWarning',
]
[tool.ruff]
@ -226,9 +230,3 @@ dev-dependencies = [
"pre-commit>=3.5.0",
"setuptools>=73.0.1",
]
override-dependencies = [
# Bypass constraints from ManimGL
"manimpango>=0.5.0,<1.0.0",
"numpy<=1.24;python_version < '3.12'",
"numpy>=1.26;python_version >= '3.12'",
]

View File

@ -39,6 +39,13 @@ def test_checkhealth(
del sys.modules["qtpy"] # Avoid using cached module
with missing_modules(*names):
if (
not manimlib_missing
and not MANIMGL_NOT_INSTALLED
and sys.version_info < (3, 10)
):
pytest.skip("See https://github.com/3b1b/manim/issues/2263")
result = runner.invoke(
checkhealth,
env={"QT_API": "pyqt6", "FORCE_QT_API": "1"},

View File

@ -3,7 +3,8 @@ from enum import EnumMeta
from pathlib import Path
import pytest
from pydantic import ValidationError
import requests
from bs4 import BeautifulSoup
from manim_slides.config import PresentationConfig
from manim_slides.convert import (
@ -156,6 +157,119 @@ class TestConverter:
file_contents = out_file.read_text()
assert "manim" in file_contents.casefold()
def test_revealjs_offline_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
out_file = tmp_path / "slides.html"
RevealJS(presentation_configs=[presentation_config], offline="true").convert_to(
out_file
)
assert out_file.exists()
assets_dir = Path(tmp_path / "slides_assets")
assert assets_dir.is_dir()
for file in [
"black.min.css",
"reveal.min.css",
"reveal.min.js",
"zenburn.min.css",
]:
assert (assets_dir / file).exists()
def test_revealjs_data_encode(
self,
tmp_path: Path,
presentation_config: PresentationConfig,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Mock requests.Session.get to return a fake response (should not be called)
class MockResponse:
def __init__(self, content: bytes, text: str, status_code: int) -> None:
self.content = content
self.text = text
self.status_code = status_code
# Apply the monkeypatch
monkeypatch.setattr(
requests.Session,
"get",
lambda self, url: MockResponse(
b"body { background-color: #9a3241; }",
"body { background-color: #9a3241; }",
200,
),
)
out_file = tmp_path / "slides.html"
RevealJS(
presentation_configs=[presentation_config], offline="false", one_file="true"
).convert_to(out_file)
assert out_file.exists()
# Check that assets are not stored
assert not (tmp_path / "slides_assets").exists()
with open(out_file, encoding="utf-8") as file:
content = file.read()
soup = BeautifulSoup(content, "html.parser")
# Check if video is encoded in base64
videos = soup.find_all("section")
assert all(
"data:video/mp4;base64," in video["data-background-video"]
for video in videos
)
# Check if CSS is not inlined
styles = soup.find_all("style")
assert not any("background-color: #9a3241;" in style.string for style in styles)
# Check if JS is not inlined
scripts = soup.find_all("script")
assert not any(
"background-color: #9a3241;" in (script.string or "") for script in scripts
)
def test_revealjs_offline_inlining(
self,
tmp_path: Path,
presentation_config: PresentationConfig,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Mock requests.Session.get to return a fake response
class MockResponse:
def __init__(self, content: bytes, text: str, status_code: int) -> None:
self.content = content
self.text = text
self.status_code = status_code
# Apply the monkeypatch
monkeypatch.setattr(
requests.Session,
"get",
lambda self, url: MockResponse(
b"body { background-color: #9a3241; }",
"body { background-color: #9a3241; }",
200,
),
)
out_file = tmp_path / "slides.html"
RevealJS(
presentation_configs=[presentation_config], offline="true", one_file="true"
).convert_to(out_file)
assert out_file.exists()
with open(out_file, encoding="utf-8") as file:
content = file.read()
soup = BeautifulSoup(content, "html.parser")
# Check if CSS is inlined
styles = soup.find_all("style")
assert any("background-color: #9a3241;" in style.string for style in styles)
# Check if JS is inlined
scripts = soup.find_all("script")
assert any("background-color: #9a3241;" in script.string for script in scripts)
def test_htmlzip_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
@ -208,10 +322,6 @@ class TestConverter:
).convert_to(out_file)
assert out_file.exists()
def test_converter_no_presentation_config(self) -> None:
with pytest.raises(ValidationError):
Converter(presentation_configs=[])
def test_pptx_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:

View File

@ -1,3 +1,4 @@
import warnings
from pathlib import Path
import pytest
@ -64,6 +65,34 @@ def test_convert(slides_folder: Path, extension: str) -> None:
assert results.exit_code == 0
@pytest.mark.parametrize(("extension",), [("html",)])
def test_convert_data_uri_deprecated(slides_folder: Path, extension: str) -> None:
runner = CliRunner(mix_stderr=False)
with runner.isolated_filesystem():
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
results = runner.invoke(
cli,
[
"convert",
"BasicSlide",
f"basic_example.{extension}",
"--folder",
str(slides_folder),
"--to",
extension,
"-cdata_uri=true",
],
)
assert any(
"'data_uri' configuration option is deprecated" in str(item.message)
and item.category is DeprecationWarning
for item in w
)
assert results.exit_code == 0
@pytest.mark.parametrize(
("extension", "expected_log"),
[("html", ""), ("pdf", ""), ("pptx", ""), ("ppt", "WARNING")],

View File

@ -6,6 +6,11 @@ import pytest
import manim_slides.slide as slide
skip_if_py39 = pytest.mark.skipif(
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263",
)
def assert_import(
*,
@ -20,6 +25,7 @@ def assert_import(
assert slide.MANIMGL == manimgl
@skip_if_py39
def test_force_api() -> None:
pytest.importorskip("manimlib")
import manim # noqa: F401
@ -53,6 +59,7 @@ def test_invalid_api() -> None:
del os.environ[slide.MANIM_API]
@skip_if_py39
@pytest.mark.filterwarnings("ignore:assert_import")
def test_manim_and_manimgl_imported() -> None:
pytest.importorskip("manimlib")
@ -79,6 +86,7 @@ def test_manim_imported() -> None:
)
@skip_if_py39
def test_manimgl_imported() -> None:
pytest.importorskip("manimlib")
import manimlib # noqa: F401

View File

@ -26,23 +26,32 @@ from manim_slides.defaults import FOLDER_PATH
from manim_slides.render import render
from manim_slides.slide.manim import Slide as CESlide
if sys.version_info < (3, 10):
class _GLSlide:
def construct(self) -> None:
pass
def render(self) -> None:
pass
GLSlide = pytest.param(
_GLSlide,
marks=pytest.mark.skip(reason="See https://github.com/3b1b/manim/issues/2263"),
)
else:
from manim_slides.slide.manimlib import Slide as GLSlide
_GLSlide = GLSlide
class CEGLSlide(CESlide):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
if sys.version_info >= (3, 12):
class _GLSlide:
pass
GLSlide = pytest.param(_GLSlide, marks=pytest.mark.skip())
else:
from manim_slides.slide.manimlib import Slide as GLSlide
SlideType = Union[type[CESlide], type[GLSlide], type[CEGLSlide]]
Slide = Union[CESlide, GLSlide, CEGLSlide]
SlideType = Union[type[CESlide], type[_GLSlide], type[CEGLSlide]]
Slide = Union[CESlide, _GLSlide, CEGLSlide]
@pytest.mark.parametrize(
@ -52,8 +61,8 @@ Slide = Union[CESlide, GLSlide, CEGLSlide]
pytest.param(
"--GL",
marks=pytest.mark.skipif(
sys.version_info >= (3, 12),
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263.",
),
),
],
@ -161,8 +170,8 @@ def test_clear_cache(
pytest.param(
"--GL",
marks=pytest.mark.skipif(
sys.version_info >= (3, 12),
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263.",
),
),
],
@ -203,13 +212,10 @@ def init_slide(cls: SlideType) -> Slide:
if issubclass(cls, CESlide):
return cls()
elif issubclass(cls, GLSlide):
from manimlib.config import get_configuration, parse_cli
from manimlib.extract_scene import get_scene_config
from manimlib.config import parse_cli
args = parse_cli()
config = get_configuration(args)
scene_config = get_scene_config(config)
return cls(**scene_config)
_args = parse_cli()
return cls()
raise ValueError(f"Unsupported class {cls}")

3102
uv.lock generated

File diff suppressed because it is too large Load Diff