From 838a670b5a27e043da2b92f8bbc4f70374eb6421 Mon Sep 17 00:00:00 2001 From: liwangping Date: Sat, 15 Aug 2026 01:27:09 +0800 Subject: [PATCH 1/2] Squashed 'vendor/term-maths/' content from commit 5a2de3b git-subtree-dir: vendor/term-maths git-subtree-split: 5a2de3b29f1d4cec72c8685b8d52ddfb53519676 --- .github/workflows/ci.yml | 61 + .github/workflows/docs.yml | 74 + .github/workflows/release.yml | 163 ++ .gitignore | 30 + Cargo.lock | 2145 ++++++++++++++++++++++++++ Cargo.toml | 40 + LICENSE-APACHE | 190 +++ LICENSE-MIT | 21 + README.md | 283 ++++ examples/crossterm_demo.rs | 49 + examples/debug_ast.rs | 20 + examples/dsp_equations.rs | 58 + examples/latex_roundtrip.rs | 25 + examples/matrix_demo.rs | 41 + examples/ratatui_demo.rs | 37 + examples/render_demo.rs | 22 + pyproject.toml | 46 + python/docs/Makefile | 14 + python/docs/api.rst | 22 + python/docs/conf.py | 61 + python/docs/examples.rst | 52 + python/docs/index.rst | 50 + python/docs/requirements.txt | 3 + python/examples/block_composition.py | 100 ++ python/examples/dsp_equations.py | 46 + python/examples/math_fonts.py | 66 + python/examples/render_demo.py | 21 + python/term_maths/__init__.py | 33 + python/term_maths/py.typed | 0 src/bin/stub_gen.rs | 16 + src/crossterm_renderer.rs | 36 + src/latex_renderer.rs | 301 ++++ src/layout.rs | 927 +++++++++++ src/lib.rs | 72 + src/mathfont.rs | 194 +++ src/python.rs | 320 ++++ src/ratatui_widget.rs | 50 + src/rendered_block.rs | 352 +++++ src/renderer.rs | 25 + tests/layout_tests.rs | 421 +++++ 40 files changed, 6487 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 examples/crossterm_demo.rs create mode 100644 examples/debug_ast.rs create mode 100644 examples/dsp_equations.rs create mode 100644 examples/latex_roundtrip.rs create mode 100644 examples/matrix_demo.rs create mode 100644 examples/ratatui_demo.rs create mode 100644 examples/render_demo.rs create mode 100644 pyproject.toml create mode 100644 python/docs/Makefile create mode 100644 python/docs/api.rst create mode 100644 python/docs/conf.py create mode 100644 python/docs/examples.rst create mode 100644 python/docs/index.rst create mode 100644 python/docs/requirements.txt create mode 100644 python/examples/block_composition.py create mode 100644 python/examples/dsp_equations.py create mode 100644 python/examples/math_fonts.py create mode 100644 python/examples/render_demo.py create mode 100644 python/term_maths/__init__.py create mode 100644 python/term_maths/py.typed create mode 100644 src/bin/stub_gen.rs create mode 100644 src/crossterm_renderer.rs create mode 100644 src/latex_renderer.rs create mode 100644 src/layout.rs create mode 100644 src/lib.rs create mode 100644 src/mathfont.rs create mode 100644 src/python.rs create mode 100644 src/ratatui_widget.rs create mode 100644 src/rendered_block.rs create mode 100644 src/renderer.rs create mode 100644 tests/layout_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5416738 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo check --all-features + + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test + - run: cargo test --all-features + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - run: cargo clippy --all-features -- -D warnings + + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt -- --check + + doc: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo doc --features ratatui,crossterm,pulldown-latex --no-deps + env: + RUSTDOCFLAGS: -D warnings diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..79f411c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,74 @@ +name: Build and Deploy Documentation + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Create virtual environment + run: | + python -m venv .venv + + - name: Install documentation dependencies + run: | + source .venv/bin/activate + python -m pip install --upgrade pip + pip install maturin + pip install -r python/docs/requirements.txt + + - name: Build and install term-maths package + run: | + source .venv/bin/activate + maturin develop --release --features python + + - name: Build documentation + run: | + source .venv/bin/activate + cd python/docs + make html + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'python/docs/_build/html' + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fca1771 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,163 @@ +name: Release + +# Triggered by pushing a version tag: git tag v1.0.0 && git push --tags +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +permissions: + contents: write # create GitHub Release + id-token: write # PyPI Trusted Publishing (OIDC) + +jobs: + # --------------------------------------------------------------------------- + # Build Python wheels for each platform + # --------------------------------------------------------------------------- + build-wheels: + name: Build wheels (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - name: Add macOS cross-compile targets + if: matrix.os == 'macos-latest' + run: rustup target add x86_64-apple-darwin aarch64-apple-darwin + + - name: Build wheels (Linux — manylinux x86_64 + aarch64) + if: matrix.os == 'ubuntu-latest' + uses: PyO3/maturin-action@v1 + with: + command: build + args: --release --features python --out dist + manylinux: auto # builds manylinux2014-compatible wheels + target: x86_64 + + - name: Build wheels (Linux — manylinux aarch64) + if: matrix.os == 'ubuntu-latest' + uses: PyO3/maturin-action@v1 + with: + command: build + args: --release --features python --out dist + manylinux: auto + target: aarch64 + + - name: Build wheels (macOS universal2) + if: matrix.os == 'macos-latest' + uses: PyO3/maturin-action@v1 + with: + command: build + args: --release --features python --out dist --target universal2-apple-darwin + + - name: Build wheels (Windows x86_64) + if: matrix.os == 'windows-latest' + uses: PyO3/maturin-action@v1 + with: + command: build + args: --release --features python --out dist + + - name: Upload wheel artifacts + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: dist/ + + # --------------------------------------------------------------------------- + # Build the source distribution (sdist) + # --------------------------------------------------------------------------- + build-sdist: + name: Build sdist + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/ + + # --------------------------------------------------------------------------- + # Publish Python package to PyPI (Trusted Publishing — no API key required) + # Configure on PyPI: add this repo as a trusted publisher for term-maths + # --------------------------------------------------------------------------- + publish-pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [build-wheels, build-sdist] + environment: + name: pypi + url: https://pypi.org/p/term-maths + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist/ + merge-multiple: true + + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + # No username/password needed — Trusted Publishing uses OIDC + + # --------------------------------------------------------------------------- + # Publish Rust crate to crates.io + # Requires secret: CARGO_REGISTRY_TOKEN + # --------------------------------------------------------------------------- + publish-crate: + name: Publish to crates.io + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - name: Publish crate + run: cargo publish --no-verify + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + # --------------------------------------------------------------------------- + # Create a GitHub Release with release notes + # --------------------------------------------------------------------------- + github-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [publish-pypi, publish-crate] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist/ + merge-multiple: true + + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist/ + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..521934d --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Rust +/target/ + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg-info/ +*.egg +dist/ +build/ +.venv/ + +# Compiled native extensions (produced by maturin develop) +*.so +*.dylib +*.dll + +# Sphinx build output +python/docs/_build/ + +# Root-level dev notes (design.md, research.md — not for the repo) +/docs/ + +# Editor / OS +*.log +*.tmp +*.bak +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b69dbc8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2145 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "numpy" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94caae805f998a07d33af06e6a3891e38556051b8045c615470a71590e13e78" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "rustc-hash", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-latex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b8bc0583825170e3f560701d966dc2f0e3a16946da371e37d30d3ad7207fb7e" +dependencies = [ + "bumpalo", +] + +[[package]] +name = "pyo3" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pyo3-stub-gen" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca7c2d6e22cba51cc9766b6dee4087218cc445fdf99db62fa4f269e074351b46" +dependencies = [ + "anyhow", + "chrono", + "inventory", + "itertools 0.13.0", + "log", + "maplit", + "num-complex", + "numpy", + "pyo3", + "pyo3-stub-gen-derive", + "serde", + "toml", +] + +[[package]] +name = "pyo3-stub-gen-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee49d727163163a0c6fc3fee4636c8b5c82e1bb868e85cf411be7ae9e4e5b40" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.11.0", + "compact_str", + "hashbrown 0.16.1", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rust-latex-parser" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b0949524549f0f83d8a14c71e9b378c45307a77bb6ea6bb969d1399d5ae134" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "term-maths" +version = "1.0.0" +dependencies = [ + "crossterm", + "pulldown-latex", + "pyo3", + "pyo3-stub-gen", + "ratatui", + "rust-latex-parser", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.11.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools 0.14.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "atomic", + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dc05c24 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "term-maths" +version = "1.0.0" +edition = "2024" +description = "Character-grid mathematical notation renderer for terminals --- LaTeX math to 2D Unicode art" +license = "MIT OR Apache-2.0" +keywords = ["math", "latex", "terminal", "unicode", "rendering"] +categories = ["command-line-interface", "text-processing", "visualization"] +exclude = [ + "python/", + "pyproject.toml", + ".github/", + "docs/", + ".venv/", +] + +[lib] +# rlib for Rust consumers; cdylib for the Python extension (required by maturin) +crate-type = ["rlib", "cdylib"] + +[dependencies] +crossterm = { version = "0.29.0", optional = true } +pulldown-latex = { version = "0.7.1", optional = true } +ratatui = { version = "0.30.0", optional = true } +rust-latex-parser = "0.1.0" +unicode-segmentation = "1.13.2" +unicode-width = "0.2.2" +pyo3 = { version = "0.23", features = ["experimental-inspect", "abi3-py310"], optional = true } +pyo3-stub-gen = { version = "0.7", optional = true } + +[features] +ratatui = ["dep:ratatui"] +crossterm = ["dep:crossterm"] +pulldown-latex = ["dep:pulldown-latex"] +python = ["dep:pyo3", "dep:pyo3-stub-gen"] + +[[bin]] +name = "stub_gen" +path = "src/bin/stub_gen.rs" +required-features = ["python"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..d0c491f --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2026 Jack Geraghty + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..f507e99 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jack Geraghty + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4cbce1 --- /dev/null +++ b/README.md @@ -0,0 +1,283 @@ +# term-maths + +Character-grid mathematical notation renderer for terminals, implemented in Rust. + +Accepts LaTeX math input and renders it as 2D Unicode character art in a terminal. Targets [JuliaMono](https://juliamono.netlify.app/) as the recommended font for full Unicode math symbol coverage. + +Available as both a **Rust crate** and a **Python package** (via PyO3 + Maturin). + +## Rust Usage + +Add to your `Cargo.toml`: + +```toml +[dependencies] +term-maths = "0.1" +``` + +Render a LaTeX expression: + +```rust +let block = term_maths::render(r"\frac{a}{b}"); +println!("{}", block); +``` + +Output: + +```text + a +─── + b +``` + +## Python Usage + +Requires a Rust toolchain and [maturin](https://www.maturin.rs): + +```sh +pip install maturin +maturin develop --features python # from the repo root +``` + +```python +import term_maths + +block = term_maths.render(r"\frac{a}{b}") +print(block) +# a +# ─── +# b + +# Compose blocks side-by-side (baseline-aligned) +lhs = term_maths.render(r"\frac{a}{b}") +rhs = term_maths.render(r"\frac{c}{d}") +sep = term_maths.RenderedBlock.from_text(" = ") +print(lhs.beside(sep).beside(rhs)) + +# Unicode math fonts +print(term_maths.map_str("blackboard", "NZQRC")) # ℕℤℚℝℂ + +# LaTeX round-trip +print(term_maths.to_latex(r"x^2 + y^2")) +``` + +See `python/examples/` for more: `render_demo.py`, `dsp_equations.py`, +`block_composition.py`, and `math_fonts.py`. + +## Rendering Examples + +All output below is produced directly by the library. + +### Fractions and Arithmetic + +```text +--- \frac{a}{b} --- + + a +─── + b + +--- \frac{1}{1+\frac{1}{x}} --- + + 1 +───────── + 1 + 1 + ─── + x + +--- \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} --- + + ──────── + -b ± √b² - 4ac +──────────────── + 2a +``` + +### Superscripts, Subscripts, and Inline Unicode + +```text +x^2 → x² +a_n → aₙ +x_i^2 → x²ᵢ +a + b = c → a + b = c +x^2 + y^2 → x² + y² = z² +``` + +### Big Operators with Limits + +```text +--- \sum_{n=0}^{N-1} --- + +N - 1 + ∑ +n = 0 + +--- \int_{0}^{1} --- + +1 +⌠ +⎮ +⌡ +0 + +--- \prod_{i=1}^{n} --- + + n + ∏ +i = 1 +``` + +### DSP Reference Equations + +```text +--- DFT Summation: X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j \frac{2\pi}{N} kn} --- + + 2π + -j ──── kn + N - 1 N +X[k] = ∑ x[n] ·e + n = 0 + +--- Convolution Integral --- + + ∞ + ⌠ +(f · g)(t) = ⎮ f(τ) g(t - τ) dτ + ⌡ + -∞ + +--- Transfer Function --- + + b₀ + b₁ z⁻¹ + b₂ z⁻² +H(z) = ────────────────────── + 1 + a₁ z⁻¹ + a₂ z⁻² + +--- Hann Window --- + + ⎛ ⎛ 2πn ⎞⎞ +w(n) = 0.5 ⎜1 - cos⎜───────⎟⎟ + ⎝ ⎝ N - 1 ⎠⎠ +``` + +### Matrices + +```text +--- pmatrix --- --- bmatrix --- --- vmatrix --- + +⎛a b⎞ ⎡1 0⎤ │a b│ +⎝c d⎠ ⎣0 1⎦ │c d│ + +--- 3x3 bmatrix --- + +⎡1 2 3⎤ +⎢4 5 6⎥ +⎣7 8 9⎦ + +--- Matrix with fractions --- + +⎛ 1 ⎞ +⎜─── 0 ⎟ +⎜ 2 ⎟ +⎜ 3 ⎟ +⎜ 0 ───⎟ +⎝ 4 ⎠ +``` + +### Delimiters, Sqrt, and Accents + +```text +--- \left(\frac{a}{b}\right) --- + +⎛ a ⎞ +⎜───⎟ +⎝ b ⎠ + +--- \sqrt{\frac{a}{b}} --- + + ─── +│ a +│─── +√ b + +--- \overline{x + y} --- + +‾‾‾‾‾ +x + y +``` + +### Math Fonts (Unicode Mathematical Alphanumeric Symbols) + +```text +\mathbb{R} → ℝ +\mathbb{Z} → ℤ +\mathcal{L} → ℒ +\mathbf{x} → 𝐱 +\mathfrak{g} → 𝔤 +\mathbb{R}^n → ℝⁿ +``` + +## Output Backends + +**Core** (always available): + +- Plain text via `render()` and `Display` +- LaTeX round-trip via `to_latex()` + +**Optional** (feature-gated): + +```toml +[dependencies] +term-maths = { version = "0.1", features = ["crossterm", "ratatui"] } +``` + +| Feature | Backend | Description | +|-------------|---------------------|--------------------------------------------------------| +| `crossterm` | `CrosstermRenderer` | Direct terminal output with cursor positioning | +| `ratatui` | `MathWidget` | TUI widget implementing `ratatui::Widget` | +| `python` | PyO3 extension | Python bindings (`maturin build --features python`) | + +### Crossterm + +```rust +use term_maths::{render, CrosstermRenderer}; + +let block = render(r"\sum_{i=0}^{n} x_i"); +CrosstermRenderer::print_at(&block, 0, 0)?; +``` + +### Ratatui + +```rust +use term_maths::{render, MathWidget}; + +let block = render(r"\frac{a}{b}"); +let widget = MathWidget::new(&block); +widget.render(area, buf); +``` + +### LaTeX Round-Trip + +```rust +let latex = term_maths::to_latex(r"x^2 + y^2"); +// "x^{2} \;+\; y^{2}" +``` + +## Font Recommendation + +For best results, use [JuliaMono](https://juliamono.netlify.app/). It provides complete coverage of: + +- Mathematical Alphanumeric Symbols (U+1D400-U+1D7FF) for bold, italic, script, fraktur, double-struck, sans-serif variants +- Box-drawing and bracket piece characters for delimiters and integrals +- Full Greek alphabet and mathematical operators +- Superscript/subscript digits and letters + +Other monospace fonts will work but may show fallback glyphs for some mathematical symbols. + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT License ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. diff --git a/examples/crossterm_demo.rs b/examples/crossterm_demo.rs new file mode 100644 index 0000000..a5576c4 --- /dev/null +++ b/examples/crossterm_demo.rs @@ -0,0 +1,49 @@ +//! Demonstrates the crossterm renderer backend. +//! +//! Run with: cargo run --example crossterm_demo --features crossterm +//! +//! This example uses cursor positioning to render the equation at a specific +//! location in the terminal. It must be run in a real terminal (not piped). + +#[cfg(feature = "crossterm")] +fn main() -> std::io::Result<()> { + use crossterm::{cursor, execute, tty::IsTty}; + use std::io::{Write, stdout}; + use term_maths::{CrosstermRenderer, render}; + + let block = render(r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"); + + let mut stdout = stdout(); + + if !stdout.is_tty() { + // Fallback: just print via Display when not in a real terminal + println!("Crossterm renderer demo — quadratic formula:\n"); + println!("{}", block); + return Ok(()); + } + + println!("Crossterm renderer demo — quadratic formula:\n"); + + // Reserve vertical space by printing blank lines, then move back up + for _ in 0..block.height() { + println!(); + } + + // Move cursor back to the start of the reserved space + let (col, row) = cursor::position()?; + let start_row = row.saturating_sub(block.height() as u16); + CrosstermRenderer::render_at(&mut stdout, &block, col, start_row)?; + + // Move cursor below the rendered block + execute!(stdout, cursor::MoveTo(0, row))?; + stdout.flush()?; + println!(); + + Ok(()) +} + +#[cfg(not(feature = "crossterm"))] +fn main() { + eprintln!("This example requires the `crossterm` feature."); + eprintln!("Run with: cargo run --example crossterm_demo --features crossterm"); +} diff --git a/examples/debug_ast.rs b/examples/debug_ast.rs new file mode 100644 index 0000000..7d4d8b6 --- /dev/null +++ b/examples/debug_ast.rs @@ -0,0 +1,20 @@ +use rust_latex_parser::parse_equation; + +fn main() { + for expr in std::env::args().skip(1) { + println!("=== {} ===", expr); + println!("{:#?}", parse_equation(&expr)); + println!(); + } + if std::env::args().len() <= 1 { + // Defaults + for expr in [ + r"a + b = c", + r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", + ] { + println!("=== {} ===", expr); + println!("{:#?}", parse_equation(expr)); + println!(); + } + } +} diff --git a/examples/dsp_equations.rs b/examples/dsp_equations.rs new file mode 100644 index 0000000..508de96 --- /dev/null +++ b/examples/dsp_equations.rs @@ -0,0 +1,58 @@ +use term_maths::render; + +fn main() { + let equations = [ + // DFT summation + ( + r"X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j \frac{2\pi}{N} kn}", + "DFT Summation", + ), + // Convolution integral + ( + r"(f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) \, d\tau", + "Convolution Integral", + ), + // Transfer function + ( + r"H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}}", + "Transfer Function", + ), + // Hann window + ( + r"w(n) = 0.5 \left(1 - \cos\left(\frac{2\pi n}{N - 1}\right)\right)", + "Hann Window", + ), + ]; + + for (latex, label) in &equations { + println!("=== {} ===", label); + println!("LaTeX: {}", latex); + println!(); + println!("{}", render(latex)); + println!(); + } + + // Also test individual components + println!("=== Standalone tests ===\n"); + + println!("--- Sum with limits ---"); + println!("{}\n", render(r"\sum_{n=0}^{N-1}")); + + println!("--- Integral with limits ---"); + println!("{}\n", render(r"\int_{0}^{1}")); + + println!("--- Product with limits ---"); + println!("{}\n", render(r"\prod_{i=1}^{n}")); + + println!("--- Delimited fraction ---"); + println!("{}\n", render(r"\left(\frac{a}{b}\right)")); + + println!("--- Overline ---"); + println!("{}\n", render(r"\overline{x + y}")); + + println!("--- Hat ---"); + println!("{}\n", render(r"\hat{x}")); + + println!("--- Sqrt of fraction ---"); + println!("{}\n", render(r"\sqrt{\frac{a}{b}}")); +} diff --git a/examples/latex_roundtrip.rs b/examples/latex_roundtrip.rs new file mode 100644 index 0000000..1d5e947 --- /dev/null +++ b/examples/latex_roundtrip.rs @@ -0,0 +1,25 @@ +//! Demonstrates the LaTeX renderer (round-trip serialisation). +//! +//! Run with: cargo run --example latex_roundtrip + +use term_maths::{render, to_latex}; + +fn main() { + let examples = [ + r"\frac{a}{b}", + r"x^2 + y^2 = z^2", + r"\sum_{i=1}^{n} x_i", + r"\sqrt{b^2 - 4ac}", + r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", + r"\mathbb{R}^n", + ]; + + for latex in &examples { + println!("Original: {}", latex); + let roundtrip = to_latex(latex); + println!("Round-trip: {}", roundtrip.trim()); + println!("Rendered:"); + println!("{}", render(latex)); + println!(); + } +} diff --git a/examples/matrix_demo.rs b/examples/matrix_demo.rs new file mode 100644 index 0000000..8e3c30a --- /dev/null +++ b/examples/matrix_demo.rs @@ -0,0 +1,41 @@ +use term_maths::render; + +fn main() { + let examples = [ + ( + r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", + "2x2 pmatrix", + ), + ( + r"\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}", + "2x2 identity bmatrix", + ), + ( + r"\begin{vmatrix} a & b \\ c & d \end{vmatrix}", + "2x2 determinant", + ), + ( + r"\begin{pmatrix} \frac{1}{2} & 0 \\ 0 & \frac{3}{4} \end{pmatrix}", + "Matrix with fractions", + ), + ( + r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}", + "3x3 bmatrix", + ), + // Math font tests + (r"\mathbb{R}", "Blackboard bold R"), + (r"\mathbb{Z}", "Blackboard bold Z"), + (r"\mathcal{L}", "Calligraphic L"), + (r"\mathbf{x}", "Bold x"), + (r"\mathfrak{g}", "Fraktur g"), + (r"\mathbb{R}^n", "R^n"), + ]; + + for (latex, label) in &examples { + println!("--- {} ---", label); + println!("LaTeX: {}", latex); + println!(); + println!("{}", render(latex)); + println!(); + } +} diff --git a/examples/ratatui_demo.rs b/examples/ratatui_demo.rs new file mode 100644 index 0000000..506d4d5 --- /dev/null +++ b/examples/ratatui_demo.rs @@ -0,0 +1,37 @@ +//! Demonstrates the ratatui widget backend. +//! +//! Run with: cargo run --example ratatui_demo --features ratatui + +#[cfg(feature = "ratatui")] +fn main() { + use ratatui::buffer::Buffer; + use ratatui::layout::Rect; + use ratatui::widgets::Widget; + use term_maths::{MathWidget, render}; + + let block = render(r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"); + + // Create a buffer large enough to hold the rendered block + let area = Rect::new(0, 0, block.width() as u16 + 2, block.height() as u16 + 1); + let mut buf = Buffer::empty(area); + + // Render the widget into the buffer + let widget = MathWidget::new(&block); + widget.render(area, &mut buf); + + // Print the buffer contents (simulating what ratatui would display) + println!("Ratatui widget demo — quadratic formula:\n"); + for y in 0..area.height { + for x in 0..area.width { + let cell = &buf[(x, y)]; + print!("{}", cell.symbol()); + } + println!(); + } +} + +#[cfg(not(feature = "ratatui"))] +fn main() { + eprintln!("This example requires the `ratatui` feature."); + eprintln!("Run with: cargo run --example ratatui_demo --features ratatui"); +} diff --git a/examples/render_demo.rs b/examples/render_demo.rs new file mode 100644 index 0000000..4bd5b9b --- /dev/null +++ b/examples/render_demo.rs @@ -0,0 +1,22 @@ +use term_maths::render; + +fn main() { + let examples = [ + (r"\frac{a}{b}", "Simple fraction"), + (r"\frac{1}{1+\frac{1}{x}}", "Nested fraction"), + (r"x^2", "Superscript"), + (r"a_n", "Subscript"), + (r"x_i^2", "Super + subscript"), + (r"a + b = c", "Sequence"), + (r"e^{i\pi} + 1 = 0", "Euler's identity"), + (r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}", "Quadratic formula"), + ]; + + for (latex, label) in &examples { + println!("--- {} ---", label); + println!("LaTeX: {}", latex); + println!(); + println!("{}", render(latex)); + println!(); + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ad183f5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["maturin>=1.7,<2"] +build-backend = "maturin" + +[project] +name = "term-maths" +dynamic = ["version"] +description = "Character-grid mathematical notation renderer for terminals — LaTeX math to 2D Unicode art" +license = { text = "MIT OR Apache-2.0" } +requires-python = ">=3.10" +readme = "README.md" +keywords = ["math", "latex", "terminal", "unicode", "rendering"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Rust", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Terminals", + "Topic :: Text Processing :: Markup :: LaTeX", +] + +[tool.maturin] +# Enables the `python` Cargo feature when building the extension +features = ["python"] +# The dotted Python module path of the compiled extension. +# maturin places the .so file at python/term_maths/_term_maths.so +module-name = "term_maths._term_maths" +# The directory containing the Python package source +python-source = "python" +manifest-path = "Cargo.toml" +# Exclude from sdist: build artefacts, dev tools, and internal notes +exclude = [ + "target/", + ".venv/", + "docs/", + "python/docs/_build/", + ".github/", +] diff --git a/python/docs/Makefile b/python/docs/Makefile new file mode 100644 index 0000000..31b02ab --- /dev/null +++ b/python/docs/Makefile @@ -0,0 +1,14 @@ +# Minimal Sphinx Makefile + +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +.PHONY: help Makefile + +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/python/docs/api.rst b/python/docs/api.rst new file mode 100644 index 0000000..b5ba485 --- /dev/null +++ b/python/docs/api.rst @@ -0,0 +1,22 @@ +API Reference +============= + +.. automodule:: term_maths + :members: + :undoc-members: + :special-members: __str__, __repr__ + :show-inheritance: + +.. rubric:: Functions + +.. autofunction:: term_maths.render +.. autofunction:: term_maths.to_latex +.. autofunction:: term_maths.map_char +.. autofunction:: term_maths.map_str + +.. rubric:: Classes + +.. autoclass:: term_maths.RenderedBlock + :members: + :undoc-members: + :special-members: __str__, __repr__ diff --git a/python/docs/conf.py b/python/docs/conf.py new file mode 100644 index 0000000..370b50b --- /dev/null +++ b/python/docs/conf.py @@ -0,0 +1,61 @@ +# Configuration file for the Sphinx documentation builder. +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os +import sys + +# Make the term_maths package importable. +# The compiled extension (_term_maths.so) must be installed first: +# pip install -e . (from repo root, with maturin installed) +# or: +# maturin develop --features python +sys.path.insert(0, os.path.abspath("../../python")) + +# --------------------------------------------------------------------------- +# Project information +# --------------------------------------------------------------------------- + +project = "term-maths" +author = "Jack Geraghty" +copyright = f"2024, {author}" +release = "0.1.0" + +# --------------------------------------------------------------------------- +# General configuration +# --------------------------------------------------------------------------- + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", # NumPy / Google-style docstrings + "sphinx_autodoc_typehints", # type hints from annotations / stubs + "sphinx.ext.viewcode", +] + +# sphinx-autodoc-typehints settings +always_document_param_types = True +typehints_fully_qualified = False +simplify_optional_unions = True + +# autodoc settings +autoclass_content = "both" # include both class and __init__ docstrings +autodoc_typehints = "description" # render type hints in the description, not signature +autodoc_member_order = "bysource" + +# --------------------------------------------------------------------------- +# HTML output +# --------------------------------------------------------------------------- + +html_theme = "furo" +html_title = "term-maths" +html_theme_options = { + "source_repository": "https://github.com/jmg049/term-maths", + "source_branch": "main", + "source_directory": "python/docs/", +} + +# --------------------------------------------------------------------------- +# Source files +# --------------------------------------------------------------------------- + +templates_path = ["_templates"] +exclude_patterns = ["_build"] diff --git a/python/docs/examples.rst b/python/docs/examples.rst new file mode 100644 index 0000000..8b22a39 --- /dev/null +++ b/python/docs/examples.rst @@ -0,0 +1,52 @@ +Examples +======== + +All examples are in ``python/examples/`` and can be run after installing the package: + +.. code-block:: sh + + maturin develop --features python + python python/examples/render_demo.py + + +Render demo +----------- + +Basic rendering of common mathematical expressions. + +.. literalinclude:: ../examples/render_demo.py + :language: python + :caption: python/examples/render_demo.py + +DSP equations +------------- + +Signal-processing formulae including the DFT, convolution integral, IIR transfer +function, and Hann window. + +.. literalinclude:: ../examples/dsp_equations.py + :language: python + :caption: python/examples/dsp_equations.py + +Block composition +----------------- + +Demonstrates how to combine :class:`~term_maths.RenderedBlock` objects using +:meth:`~term_maths.RenderedBlock.beside`, :meth:`~term_maths.RenderedBlock.pad`, +:meth:`~term_maths.RenderedBlock.center_in`, :meth:`~term_maths.RenderedBlock.above`, +and :meth:`~term_maths.RenderedBlock.hline`. + +.. literalinclude:: ../examples/block_composition.py + :language: python + :caption: python/examples/block_composition.py + +Unicode math fonts +------------------ + +Shows :func:`~term_maths.map_char` and :func:`~term_maths.map_str` in action across +all supported font styles: bold, blackboard (double-struck), calligraphic, fraktur, +roman, sans-serif, and monospace. + +.. literalinclude:: ../examples/math_fonts.py + :language: python + :caption: python/examples/math_fonts.py diff --git a/python/docs/index.rst b/python/docs/index.rst new file mode 100644 index 0000000..e488e61 --- /dev/null +++ b/python/docs/index.rst @@ -0,0 +1,50 @@ +term-maths Python API +===================== + +**term-maths** renders LaTeX math expressions as 2D Unicode art for terminals. + +.. code-block:: python + + import term_maths + + block = term_maths.render(r"\frac{a}{b}") + print(block) + # a + # ─── + # b + + # Compose blocks + lhs = term_maths.render(r"x^2") + rhs = term_maths.render(r"y^2") + sep = term_maths.RenderedBlock.from_text(" + ") + combined = lhs.beside(sep).beside(rhs) + print(combined) + + # Unicode math fonts + print(term_maths.map_str("blackboard", "NZQRC")) # ℕℤℚℝℂ + +Contents +-------- + +.. toctree:: + :maxdepth: 2 + + api + examples + +Installation +------------ + +Requires `maturin `_ and a Rust toolchain. + +.. code-block:: sh + + pip install maturin + maturin develop --features python # from the repo root + + +Indices +------- + +* :ref:`genindex` +* :ref:`modindex` diff --git a/python/docs/requirements.txt b/python/docs/requirements.txt new file mode 100644 index 0000000..c4d1136 --- /dev/null +++ b/python/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=7.0 +furo>=2024.0 +sphinx-autodoc-typehints>=2.0 diff --git a/python/examples/block_composition.py b/python/examples/block_composition.py new file mode 100644 index 0000000..7c7e0e8 --- /dev/null +++ b/python/examples/block_composition.py @@ -0,0 +1,100 @@ +""" +Block composition demo. + +Shows how to build composite expressions by combining RenderedBlock objects +using beside(), pad(), center_in(), above(), and hline(). +""" + +import term_maths +from term_maths import RenderedBlock + + +def sep(text: str = " ") -> RenderedBlock: + """Convenience: create a separator block from plain text.""" + return RenderedBlock.from_text(text) + + +# --------------------------------------------------------------------------- +# 1. Side-by-side composition aligned on baselines +# --------------------------------------------------------------------------- +print("=== Side-by-side (baseline-aligned) ===\n") + +lhs = term_maths.render(r"\frac{a}{b}") +eq = sep(" = ") +rhs = term_maths.render(r"\frac{c}{d}") + +print(lhs.beside(eq).beside(rhs)) +print() + +# A tall block beside a short one — short block sits on the baseline +tall = term_maths.render(r"\frac{1}{1 + \frac{1}{x}}") +plus = sep(" + ") +short = term_maths.render(r"y") + +print(tall.beside(plus).beside(short)) +print() + +# --------------------------------------------------------------------------- +# 2. Horizontal centering under a fraction bar +# --------------------------------------------------------------------------- +print("=== Manual fraction construction ===\n") + +numerator = term_maths.render(r"a + b") +denominator = term_maths.render(r"c + d") +bar_width = max(numerator.width, denominator.width) + 2 +bar = RenderedBlock.hline("─", bar_width) + +num_c = numerator.center_in(bar_width) +den_c = denominator.center_in(bar_width) + +# Stack: numerator / bar / denominator; baseline is the bar row +fraction = RenderedBlock.above(num_c, bar, baseline_row=num_c.height) +fraction = RenderedBlock.above(fraction, den_c, baseline_row=num_c.height) + +print(fraction) +print() + +# --------------------------------------------------------------------------- +# 3. Padding and alignment +# --------------------------------------------------------------------------- +print("=== Padding ===\n") + +block = term_maths.render(r"x^2 + y^2") +padded = block.pad(left=2, right=2, top=1, bottom=1) +print(f"Original ({block.width}×{block.height}):") +print(block) +print(f"\nPadded ({padded.width}×{padded.height}):") +print(padded) +print() + +# --------------------------------------------------------------------------- +# 4. Accessing cells programmatically +# --------------------------------------------------------------------------- +print("=== Cell grid access ===\n") + +block = term_maths.render(r"\frac{1}{2}") +print(f"RenderedBlock: width={block.width}, height={block.height}, baseline={block.baseline}") +print(f"repr: {block!r}") +print() + +cells = block.cells() +for row_idx, row in enumerate(cells): + marker = " <-- baseline" if row_idx == block.baseline else "" + print(f" row {row_idx}: {row}{marker}") +print() + +# --------------------------------------------------------------------------- +# 5. Building a table of expressions +# --------------------------------------------------------------------------- +print("=== Expression table ===\n") + +expressions = [ + r"\sum_{k=0}^{n} k", + r"\frac{n(n+1)}{2}", +] + +blocks = [term_maths.render(e) for e in expressions] +eq_sep = sep(" = ") +composed = blocks[0].beside(eq_sep).beside(blocks[1]) +print(composed) +print() diff --git a/python/examples/dsp_equations.py b/python/examples/dsp_equations.py new file mode 100644 index 0000000..b7f3e27 --- /dev/null +++ b/python/examples/dsp_equations.py @@ -0,0 +1,46 @@ +"""DSP equations demo — Python equivalent of examples/dsp_equations.rs.""" + +import term_maths + +EQUATIONS = [ + ( + r"X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j \frac{2\pi}{N} kn}", + "DFT Summation", + ), + ( + r"(f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) \, d\tau", + "Convolution Integral", + ), + ( + r"H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}}", + "Transfer Function", + ), + ( + r"w(n) = 0.5 \left(1 - \cos\left(\frac{2\pi n}{N - 1}\right)\right)", + "Hann Window", + ), +] + +for latex, label in EQUATIONS: + print(f"=== {label} ===") + print(f"LaTeX: {latex}") + print() + print(term_maths.render(latex)) + print() + +print("=== Standalone operators ===\n") + +standalone = [ + (r"\sum_{n=0}^{N-1}", "Sum with limits"), + (r"\int_{0}^{1}", "Integral with limits"), + (r"\prod_{i=1}^{n}", "Product with limits"), + (r"\left(\frac{a}{b}\right)", "Delimited fraction"), + (r"\overline{x + y}", "Overline"), + (r"\hat{x}", "Hat"), + (r"\sqrt{\frac{a}{b}}", "Sqrt of fraction"), +] + +for latex, label in standalone: + print(f"--- {label} ---") + print(term_maths.render(latex)) + print() diff --git a/python/examples/math_fonts.py b/python/examples/math_fonts.py new file mode 100644 index 0000000..3f53251 --- /dev/null +++ b/python/examples/math_fonts.py @@ -0,0 +1,66 @@ +""" +Unicode mathematical font demo. + +Shows how map_char() and map_str() transform ASCII letters and digits into +their Unicode Mathematical Alphanumeric Symbols equivalents. +""" + +import term_maths + +FONTS = ["bold", "blackboard", "calligraphic", "fraktur", "roman", "sans_serif", "monospace"] + +ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +print("=== Font map (uppercase A–Z) ===\n") +sample = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +for font in FONTS: + mapped = term_maths.map_str(font, sample) + print(f" {font:<12}: {mapped}") + +print() + +print("=== Font map (lowercase a–z) ===\n") +sample = "abcdefghijklmnopqrstuvwxyz" +for font in FONTS: + mapped = term_maths.map_str(font, sample) + print(f" {font:<12}: {mapped}") + +print() + +print("=== Font map (digits 0–9) ===\n") +sample = "0123456789" +for font in FONTS: + mapped = term_maths.map_str(font, sample) + print(f" {font:<12}: {mapped}") + +print() + +print("=== Common mathematical sets ===\n") +sets = { + "Naturals ℕ": ("blackboard", "N"), + "Integers ℤ": ("blackboard", "Z"), + "Rationals ℚ": ("blackboard", "Q"), + "Reals ℝ": ("blackboard", "R"), + "Complex ℂ": ("blackboard", "C"), +} +for label, (font, ch) in sets.items(): + print(f" {label} → {term_maths.map_char(font, ch)}") + +print() + +print("=== Rendered with \\mathbb (via LaTeX parser) ===\n") +for expr, label in [ + (r"\mathbb{NZQRC}", "Common sets"), + (r"\mathbf{v}", "Bold vector"), + (r"\mathcal{L}", "Calligraphic L (Laplace)"), + (r"\mathfrak{g}", "Fraktur g (Lie algebra)"), +]: + print(f" {label}: {term_maths.render(expr)}") + +print() + +print("=== Error handling ===\n") +try: + term_maths.map_str("unknown_font", "hello") +except ValueError as e: + print(f" ValueError: {e}") diff --git a/python/examples/render_demo.py b/python/examples/render_demo.py new file mode 100644 index 0000000..631310e --- /dev/null +++ b/python/examples/render_demo.py @@ -0,0 +1,21 @@ +"""Basic rendering demo — Python equivalent of examples/render_demo.rs.""" + +import term_maths + +EXAMPLES = [ + (r"\frac{a}{b}", "Simple fraction"), + (r"\frac{1}{1+\frac{1}{x}}", "Nested fraction"), + (r"x^2", "Superscript"), + (r"a_n", "Subscript"), + (r"x_i^2", "Super + subscript"), + (r"a + b = c", "Sequence"), + (r"e^{i\pi} + 1 = 0", "Euler's identity"), + (r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}", "Quadratic formula"), +] + +for latex, label in EXAMPLES: + print(f"--- {label} ---") + print(f"LaTeX: {latex}") + print() + print(term_maths.render(latex)) + print() diff --git a/python/term_maths/__init__.py b/python/term_maths/__init__.py new file mode 100644 index 0000000..78e7535 --- /dev/null +++ b/python/term_maths/__init__.py @@ -0,0 +1,33 @@ +""" +term_maths — Character-grid mathematical notation renderer. + +Renders LaTeX math expressions as 2D Unicode art suitable for terminal display. + +Quick start:: + + >>> import term_maths + >>> print(term_maths.render(r"\\frac{a}{b}")) + a + ─── + b + +The :class:`RenderedBlock` returned by :func:`render` can be composed further +using methods like :meth:`~RenderedBlock.beside`, :meth:`~RenderedBlock.pad`, +and :meth:`~RenderedBlock.center_in`. +""" + +from ._term_maths import ( + RenderedBlock, + render, + to_latex, + map_char, + map_str, +) + +__all__ = [ + "RenderedBlock", + "render", + "to_latex", + "map_char", + "map_str", +] diff --git a/python/term_maths/py.typed b/python/term_maths/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/bin/stub_gen.rs b/src/bin/stub_gen.rs new file mode 100644 index 0000000..c5e605b --- /dev/null +++ b/src/bin/stub_gen.rs @@ -0,0 +1,16 @@ +//! Generates Python type stub files (.pyi) for the term_maths extension module. +//! +//! Run with: +//! +//! ```sh +//! cargo run --features python --bin stub_gen +//! ``` +//! +//! The stubs are written to `python/term_maths/_term_maths.pyi` (relative to +//! the workspace root). The output path is determined automatically by +//! pyo3-stub-gen by scanning upward for `pyproject.toml`. + +fn main() { + let stub = term_maths::python::stub_info_gatherer().expect("Failed to collect stub info"); + stub.generate().expect("Failed to generate Python stubs"); +} diff --git a/src/crossterm_renderer.rs b/src/crossterm_renderer.rs new file mode 100644 index 0000000..b20def4 --- /dev/null +++ b/src/crossterm_renderer.rs @@ -0,0 +1,36 @@ +//! Crossterm output backend — writes a `RenderedBlock` to the terminal +//! at a specified cursor position. +//! +//! Feature-gated behind `crossterm`. + +use std::io::Write; + +use crossterm::{cursor::MoveTo, execute, style::Print}; + +use crate::rendered_block::RenderedBlock; + +/// Renders a `RenderedBlock` to a terminal writer using crossterm commands. +pub struct CrosstermRenderer; + +impl CrosstermRenderer { + /// Write a rendered block to the terminal at the given (col, row) position. + pub fn render_at( + writer: &mut W, + block: &RenderedBlock, + col: u16, + row: u16, + ) -> std::io::Result<()> { + for (r, cells) in block.cells().iter().enumerate() { + execute!(writer, MoveTo(col, row + r as u16))?; + let line: String = cells.iter().map(|s| s.as_str()).collect(); + execute!(writer, Print(&line))?; + } + Ok(()) + } + + /// Write a rendered block to stdout at the given position. + pub fn print_at(block: &RenderedBlock, col: u16, row: u16) -> std::io::Result<()> { + let mut stdout = std::io::stdout(); + Self::render_at(&mut stdout, block, col, row) + } +} diff --git a/src/latex_renderer.rs b/src/latex_renderer.rs new file mode 100644 index 0000000..dd770c1 --- /dev/null +++ b/src/latex_renderer.rs @@ -0,0 +1,301 @@ +//! LaTeX renderer — serialises an `EqNode` AST back to a LaTeX string. + +use rust_latex_parser::{AccentKind, EqNode, MathFontKind, MatrixKind}; + +use crate::renderer::MathRenderer; + +/// Serialises an `EqNode` back to a LaTeX math string. +pub struct LatexRenderer; + +impl MathRenderer for LatexRenderer { + type Output = String; + + fn render(&self, node: &EqNode) -> String { + node_to_latex(node) + } +} + +fn node_to_latex(node: &EqNode) -> String { + match node { + EqNode::Text(s) => latex_escape_text(s), + EqNode::Space(pts) => space_to_latex(*pts), + EqNode::Seq(children) => children.iter().map(node_to_latex).collect(), + EqNode::Frac(num, den) => { + format!(r"\frac{{{}}}{{{}}}", node_to_latex(num), node_to_latex(den)) + } + EqNode::Sup(base, sup) => { + format!("{}^{{{}}}", node_to_latex(base), node_to_latex(sup)) + } + EqNode::Sub(base, sub) => { + format!("{}_{{{}}} ", node_to_latex(base), node_to_latex(sub)) + } + EqNode::SupSub(base, sup, sub) => { + format!( + "{}^{{{}}}_{{{}}}", + node_to_latex(base), + node_to_latex(sup), + node_to_latex(sub) + ) + } + EqNode::Sqrt(body) => format!(r"\sqrt{{{}}}", node_to_latex(body)), + EqNode::BigOp { + symbol, + lower, + upper, + } => { + let sym = unicode_to_latex_op(symbol); + let mut s = sym; + if let Some(lo) = lower { + s.push_str(&format!("_{{{}}}", node_to_latex(lo))); + } + if let Some(up) = upper { + s.push_str(&format!("^{{{}}}", node_to_latex(up))); + } + s + } + EqNode::Accent(body, kind) => { + let cmd = match kind { + AccentKind::Hat => r"\hat", + AccentKind::Bar => r"\overline", + AccentKind::Dot => r"\dot", + AccentKind::DoubleDot => r"\ddot", + AccentKind::Tilde => r"\tilde", + AccentKind::Vec => r"\vec", + }; + format!("{}{{{}}}", cmd, node_to_latex(body)) + } + EqNode::Limit { name, lower } => { + let latex_name = format!(r"\{}", name); + if let Some(lo) = lower { + format!("{}_{{{}}}", latex_name, node_to_latex(lo)) + } else { + latex_name + } + } + EqNode::TextBlock(s) => format!(r"\text{{{}}}", s), + EqNode::MathFont { kind, content } => { + let cmd = match kind { + MathFontKind::Bold => r"\mathbf", + MathFontKind::Blackboard => r"\mathbb", + MathFontKind::Calligraphic => r"\mathcal", + MathFontKind::Roman => r"\mathrm", + MathFontKind::Fraktur => r"\mathfrak", + MathFontKind::SansSerif => r"\mathsf", + MathFontKind::Monospace => r"\mathtt", + }; + format!("{}{{{}}}", cmd, node_to_latex(content)) + } + EqNode::Delimited { + left, + right, + content, + } => { + format!( + r"\left{} {} \right{}", + latex_delim(left), + node_to_latex(content), + latex_delim(right) + ) + } + EqNode::Matrix { kind, rows } => { + let env = match kind { + MatrixKind::Plain => "matrix", + MatrixKind::Paren => "pmatrix", + MatrixKind::Bracket => "bmatrix", + MatrixKind::Brace => "Bmatrix", + MatrixKind::VBar => "vmatrix", + MatrixKind::DoubleVBar => "Vmatrix", + }; + let rows_str: Vec = rows + .iter() + .map(|row| { + row.iter() + .map(node_to_latex) + .collect::>() + .join(" & ") + }) + .collect(); + format!( + r"\begin{{{}}} {} \end{{{}}}", + env, + rows_str.join(r" \\ "), + env + ) + } + EqNode::Cases { rows } => { + let rows_str: Vec = rows + .iter() + .map(|(val, cond)| { + if let Some(c) = cond { + format!("{} & {}", node_to_latex(val), node_to_latex(c)) + } else { + node_to_latex(val) + } + }) + .collect(); + format!(r"\begin{{cases}} {} \end{{cases}}", rows_str.join(r" \\ ")) + } + EqNode::Binom(top, bottom) => { + format!( + r"\binom{{{}}}{{{}}}", + node_to_latex(top), + node_to_latex(bottom) + ) + } + EqNode::Brace { + content, + label, + over, + } => { + let cmd = if *over { r"\overbrace" } else { r"\underbrace" }; + let mut s = format!("{}{{{}}}", cmd, node_to_latex(content)); + if let Some(lbl) = label { + if *over { + s.push_str(&format!("^{{{}}}", node_to_latex(lbl))); + } else { + s.push_str(&format!("_{{{}}}", node_to_latex(lbl))); + } + } + s + } + EqNode::StackRel { + base, + annotation, + over, + } => { + let cmd = if *over { r"\overset" } else { r"\underset" }; + format!( + "{}{{{}}}{{{}}}", + cmd, + node_to_latex(annotation), + node_to_latex(base) + ) + } + } +} + +/// Escape special LaTeX characters in text content. +fn latex_escape_text(s: &str) -> String { + // Map common Unicode back to LaTeX commands + let mut result = String::new(); + for ch in s.chars() { + match ch { + 'α' => result.push_str(r"\alpha "), + 'β' => result.push_str(r"\beta "), + 'γ' => result.push_str(r"\gamma "), + 'δ' => result.push_str(r"\delta "), + 'ε' => result.push_str(r"\epsilon "), + 'ζ' => result.push_str(r"\zeta "), + 'η' => result.push_str(r"\eta "), + 'θ' => result.push_str(r"\theta "), + 'ι' => result.push_str(r"\iota "), + 'κ' => result.push_str(r"\kappa "), + 'λ' => result.push_str(r"\lambda "), + 'μ' => result.push_str(r"\mu "), + 'ν' => result.push_str(r"\nu "), + 'ξ' => result.push_str(r"\xi "), + 'π' => result.push_str(r"\pi "), + 'ρ' => result.push_str(r"\rho "), + 'σ' => result.push_str(r"\sigma "), + 'τ' => result.push_str(r"\tau "), + 'υ' => result.push_str(r"\upsilon "), + 'φ' => result.push_str(r"\phi "), + 'χ' => result.push_str(r"\chi "), + 'ψ' => result.push_str(r"\psi "), + 'ω' => result.push_str(r"\omega "), + '∞' => result.push_str(r"\infty "), + '∑' => result.push_str(r"\sum "), + '∏' => result.push_str(r"\prod "), + '∫' => result.push_str(r"\int "), + '±' => result.push_str(r"\pm "), + '·' => result.push_str(r"\cdot "), + '→' => result.push_str(r"\rightarrow "), + '←' => result.push_str(r"\leftarrow "), + '≤' => result.push_str(r"\leq "), + '≥' => result.push_str(r"\geq "), + '≠' => result.push_str(r"\neq "), + '∈' => result.push_str(r"\in "), + '∀' => result.push_str(r"\forall "), + '∃' => result.push_str(r"\exists "), + '∂' => result.push_str(r"\partial "), + '∇' => result.push_str(r"\nabla "), + _ => result.push(ch), + } + } + result +} + +fn space_to_latex(pts: f32) -> String { + if pts < 0.0 { + r"\!".to_string() + } else if pts < 3.0 { + r"\,".to_string() + } else if pts < 5.0 { + r"\;".to_string() + } else if pts >= 18.0 { + r"\quad ".to_string() + } else { + " ".to_string() + } +} + +fn unicode_to_latex_op(symbol: &str) -> String { + match symbol { + "∑" => r"\sum".to_string(), + "∏" => r"\prod".to_string(), + "∫" => r"\int".to_string(), + "∬" => r"\iint".to_string(), + "∮" => r"\oint".to_string(), + "⋃" => r"\bigcup".to_string(), + "⋂" => r"\bigcap".to_string(), + "⊕" => r"\bigoplus".to_string(), + "⊗" => r"\bigotimes".to_string(), + _ => symbol.to_string(), + } +} + +fn latex_delim(d: &str) -> String { + match d { + "." => ".".to_string(), + "(" | ")" | "[" | "]" | "|" => d.to_string(), + "{" => r"\{".to_string(), + "}" => r"\}".to_string(), + "‖" => r"\|".to_string(), + _ => d.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::renderer::MathRenderer; + use rust_latex_parser::parse_equation; + + #[test] + fn test_simple_fraction_roundtrip() { + let renderer = LatexRenderer; + let ast = parse_equation(r"\frac{a}{b}"); + let latex = renderer.render(&ast); + assert!(latex.contains(r"\frac")); + assert!(latex.contains('a')); + assert!(latex.contains('b')); + } + + #[test] + fn test_superscript_roundtrip() { + let renderer = LatexRenderer; + let ast = parse_equation(r"x^2"); + let latex = renderer.render(&ast); + assert!(latex.contains("x^")); + assert!(latex.contains('2')); + } + + #[test] + fn test_matrix_roundtrip() { + let renderer = LatexRenderer; + let ast = parse_equation(r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}"); + let latex = renderer.render(&ast); + assert!(latex.contains("pmatrix")); + assert!(latex.contains('&')); + } +} diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..e72ed0f --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,927 @@ +use rust_latex_parser::{AccentKind, EqNode, MathFontKind, MatrixKind}; + +use crate::mathfont; +use crate::rendered_block::RenderedBlock; + +/// Render an `EqNode` AST into a `RenderedBlock`. +pub fn layout(node: &EqNode) -> RenderedBlock { + match node { + EqNode::Text(s) => layout_text(s), + EqNode::Space(pts) => layout_space(*pts), + EqNode::Seq(children) => layout_seq(children), + EqNode::Frac(num, den) => layout_frac(num, den), + EqNode::Sup(base, sup) => layout_sup(base, sup), + EqNode::Sub(base, sub) => layout_sub(base, sub), + EqNode::SupSub(base, sup, sub) => layout_supsub(base, sup, sub), + EqNode::Sqrt(body) => layout_sqrt(body), + EqNode::BigOp { + symbol, + lower, + upper, + } => layout_bigop(symbol, lower, upper), + EqNode::Accent(body, kind) => layout_accent(body, kind), + EqNode::Limit { name, lower } => layout_limit(name, lower), + EqNode::TextBlock(s) => RenderedBlock::from_text(s), + EqNode::MathFont { kind, content } => layout_mathfont(kind, content), + EqNode::Delimited { + left, + right, + content, + } => layout_delimited(left, right, content), + EqNode::Matrix { kind, rows } => layout_matrix(kind, rows), + EqNode::Cases { rows } => layout_cases(rows), + EqNode::Binom(top, bottom) => layout_binom(top, bottom), + EqNode::Brace { + content, + label, + over, + } => layout_brace(content, label, over), + EqNode::StackRel { + base, + annotation, + over, + } => layout_stackrel(base, annotation, over), + } +} + +fn layout_text(s: &str) -> RenderedBlock { + RenderedBlock::from_text(s) +} + +/// Map a character to its Unicode superscript equivalent, if one exists. +fn to_superscript_char(ch: char) -> Option { + match ch { + '0' => Some('⁰'), + '1' => Some('¹'), + '2' => Some('²'), + '3' => Some('³'), + '4' => Some('⁴'), + '5' => Some('⁵'), + '6' => Some('⁶'), + '7' => Some('⁷'), + '8' => Some('⁸'), + '9' => Some('⁹'), + '+' => Some('⁺'), + '-' => Some('⁻'), + '=' => Some('⁼'), + '(' => Some('⁽'), + ')' => Some('⁾'), + 'n' => Some('ⁿ'), + 'i' => Some('ⁱ'), + _ => None, + } +} + +/// Map a character to its Unicode subscript equivalent, if one exists. +fn to_subscript_char(ch: char) -> Option { + match ch { + '0' => Some('₀'), + '1' => Some('₁'), + '2' => Some('₂'), + '3' => Some('₃'), + '4' => Some('₄'), + '5' => Some('₅'), + '6' => Some('₆'), + '7' => Some('₇'), + '8' => Some('₈'), + '9' => Some('₉'), + '+' => Some('₊'), + '-' => Some('₋'), + '=' => Some('₌'), + '(' => Some('₍'), + ')' => Some('₎'), + 'a' => Some('ₐ'), + 'e' => Some('ₑ'), + 'h' => Some('ₕ'), + 'i' => Some('ᵢ'), + 'j' => Some('ⱼ'), + 'k' => Some('ₖ'), + 'l' => Some('ₗ'), + 'm' => Some('ₘ'), + 'n' => Some('ₙ'), + 'o' => Some('ₒ'), + 'p' => Some('ₚ'), + 'r' => Some('ᵣ'), + 's' => Some('ₛ'), + 't' => Some('ₜ'), + 'u' => Some('ᵤ'), + 'v' => Some('ᵥ'), + 'x' => Some('ₓ'), + _ => None, + } +} + +/// Try to convert a node's text content to Unicode superscript characters. +/// Returns None if any character lacks a superscript form. +fn try_unicode_superscript(node: &EqNode) -> Option { + let text = extract_flat_text(node)?; + text.chars().map(to_superscript_char).collect() +} + +/// Try to convert a node's text content to Unicode subscript characters. +fn try_unicode_subscript(node: &EqNode) -> Option { + let text = extract_flat_text(node)?; + text.chars().map(to_subscript_char).collect() +} + +/// Extract flat text from simple nodes (Text, Seq of Text). +fn extract_flat_text(node: &EqNode) -> Option { + match node { + EqNode::Text(s) => Some(s.clone()), + EqNode::Seq(children) => { + let mut result = String::new(); + for child in children { + match child { + EqNode::Text(s) => result.push_str(s), + EqNode::Space(_) => {} // skip spaces in scripts + _ => return None, + } + } + if result.is_empty() { + None + } else { + Some(result) + } + } + _ => None, + } +} + +/// Render a Space node. The parser auto-inserts Space nodes around operators. +/// Negative and very small spaces collapse. Standard operator spaces (3–5pt) +/// become a single space. Larger explicit spaces (\quad etc.) grow accordingly. +fn layout_space(pts: f32) -> RenderedBlock { + if pts <= 0.0 || pts < 2.0 { + RenderedBlock::empty() + } else if pts >= 18.0 { + // \quad or larger + RenderedBlock::from_text(" ") + } else { + RenderedBlock::from_char(' ') + } +} + +/// Check if a node is whitespace-like (Space node or Text containing only spaces). +fn is_space_like(node: &EqNode) -> bool { + match node { + EqNode::Space(_) => true, + EqNode::Text(s) => s.chars().all(|c| c == ' '), + _ => false, + } +} + +fn layout_seq(children: &[EqNode]) -> RenderedBlock { + // Flatten nested Seqs so we can handle spacing uniformly. + let flat = flatten_seq(children); + // Collapse consecutive whitespace-like nodes into a single space. + let mut result = RenderedBlock::empty(); + let mut prev_was_space = false; + for child in &flat { + if is_space_like(child) { + if !prev_was_space { + prev_was_space = true; + result = result.beside(&RenderedBlock::from_char(' ')); + } + continue; + } + prev_was_space = false; + let block = layout(child); + result = result.beside(&block); + } + result +} + +/// Trim leading/trailing whitespace from a node. +/// Strips Space nodes and whitespace-only Text nodes from Seq boundaries. +fn trim_node(node: &EqNode) -> EqNode { + match node { + EqNode::Seq(children) => { + let trimmed: Vec = children + .iter() + .map(|c| match c { + EqNode::Text(s) => EqNode::Text(s.trim().to_string()), + other => other.clone(), + }) + .filter(|c| !is_space_like(c) || !matches!(c, EqNode::Text(s) if s.is_empty())) + .collect(); + // Remove leading/trailing space-like nodes + let start = trimmed.iter().position(|c| !is_space_like(c)).unwrap_or(0); + let end = trimmed + .iter() + .rposition(|c| !is_space_like(c)) + .map_or(0, |i| i + 1); + if start >= end { + return EqNode::Seq(vec![]); + } + EqNode::Seq(trimmed[start..end].to_vec()) + } + EqNode::Text(s) => EqNode::Text(s.trim().to_string()), + other => other.clone(), + } +} + +/// Recursively flatten nested Seq nodes into a single flat list. +fn flatten_seq(children: &[EqNode]) -> Vec<&EqNode> { + let mut result = Vec::new(); + for child in children { + if let EqNode::Seq(inner) = child { + result.extend(flatten_seq(inner)); + } else { + result.push(child); + } + } + result +} + +fn layout_frac(num: &EqNode, den: &EqNode) -> RenderedBlock { + let num_block = layout(num); + let den_block = layout(den); + + let bar_width = num_block.width().max(den_block.width()) + 2; // +2 for padding + let bar = RenderedBlock::hline('─', bar_width); + + let num_centered = num_block.center_in(bar_width); + let den_centered = den_block.center_in(bar_width); + + // Stack: numerator, bar, denominator. Baseline is the bar row. + let top = RenderedBlock::above(&num_centered, &bar, 0); + let baseline_row = top.height() - 1; // bar is the last row of 'top' + RenderedBlock::above(&top, &den_centered, baseline_row) +} + +fn layout_sup(base: &EqNode, sup: &EqNode) -> RenderedBlock { + // Try inline Unicode superscript first + if let Some(sup_text) = try_unicode_superscript(sup) { + let base_block = layout(base); + let sup_block = RenderedBlock::from_text(&sup_text); + return base_block.beside(&sup_block); + } + + let base_block = layout(base); + let sup_block = layout(sup); + + let can_overlap = base_block.height() > 1; + let sup_above = if can_overlap { + sup_block.height().saturating_sub(1) + } else { + sup_block.height() + }; + + let rows = build_sup_sub_grid( + base_block.cells(), + base_block.width(), + base_block.baseline(), + sup_block.cells(), + sup_block.width(), + None, + 0, + ); + + let total_height = rows.len(); + let baseline = sup_above + base_block.baseline(); + + RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1))) +} + +fn layout_sub(base: &EqNode, sub: &EqNode) -> RenderedBlock { + // Try inline Unicode subscript first + if let Some(sub_text) = try_unicode_subscript(sub) { + let base_block = layout(base); + let sub_block = RenderedBlock::from_text(&sub_text); + return base_block.beside(&sub_block); + } + + let base_block = layout(base); + let sub_block = layout(sub); + + let rows = build_sup_sub_grid( + base_block.cells(), + base_block.width(), + base_block.baseline(), + &[], + 0, + Some((sub_block.cells(), sub_block.width())), + 0, + ); + + let baseline = base_block.baseline(); + let total_height = rows.len(); + + RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1))) +} + +fn layout_supsub(base: &EqNode, sup: &EqNode, sub: &EqNode) -> RenderedBlock { + // Try inline Unicode for both scripts + let sup_inline = try_unicode_superscript(sup); + let sub_inline = try_unicode_subscript(sub); + + if let (Some(sup_text), Some(sub_text)) = (&sup_inline, &sub_inline) { + let base_block = layout(base); + let scripts = format!("{}{}", sup_text, sub_text); + // Subscript chars go right after superscript chars, all inline + // Actually stack them: sup on same line, sub on same line + // For compactness: base followed by sup_text on top row, sub_text on bottom + // Simplest: just append both inline + return base_block.beside(&RenderedBlock::from_text(&scripts)); + } + + // Fall back to multi-row layout + let base_block = layout(base); + let sup_block = layout(sup); + let sub_block = layout(sub); + + let can_overlap_sup = base_block.height() > 1; + let sup_above = if can_overlap_sup { + sup_block.height().saturating_sub(1) + } else { + sup_block.height() + }; + + let rows = build_sup_sub_grid( + base_block.cells(), + base_block.width(), + base_block.baseline(), + sup_block.cells(), + sup_block.width(), + Some((sub_block.cells(), sub_block.width())), + 0, + ); + + let total_height = rows.len(); + let baseline = sup_above + base_block.baseline(); + + RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1))) +} + +/// Build a grid for base with optional superscript above-right and subscript below-right. +/// +/// Layout: +/// ```text +/// [sup rows] +/// [base] [overlap ] +/// [sub rows] +/// ``` +/// +/// The superscript's last row overlaps with the base's first row (right side). +/// The subscript's first row overlaps with the base's last row (right side). +/// Build a grid for base with optional superscript above-right and subscript below-right. +/// +/// For a single-row base like `x`: +/// - `x^2` renders as: ` 2` / `x ` +/// - `x_i` renders as: `x ` / ` i` +/// - `x_i^2` renders as: ` 2` / `x ` / ` i` +/// +/// For multi-row bases, sup overlaps with the top row and sub with the bottom row. +fn build_sup_sub_grid( + base_cells: &[Vec], + base_width: usize, + _base_baseline: usize, + sup_cells: &[Vec], + sup_width: usize, + sub: Option<(&[Vec], usize)>, + _sub_baseline: usize, +) -> Vec> { + let base_height = base_cells.len(); + let sup_height = sup_cells.len(); + let (sub_cells, sub_width) = sub.unwrap_or((&[], 0)); + let sub_height = sub_cells.len(); + let has_sup = sup_height > 0; + let has_sub = sub_height > 0; + + let script_width = sup_width.max(sub_width); + + // For single-row bases with both sup and sub, don't overlap — stack all three. + // For multi-row bases or single script, allow 1 row of overlap. + let can_overlap_sup = has_sup && base_height > 1; + let can_overlap_sub = has_sub && base_height > 1 && !(has_sup && base_height <= 2); + + let sup_above = if can_overlap_sup { + sup_height.saturating_sub(1) + } else { + sup_height + }; + + let sub_below = if can_overlap_sub { + sub_height.saturating_sub(1) + } else { + sub_height + }; + + let total_height = sup_above + base_height + sub_below; + let mut rows = Vec::with_capacity(total_height); + + let empty_script = || std::iter::repeat_n(" ".to_string(), script_width); + + // Helper to append a script row (or padding) to a row + fn append_script_row( + row: &mut Vec, + cells: &[Vec], + idx: usize, + script_width: usize, + ) { + if idx < cells.len() { + row.extend(cells[idx].iter().cloned()); + let used = cells[idx].len(); + row.extend(std::iter::repeat_n( + " ".to_string(), + script_width.saturating_sub(used), + )); + } else { + row.extend(std::iter::repeat_n(" ".to_string(), script_width)); + } + } + + // Phase 1: sup-only rows above the base + for r in 0..sup_above { + let mut row = vec![" ".to_string(); base_width]; + append_script_row(&mut row, sup_cells, r, script_width); + rows.push(row); + } + + // Phase 2: base rows (with possible script overlap) + for (r, base_row) in base_cells.iter().enumerate().take(base_height) { + let mut row = base_row.clone(); + + // Check if a sup row overlaps here + let sup_idx = if can_overlap_sup { + sup_above + r + } else { + usize::MAX + }; + // Check if a sub row overlaps here + let sub_overlap_start = if can_overlap_sub { + base_height.saturating_sub(sub_height) + } else { + usize::MAX + }; + let sub_idx = if r >= sub_overlap_start && can_overlap_sub { + r - sub_overlap_start + } else { + usize::MAX + }; + + if sup_idx < sup_height { + append_script_row(&mut row, sup_cells, sup_idx, script_width); + } else if sub_idx < sub_height { + append_script_row(&mut row, sub_cells, sub_idx, script_width); + } else { + row.extend(empty_script()); + } + + rows.push(row); + } + + // Phase 3: sub-only rows below the base + let sub_start = if can_overlap_sub { + sub_height.min(base_height) + } else { + 0 + }; + for r in sub_start..sub_height { + let mut row = vec![" ".to_string(); base_width]; + append_script_row(&mut row, sub_cells, r, script_width); + rows.push(row); + } + + rows +} + +fn layout_sqrt(body: &EqNode) -> RenderedBlock { + let body_block = layout(body); + let body_h = body_block.height(); + let body_w = body_block.width(); + + // Single-row body: ___ + // √abc + // + // Multi-row body: ________ + // ╱ num + // ╱ ───── + // √ den + + if body_h == 1 { + // Simple case: √ prefix with overline above + let mut rows = Vec::with_capacity(2); + // Overline row + let mut top = vec![" ".to_string()]; + top.extend(std::iter::repeat_n("─".to_string(), body_w)); + rows.push(top); + // Body row with √ + let mut bot = vec!["√".to_string()]; + bot.extend(body_block.cells()[0].iter().cloned()); + rows.push(bot); + RenderedBlock::new(rows, 1) // baseline at body row + } else { + // Multi-row: radical extends upward + let mut rows = Vec::with_capacity(body_h + 1); + + // Overline row + let mut top = vec![" ".to_string()]; + top.extend(std::iter::repeat_n("─".to_string(), body_w)); + rows.push(top); + + // Body rows with radical on the left + for r in 0..body_h { + let radical_char = if r == body_h - 1 { "√" } else { "│" }; + let mut row = vec![radical_char.to_string()]; + row.extend(body_block.cells()[r].iter().cloned()); + rows.push(row); + } + + let baseline = 1 + body_block.baseline(); + RenderedBlock::new(rows, baseline) + } +} + +/// Build a multi-row operator symbol for integrals (⌠⎮⌡) and large Σ/∏. +fn build_bigop_symbol(symbol: &str) -> RenderedBlock { + match symbol { + "∫" => { + // 3-row integral using bracket pieces + let rows = vec![ + vec!["⌠".to_string()], + vec!["⎮".to_string()], + vec!["⌡".to_string()], + ]; + RenderedBlock::new(rows, 1) // baseline at middle + } + "∬" => { + let rows = vec![ + vec!["⌠".to_string(), "⌠".to_string()], + vec!["⎮".to_string(), "⎮".to_string()], + vec!["⌡".to_string(), "⌡".to_string()], + ]; + RenderedBlock::new(rows, 1) + } + "∮" => { + // Contour integral — use single char since no multi-row form exists + let rows = vec![ + vec!["⌠".to_string()], + vec!["⎮".to_string()], + vec!["⌡".to_string()], + ]; + RenderedBlock::new(rows, 1) + } + _ => { + // Σ, ∏, etc. — single character is fine, they're already wide enough + RenderedBlock::from_text(symbol) + } + } +} + +fn layout_bigop( + symbol: &str, + lower: &Option>, + upper: &Option>, +) -> RenderedBlock { + let op_block = build_bigop_symbol(symbol); + + let upper_block = upper.as_ref().map(|u| layout(u)); + let lower_block = lower.as_ref().map(|l| layout(l)); + + let max_width = [ + op_block.width(), + upper_block.as_ref().map_or(0, |b| b.width()), + lower_block.as_ref().map_or(0, |b| b.width()), + ] + .into_iter() + .max() + .unwrap_or(1); + + let op_centered = op_block.center_in(max_width); + + let mut result = if let Some(ub) = &upper_block { + let ub_centered = ub.center_in(max_width); + let baseline = ub_centered.height(); // op starts after upper limit + RenderedBlock::above(&ub_centered, &op_centered, baseline) + } else { + op_centered.clone() + }; + + // Baseline at the middle of the operator symbol + let op_mid = upper_block.as_ref().map_or(0, |b| b.height()) + op_block.height() / 2; + + if let Some(lb) = &lower_block { + let lb_centered = lb.center_in(max_width); + result = RenderedBlock::above(&result, &lb_centered, op_mid); + } + + RenderedBlock::new(result.cells().to_vec(), op_mid) +} + +fn layout_accent(body: &EqNode, kind: &AccentKind) -> RenderedBlock { + let body_block = layout(body); + let w = body_block.width(); + + let accent_block = match kind { + AccentKind::Bar => { + // Overline: use ‾ repeated across full width + RenderedBlock::hline('‾', w) + } + AccentKind::Hat => { + if w <= 1 { + RenderedBlock::from_char('^') + } else if w <= 3 { + RenderedBlock::from_text("/\\").center_in(w) + } else { + // Wide hat: /‾‾‾\ shape + let inner = w.saturating_sub(2); + let hat_str: String = std::iter::once('/') + .chain(std::iter::repeat_n('‾', inner)) + .chain(std::iter::once('\\')) + .collect(); + RenderedBlock::from_text(&hat_str) + } + } + AccentKind::Tilde => { + if w <= 1 { + RenderedBlock::from_char('~') + } else { + // Wide tilde using ˜ repeated or ~ centered + RenderedBlock::hline('~', w) + } + } + AccentKind::Vec => { + if w <= 1 { + RenderedBlock::from_char('→') + } else { + // Arrow spanning width: ──→ + let shaft = w.saturating_sub(1); + let arrow_str: String = std::iter::repeat_n('─', shaft) + .chain(std::iter::once('→')) + .collect(); + RenderedBlock::from_text(&arrow_str) + } + } + AccentKind::Dot => RenderedBlock::from_char('˙').center_in(w), + AccentKind::DoubleDot => RenderedBlock::from_text("¨").center_in(w), + }; + + let baseline = accent_block.height() + body_block.baseline(); + RenderedBlock::above(&accent_block, &body_block, baseline) +} + +fn layout_limit(name: &str, lower: &Option>) -> RenderedBlock { + let name_block = RenderedBlock::from_text(name); + + if let Some(low) = lower { + let low_block = layout(low); + let max_width = name_block.width().max(low_block.width()); + let name_centered = name_block.center_in(max_width); + let low_centered = low_block.center_in(max_width); + let baseline = name_centered.height() - 1; + RenderedBlock::above(&name_centered, &low_centered, baseline) + } else { + name_block + } +} + +fn layout_mathfont(kind: &MathFontKind, content: &EqNode) -> RenderedBlock { + // Extract text and apply Unicode math font mapping + if let Some(text) = extract_flat_text(content) { + let mapped = mathfont::map_str(kind, &text); + RenderedBlock::from_text(&mapped) + } else { + // Complex content inside font command — render normally + layout(content) + } +} + +fn layout_delimited(left: &str, right: &str, content: &EqNode) -> RenderedBlock { + let content_block = layout(content); + let h = content_block.height(); + + let left_block = build_delimiter(left, h); + let right_block = build_delimiter(right, h); + + left_block.beside(&content_block).beside(&right_block) +} + +/// Build a vertically-scaled delimiter. +fn build_delimiter(delim: &str, height: usize) -> RenderedBlock { + if delim == "." || delim.is_empty() { + // Invisible delimiter + return RenderedBlock::new(vec![vec![" ".to_string()]; height], height / 2); + } + + if height <= 1 { + return RenderedBlock::new(vec![vec![delim.to_string()]], 0); + } + + let (top, mid, bot) = match delim { + "(" => ("⎛", "⎜", "⎝"), + ")" => ("⎞", "⎟", "⎠"), + "[" => ("⎡", "⎢", "⎣"), + "]" => ("⎤", "⎥", "⎦"), + "{" => ("⎧", "⎨", "⎩"), + "}" => ("⎫", "⎬", "⎭"), + "|" => ("│", "│", "│"), + "‖" => ("‖", "‖", "‖"), + _ => (delim, delim, delim), + }; + + let mut rows = Vec::with_capacity(height); + rows.push(vec![top.to_string()]); + for _ in 1..height.saturating_sub(1) { + rows.push(vec![mid.to_string()]); + } + if height > 1 { + rows.push(vec![bot.to_string()]); + } + + RenderedBlock::new(rows, height / 2) +} + +fn layout_matrix(kind: &MatrixKind, matrix_rows: &[Vec]) -> RenderedBlock { + if matrix_rows.is_empty() { + return RenderedBlock::empty(); + } + + // Render all cells, trimming whitespace from cell content + let rendered: Vec> = matrix_rows + .iter() + .map(|row| row.iter().map(|cell| layout(&trim_node(cell))).collect()) + .collect(); + + let num_cols = rendered.iter().map(|r| r.len()).max().unwrap_or(0); + + // Compute column widths + let mut col_widths = vec![0usize; num_cols]; + for row in &rendered { + for (c, cell) in row.iter().enumerate() { + col_widths[c] = col_widths[c].max(cell.width()); + } + } + + // Build each matrix row using beside() for proper baseline alignment. + // Then stack rows vertically with a separator gap. + let col_sep = 2; // spaces between columns + let separator = RenderedBlock::from_text(&" ".repeat(col_sep)); + + let mut row_blocks: Vec = Vec::new(); + + for row in &rendered { + let mut row_block = RenderedBlock::empty(); + for (c, cell) in row.iter().enumerate() { + let padded = cell.center_in(col_widths[c]); + if !row_block.is_empty() { + row_block = row_block.beside(&separator); + } + row_block = row_block.beside(&padded); + } + // Pad to fill missing columns + for w in col_widths.iter().take(num_cols).skip(row.len()) { + row_block = row_block.beside(&separator); + row_block = row_block.beside(&RenderedBlock::from_text(&" ".repeat(*w))); + } + row_blocks.push(row_block); + } + + // Stack rows vertically. Each row_block already has correct baselines from beside(). + let grid_width = row_blocks.iter().map(|r| r.width()).max().unwrap_or(0); + + let mut grid = RenderedBlock::empty(); + for row_block in &row_blocks { + let padded = row_block.center_in(grid_width); + if grid.is_empty() { + grid = padded; + } else { + let baseline = grid.height() / 2; // intermediate baseline + grid = RenderedBlock::above(&grid, &padded, baseline); + } + } + + // Set baseline to middle of entire grid + let total_height = grid.height(); + let grid = RenderedBlock::new(grid.cells().to_vec(), total_height / 2); + + // Wrap with delimiters based on matrix kind + let (left, right) = match kind { + MatrixKind::Paren => ("(", ")"), + MatrixKind::Bracket => ("[", "]"), + MatrixKind::Brace => ("{", "}"), + MatrixKind::VBar => ("|", "|"), + MatrixKind::DoubleVBar => ("‖", "‖"), + MatrixKind::Plain => ("", ""), + }; + + if left.is_empty() { + grid + } else { + let left_d = build_delimiter(left, total_height); + let right_d = build_delimiter(right, total_height); + left_d.beside(&grid).beside(&right_d) + } +} + +fn layout_cases(rows: &[(EqNode, Option)]) -> RenderedBlock { + // Render as a left-brace delimited set of rows + let rendered: Vec = rows + .iter() + .map(|(val, cond)| { + let val_block = layout(val); + if let Some(c) = cond { + let cond_block = layout(c); + val_block + .beside(&RenderedBlock::from_text(" if ")) + .beside(&cond_block) + } else { + val_block + } + }) + .collect(); + + let max_width = rendered.iter().map(|b| b.width()).max().unwrap_or(0); + let mut grid = RenderedBlock::empty(); + for row_block in &rendered { + let padded = RenderedBlock::new(row_block.cells().to_vec(), row_block.baseline()); + // Pad to max width + let full_row = RenderedBlock::new( + padded + .cells() + .iter() + .map(|r| { + let mut r = r.clone(); + r.extend(std::iter::repeat_n( + " ".to_string(), + max_width.saturating_sub(r.len()), + )); + r + }) + .collect(), + padded.baseline(), + ); + if grid.is_empty() { + grid = full_row; + } else { + grid = RenderedBlock::above(&grid, &full_row, grid.height() / 2); + } + } + + let total_height = grid.height(); + let grid = RenderedBlock::new(grid.cells().to_vec(), total_height / 2); + let left_brace = build_delimiter("{", total_height); + left_brace.beside(&grid) +} + +fn layout_binom(top: &EqNode, bottom: &EqNode) -> RenderedBlock { + // Render as a fraction with parentheses instead of a bar + let top_block = layout(top); + let bot_block = layout(bottom); + + let inner_width = top_block.width().max(bot_block.width()); + let top_centered = top_block.center_in(inner_width); + let bot_centered = bot_block.center_in(inner_width); + + let baseline = top_centered.height(); + let stacked = RenderedBlock::above(&top_centered, &bot_centered, baseline - 1); + + let h = stacked.height(); + let left = build_delimiter("(", h); + let right = build_delimiter(")", h); + left.beside(&stacked).beside(&right) +} + +fn layout_brace(content: &EqNode, label: &Option>, over: &bool) -> RenderedBlock { + let content_block = layout(content); + let w = content_block.width(); + + // Build a horizontal brace + let brace_str = if *over { "⏞" } else { "⏟" }; + let brace_block = RenderedBlock::hline(brace_str.chars().next().unwrap(), w); + + if let Some(lbl) = label { + let label_block = layout(lbl).center_in(w); + if *over { + let top = RenderedBlock::above(&label_block, &brace_block, label_block.height()); + let baseline = top.height() + content_block.baseline(); + RenderedBlock::above(&top, &content_block, baseline) + } else { + let bottom = RenderedBlock::above(&brace_block, &label_block, 0); + let baseline = content_block.baseline(); + RenderedBlock::above(&content_block, &bottom, baseline) + } + } else if *over { + let baseline = brace_block.height() + content_block.baseline(); + RenderedBlock::above(&brace_block, &content_block, baseline) + } else { + let baseline = content_block.baseline(); + RenderedBlock::above(&content_block, &brace_block, baseline) + } +} + +fn layout_stackrel(base: &EqNode, annotation: &EqNode, over: &bool) -> RenderedBlock { + let base_block = layout(base); + let ann_block = layout(annotation); + let w = base_block.width().max(ann_block.width()); + let base_centered = base_block.center_in(w); + let ann_centered = ann_block.center_in(w); + + if *over { + let baseline = ann_centered.height() + base_block.baseline(); + RenderedBlock::above(&ann_centered, &base_centered, baseline) + } else { + let baseline = base_block.baseline(); + RenderedBlock::above(&base_centered, &ann_centered, baseline) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ccf5dd2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,72 @@ +//! # term-maths +//! +//! Character-grid mathematical notation renderer for terminals. +//! +//! Accepts LaTeX math input and renders it as 2D Unicode character art suitable +//! for display in a terminal. Targets JuliaMono as the recommended font. +//! +//! ## Quick Start +//! +//! ```rust +//! let block = term_maths::render(r"\frac{a}{b}"); +//! println!("{}", block); +//! // a +//! // ─── +//! // b +//! ``` +//! +//! ## Output Backends +//! +//! - **Plain text** — always available via [`render()`] and [`Display`](std::fmt::Display) +//! - **crossterm** — direct terminal output (feature `crossterm`) +//! - **ratatui** — TUI widget (feature `ratatui`) +//! - **LaTeX round-trip** — serialise back to LaTeX via [`to_latex()`] + +pub mod latex_renderer; +pub mod layout; +pub mod mathfont; +pub mod rendered_block; +pub mod renderer; + +#[cfg(feature = "crossterm")] +pub mod crossterm_renderer; + +#[cfg(feature = "ratatui")] +pub mod ratatui_widget; + +#[cfg(feature = "python")] +pub mod python; + +pub use latex_renderer::LatexRenderer; +pub use rendered_block::RenderedBlock; +pub use renderer::{MathRenderer, TerminalRenderer}; + +#[cfg(feature = "crossterm")] +pub use crossterm_renderer::CrosstermRenderer; + +#[cfg(feature = "ratatui")] +pub use ratatui_widget::MathWidget; + +use rust_latex_parser::parse_equation; + +/// Parse a LaTeX math string and render it as a 2D character grid. +/// +/// This is the primary entry point for the library. +/// +/// ```rust +/// let block = term_maths::render(r"x^2 + y^2 = z^2"); +/// assert_eq!(format!("{}", block), "x² + y² = z²"); +/// ``` +pub fn render(latex: &str) -> RenderedBlock { + let ast = parse_equation(latex); + layout::layout(&ast) +} + +/// Parse a LaTeX math string and serialise it back to LaTeX (round-trip). +/// +/// Useful for normalising LaTeX input or for the LaTeX output backend. +pub fn to_latex(latex: &str) -> String { + let ast = parse_equation(latex); + let renderer = LatexRenderer; + renderer.render(&ast) +} diff --git a/src/mathfont.rs b/src/mathfont.rs new file mode 100644 index 0000000..61b0548 --- /dev/null +++ b/src/mathfont.rs @@ -0,0 +1,194 @@ +//! Unicode Mathematical Alphanumeric Symbols mapping (U+1D400–U+1D7FF). +//! +//! Maps ASCII Latin letters (and digits) to their styled variants in the +//! Unicode Mathematical Alphanumeric Symbols block, keyed by `MathFontKind`. + +use rust_latex_parser::MathFontKind; + +/// Convert a character to its mathematical font variant. +/// Returns the original character if no mapping exists. +pub fn map_char(kind: &MathFontKind, ch: char) -> char { + match kind { + MathFontKind::Bold => to_bold(ch), + MathFontKind::Blackboard => to_double_struck(ch), + MathFontKind::Calligraphic => to_script(ch), + MathFontKind::Fraktur => to_fraktur(ch), + MathFontKind::Roman => ch, // upright, no transformation + MathFontKind::SansSerif => to_sans_serif(ch), + MathFontKind::Monospace => to_monospace(ch), + } +} + +/// Convert a string by mapping each character through the font transform. +pub fn map_str(kind: &MathFontKind, s: &str) -> String { + s.chars().map(|c| map_char(kind, c)).collect() +} + +// U+1D400 MATHEMATICAL BOLD CAPITAL A .. U+1D419 MATHEMATICAL BOLD CAPITAL Z +// U+1D41A MATHEMATICAL BOLD SMALL A .. U+1D433 MATHEMATICAL BOLD SMALL Z +// U+1D7CE MATHEMATICAL BOLD DIGIT ZERO .. U+1D7D7 MATHEMATICAL BOLD DIGIT NINE +fn to_bold(ch: char) -> char { + match ch { + 'A'..='Z' => char::from_u32(0x1D400 + (ch as u32 - 'A' as u32)).unwrap_or(ch), + 'a'..='z' => char::from_u32(0x1D41A + (ch as u32 - 'a' as u32)).unwrap_or(ch), + '0'..='9' => char::from_u32(0x1D7CE + (ch as u32 - '0' as u32)).unwrap_or(ch), + // Bold Greek uppercase: U+1D6A8–U+1D6C0 + 'Α'..='Ω' => char::from_u32(0x1D6A8 + (ch as u32 - 'Α' as u32)).unwrap_or(ch), + // Bold Greek lowercase: U+1D6C2–U+1D6DA + 'α'..='ω' => char::from_u32(0x1D6C2 + (ch as u32 - 'α' as u32)).unwrap_or(ch), + _ => ch, + } +} + +// U+1D538 MATHEMATICAL DOUBLE-STRUCK CAPITAL A .. U+1D551 +// Exceptions: C=ℂ, H=ℍ, N=ℕ, P=ℙ, Q=ℚ, R=ℝ, Z=ℤ (in Letterlike Symbols block) +// U+1D552 MATHEMATICAL DOUBLE-STRUCK SMALL A .. U+1D56B +// U+1D7D8 MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO .. U+1D7E1 +fn to_double_struck(ch: char) -> char { + match ch { + 'C' => 'ℂ', + 'H' => 'ℍ', + 'N' => 'ℕ', + 'P' => 'ℙ', + 'Q' => 'ℚ', + 'R' => 'ℝ', + 'Z' => 'ℤ', + 'A' | 'B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y' => { + char::from_u32(0x1D538 + (ch as u32 - 'A' as u32)).unwrap_or(ch) + } + 'a'..='z' => char::from_u32(0x1D552 + (ch as u32 - 'a' as u32)).unwrap_or(ch), + '0'..='9' => char::from_u32(0x1D7D8 + (ch as u32 - '0' as u32)).unwrap_or(ch), + _ => ch, + } +} + +// U+1D49C MATHEMATICAL SCRIPT CAPITAL A .. U+1D4B5 +// Exceptions: B=ℬ, E=ℰ, F=ℱ, H=ℋ, I=ℐ, L=ℒ, M=ℳ, R=ℛ (Letterlike Symbols) +// U+1D4B6 MATHEMATICAL SCRIPT SMALL A .. U+1D4CF +// Exceptions: e=ℯ, g=ℊ, o=ℴ +fn to_script(ch: char) -> char { + match ch { + 'B' => 'ℬ', + 'E' => 'ℰ', + 'F' => 'ℱ', + 'H' => 'ℋ', + 'I' => 'ℐ', + 'L' => 'ℒ', + 'M' => 'ℳ', + 'R' => 'ℛ', + 'e' => 'ℯ', + 'g' => 'ℊ', + 'o' => 'ℴ', + 'A' | 'C' | 'D' | 'G' | 'J' | 'K' | 'N'..='Q' | 'S'..='Z' => { + char::from_u32(0x1D49C + (ch as u32 - 'A' as u32)).unwrap_or(ch) + } + 'a'..='d' | 'f' | 'h'..='n' | 'p'..='z' => { + char::from_u32(0x1D4B6 + (ch as u32 - 'a' as u32)).unwrap_or(ch) + } + _ => ch, + } +} + +// U+1D504 MATHEMATICAL FRAKTUR CAPITAL A .. U+1D51C +// Exceptions: C=ℭ, H=ℌ, I=ℑ, R=ℜ, Z=ℨ +// U+1D51E MATHEMATICAL FRAKTUR SMALL A .. U+1D537 +fn to_fraktur(ch: char) -> char { + match ch { + 'C' => 'ℭ', + 'H' => 'ℌ', + 'I' => 'ℑ', + 'R' => 'ℜ', + 'Z' => 'ℨ', + 'A' | 'B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y' => { + char::from_u32(0x1D504 + (ch as u32 - 'A' as u32)).unwrap_or(ch) + } + 'a'..='z' => char::from_u32(0x1D51E + (ch as u32 - 'a' as u32)).unwrap_or(ch), + _ => ch, + } +} + +// U+1D5A0 MATHEMATICAL SANS-SERIF CAPITAL A .. U+1D5B9 +// U+1D5BA MATHEMATICAL SANS-SERIF SMALL A .. U+1D5D3 +// U+1D7E2 MATHEMATICAL SANS-SERIF DIGIT ZERO .. U+1D7EB +fn to_sans_serif(ch: char) -> char { + match ch { + 'A'..='Z' => char::from_u32(0x1D5A0 + (ch as u32 - 'A' as u32)).unwrap_or(ch), + 'a'..='z' => char::from_u32(0x1D5BA + (ch as u32 - 'a' as u32)).unwrap_or(ch), + '0'..='9' => char::from_u32(0x1D7E2 + (ch as u32 - '0' as u32)).unwrap_or(ch), + _ => ch, + } +} + +// U+1D670 MATHEMATICAL MONOSPACE CAPITAL A .. U+1D689 +// U+1D68A MATHEMATICAL MONOSPACE SMALL A .. U+1D6A3 +// U+1D7F6 MATHEMATICAL MONOSPACE DIGIT ZERO .. U+1D7FF +fn to_monospace(ch: char) -> char { + match ch { + 'A'..='Z' => char::from_u32(0x1D670 + (ch as u32 - 'A' as u32)).unwrap_or(ch), + 'a'..='z' => char::from_u32(0x1D68A + (ch as u32 - 'a' as u32)).unwrap_or(ch), + '0'..='9' => char::from_u32(0x1D7F6 + (ch as u32 - '0' as u32)).unwrap_or(ch), + _ => ch, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bold() { + assert_eq!(to_bold('A'), '𝐀'); + assert_eq!(to_bold('Z'), '𝐙'); + assert_eq!(to_bold('a'), '𝐚'); + assert_eq!(to_bold('z'), '𝐳'); + assert_eq!(to_bold('0'), '𝟎'); + } + + #[test] + fn test_double_struck() { + assert_eq!(to_double_struck('R'), 'ℝ'); + assert_eq!(to_double_struck('Z'), 'ℤ'); + assert_eq!(to_double_struck('N'), 'ℕ'); + assert_eq!(to_double_struck('C'), 'ℂ'); + assert_eq!(to_double_struck('Q'), 'ℚ'); + // Non-exception uppercase + assert_eq!(to_double_struck('A'), '𝔸'); + } + + #[test] + fn test_script() { + assert_eq!(to_script('L'), 'ℒ'); + assert_eq!(to_script('H'), 'ℋ'); + assert_eq!(to_script('B'), 'ℬ'); + // Non-exception + assert_eq!(to_script('A'), '𝒜'); + } + + #[test] + fn test_fraktur() { + assert_eq!(to_fraktur('H'), 'ℌ'); + assert_eq!(to_fraktur('R'), 'ℜ'); + assert_eq!(to_fraktur('a'), '𝔞'); + assert_eq!(to_fraktur('g'), '𝔤'); + } + + #[test] + fn test_sans_serif() { + assert_eq!(to_sans_serif('A'), '𝖠'); + assert_eq!(to_sans_serif('a'), '𝖺'); + } + + #[test] + fn test_monospace() { + assert_eq!(to_monospace('A'), '𝙰'); + assert_eq!(to_monospace('a'), '𝚊'); + assert_eq!(to_monospace('0'), '𝟶'); + } + + #[test] + fn test_non_letter_passthrough() { + // Non-letter characters should pass through unchanged + assert_eq!(map_char(&MathFontKind::Bold, '+'), '+'); + assert_eq!(map_char(&MathFontKind::Blackboard, ' '), ' '); + } +} diff --git a/src/python.rs b/src/python.rs new file mode 100644 index 0000000..4ff3257 --- /dev/null +++ b/src/python.rs @@ -0,0 +1,320 @@ +//! Python bindings for term-maths (enabled with the `python` feature). +//! +//! This module exposes the core API as a Python extension module named `_term_maths`. +//! It is intended to be imported through the `term_maths` Python package, which +//! re-exports everything from this compiled extension. +//! +//! ## Python usage +//! +//! ```python +//! import term_maths +//! +//! block = term_maths.render(r"\frac{a}{b}") +//! print(block) # multi-line Unicode art +//! print(block.width) # int +//! print(block.height) # int +//! print(block.baseline) # int +//! print(block.cells()) # list[list[str]] +//! ``` + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3_stub_gen::define_stub_info_gatherer; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; + +use crate::RenderedBlock; + +// --------------------------------------------------------------------------- +// RenderedBlock Python wrapper +// --------------------------------------------------------------------------- + +/// A rectangular character grid produced by rendering a LaTeX math expression. +/// +/// Each cell contains one terminal column's worth of text. The :attr:`baseline` +/// marks the row used for horizontal alignment when composing blocks side-by-side. +/// +/// Construct via the module-level :func:`render` function or the static +/// constructors (:meth:`from_char`, :meth:`from_text`, :meth:`empty`, +/// :meth:`hline`). +#[gen_stub_pyclass] +#[pyclass(name = "RenderedBlock", module = "term_maths")] +pub struct PyRenderedBlock(pub RenderedBlock); + +#[gen_stub_pymethods] +#[pymethods] +impl PyRenderedBlock { + // ------------------------------------------------------------------ + // Static constructors + // ------------------------------------------------------------------ + + /// Create a block containing a single character. + /// + /// :param ch: A single Unicode character. + /// :type ch: str + /// :raises ValueError: If ``ch`` is not exactly one character. + #[staticmethod] + fn from_char(ch: &str) -> PyResult { + let c = ch.chars().next().ok_or_else(|| { + PyValueError::new_err("from_char expects a single character, got an empty string") + })?; + Ok(PyRenderedBlock(RenderedBlock::from_char(c))) + } + + /// Create a single-row block from a text string. + /// + /// :param text: The text to render. + /// :type text: str + #[staticmethod] + fn from_text(text: &str) -> PyRenderedBlock { + PyRenderedBlock(RenderedBlock::from_text(text)) + } + + /// Create an empty block with zero dimensions. + #[staticmethod] + fn empty() -> PyRenderedBlock { + PyRenderedBlock(RenderedBlock::empty()) + } + + /// Create a horizontal line of a given character repeated *width* times. + /// + /// :param ch: The character to repeat (e.g. ``'─'``). + /// :type ch: str + /// :param width: Number of columns. + /// :type width: int + /// :raises ValueError: If ``ch`` is not exactly one character. + #[staticmethod] + fn hline(ch: &str, width: usize) -> PyResult { + let c = ch.chars().next().ok_or_else(|| { + PyValueError::new_err("hline expects a single character, got an empty string") + })?; + Ok(PyRenderedBlock(RenderedBlock::hline(c, width))) + } + + // ------------------------------------------------------------------ + // Properties + // ------------------------------------------------------------------ + + /// Width of the block in terminal columns. + #[getter] + fn width(&self) -> usize { + self.0.width() + } + + /// Height of the block in rows. + #[getter] + fn height(&self) -> usize { + self.0.height() + } + + /// Row index (0-indexed from top) used as the alignment baseline. + #[getter] + fn baseline(&self) -> usize { + self.0.baseline() + } + + // ------------------------------------------------------------------ + // Methods + // ------------------------------------------------------------------ + + /// Return the cell grid as a list of rows, where each row is a list of + /// single-column strings. + /// + /// :rtype: list[list[str]] + fn cells(&self) -> Vec> { + self.0.cells().to_vec() + } + + /// Return ``True`` if the block has zero width or height. + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Place *other* immediately to the right of *self*, aligning on baselines. + /// + /// Shorter blocks are padded with empty rows above or below as needed. + /// + /// :param other: The block to append on the right. + /// :type other: RenderedBlock + /// :rtype: RenderedBlock + fn beside(&self, other: &PyRenderedBlock) -> PyRenderedBlock { + PyRenderedBlock(self.0.beside(&other.0)) + } + + /// Stack *top* above *bottom* and set the baseline to *baseline_row*. + /// + /// :param top: Upper block. + /// :type top: RenderedBlock + /// :param bottom: Lower block. + /// :type bottom: RenderedBlock + /// :param baseline_row: Row index (in the combined block) for the baseline. + /// :type baseline_row: int + /// :rtype: RenderedBlock + #[staticmethod] + fn above( + top: &PyRenderedBlock, + bottom: &PyRenderedBlock, + baseline_row: usize, + ) -> PyRenderedBlock { + PyRenderedBlock(RenderedBlock::above(&top.0, &bottom.0, baseline_row)) + } + + /// Add empty space around the block. + /// + /// :param left: Columns to add on the left. + /// :param right: Columns to add on the right. + /// :param top: Rows to add on top. + /// :param bottom: Rows to add on the bottom. + /// :rtype: RenderedBlock + fn pad(&self, left: usize, right: usize, top: usize, bottom: usize) -> PyRenderedBlock { + PyRenderedBlock(self.0.pad(left, right, top, bottom)) + } + + /// Horizontally centre the block within a target width. + /// + /// If *target_width* is not larger than the current width, returns a clone. + /// + /// :param target_width: Desired total width in columns. + /// :type target_width: int + /// :rtype: RenderedBlock + fn center_in(&self, target_width: usize) -> PyRenderedBlock { + PyRenderedBlock(self.0.center_in(target_width)) + } + + // ------------------------------------------------------------------ + // Dunder methods + // ------------------------------------------------------------------ + + fn __str__(&self) -> String { + format!("{}", self.0) + } + + fn __repr__(&self) -> String { + format!( + "RenderedBlock(width={}, height={}, baseline={})", + self.0.width(), + self.0.height(), + self.0.baseline(), + ) + } +} + +// --------------------------------------------------------------------------- +// Helper: parse font kind from string +// --------------------------------------------------------------------------- + +fn parse_font_kind(font: &str) -> PyResult { + use rust_latex_parser::MathFontKind; + match font { + "bold" => Ok(MathFontKind::Bold), + "blackboard" => Ok(MathFontKind::Blackboard), + "calligraphic" => Ok(MathFontKind::Calligraphic), + "fraktur" => Ok(MathFontKind::Fraktur), + "roman" => Ok(MathFontKind::Roman), + "sans_serif" => Ok(MathFontKind::SansSerif), + "monospace" => Ok(MathFontKind::Monospace), + other => Err(PyValueError::new_err(format!( + "Unknown font kind {other:?}. \ + Valid options: bold, blackboard, calligraphic, fraktur, roman, sans_serif, monospace" + ))), + } +} + +// --------------------------------------------------------------------------- +// Python module definition (inline style — required for experimental-inspect) +// --------------------------------------------------------------------------- + +/// Python extension module ``_term_maths``. +/// +/// Import via the ``term_maths`` package rather than directly: +/// +/// .. code-block:: python +/// +/// import term_maths +/// block = term_maths.render(r"\frac{a}{b}") +#[pymodule] +pub mod _term_maths { + use super::*; + + // Re-export the class so pyclass metadata is visible to the module. + #[pymodule_export] + use super::PyRenderedBlock; + + /// Parse a LaTeX math string and render it as a 2D character grid. + /// + /// This is the primary entry point of the library. + /// + /// :param latex: A LaTeX math expression (without surrounding ``$`` delimiters). + /// :type latex: str + /// :returns: The rendered block. + /// :rtype: RenderedBlock + /// + /// Example: + /// + /// ```python + /// >>> import term_maths + /// >>> print(term_maths.render(r"\frac{a}{b}")) + /// a + /// ─── + /// b + /// ``` + #[pyfunction] + pub fn render(latex: &str) -> PyRenderedBlock { + PyRenderedBlock(crate::render(latex)) + } + + /// Parse a LaTeX math string and serialise it back to normalised LaTeX. + /// + /// Useful for round-tripping or canonicalising LaTeX input. + /// + /// :param latex: A LaTeX math expression. + /// :type latex: str + /// :rtype: str + #[pyfunction] + pub fn to_latex(latex: &str) -> String { + crate::to_latex(latex) + } + + /// Map a single character to its Unicode mathematical font variant. + /// + /// Returns the original character unchanged if no mapping exists for + /// the given font kind. + /// + /// :param font: One of ``"bold"``, ``"blackboard"``, ``"calligraphic"``, + /// ``"fraktur"``, ``"roman"``, ``"sans_serif"``, ``"monospace"``. + /// :type font: str + /// :param ch: A single Unicode character. + /// :type ch: str + /// :rtype: str + /// :raises ValueError: If *font* is not a recognised font kind, or *ch* is empty. + #[pyfunction] + pub fn map_char(font: &str, ch: &str) -> PyResult { + let kind = parse_font_kind(font)?; + let c = ch.chars().next().ok_or_else(|| { + PyValueError::new_err("map_char expects a single character, got an empty string") + })?; + Ok(crate::mathfont::map_char(&kind, c).to_string()) + } + + /// Map every character in a string to its Unicode mathematical font variant. + /// + /// Characters without a mapping are passed through unchanged. + /// + /// :param font: One of ``"bold"``, ``"blackboard"``, ``"calligraphic"``, + /// ``"fraktur"``, ``"roman"``, ``"sans_serif"``, ``"monospace"``. + /// :type font: str + /// :param s: The string to transform. + /// :type s: str + /// :rtype: str + /// :raises ValueError: If *font* is not a recognised font kind. + #[pyfunction] + pub fn map_str(font: &str, s: &str) -> PyResult { + let kind = parse_font_kind(font)?; + Ok(crate::mathfont::map_str(&kind, s)) + } +} + +// --------------------------------------------------------------------------- +// Stub generation entry point (used by src/bin/stub_gen.rs) +// --------------------------------------------------------------------------- + +define_stub_info_gatherer!(stub_info_gatherer); diff --git a/src/ratatui_widget.rs b/src/ratatui_widget.rs new file mode 100644 index 0000000..54c138e --- /dev/null +++ b/src/ratatui_widget.rs @@ -0,0 +1,50 @@ +//! Ratatui widget backend — renders a `RenderedBlock` into a ratatui `Buffer`. +//! +//! Feature-gated behind `ratatui`. + +use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget}; + +use crate::rendered_block::RenderedBlock; + +/// A ratatui widget that renders a `RenderedBlock` into a terminal buffer. +pub struct MathWidget<'a> { + block: &'a RenderedBlock, + style: Style, +} + +impl<'a> MathWidget<'a> { + pub fn new(block: &'a RenderedBlock) -> Self { + Self { + block, + style: Style::default(), + } + } + + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self + } +} + +impl Widget for MathWidget<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let max_rows = area.height as usize; + let max_cols = area.width as usize; + + for (r, row) in self.block.cells().iter().enumerate() { + if r >= max_rows { + break; + } + let y = area.y + r as u16; + let mut x_offset = 0usize; + for cell in row { + if x_offset >= max_cols { + break; + } + let x = area.x + x_offset as u16; + buf.set_string(x, y, cell, self.style); + x_offset += unicode_width::UnicodeWidthStr::width(cell.as_str()).max(1); + } + } + } +} diff --git a/src/rendered_block.rs b/src/rendered_block.rs new file mode 100644 index 0000000..5d2463c --- /dev/null +++ b/src/rendered_block.rs @@ -0,0 +1,352 @@ +use std::fmt; +use unicode_width::UnicodeWidthStr; + +/// A rectangular character grid with dimensional metadata. +/// +/// This is the core data structure for 2D math rendering. Each cell contains +/// a string (to handle multi-codepoint grapheme clusters). The baseline marks +/// the row used for horizontal alignment when composing blocks side-by-side. +#[derive(Debug, Clone)] +pub struct RenderedBlock { + /// Rows of character cells. Each cell is a `String` occupying one terminal column. + cells: Vec>, + /// Width in terminal columns (via unicode-width). + width: usize, + /// Height in rows. + height: usize, + /// Row index of the alignment baseline (0-indexed from top). + baseline: usize, +} + +impl RenderedBlock { + /// Create a new block from rows of cell strings. + /// + /// Width is computed from the first row (all rows must have equal width). + /// The baseline defaults to `height / 2` if not specified. + pub fn new(cells: Vec>, baseline: usize) -> Self { + let height = cells.len(); + let width = cells.first().map_or(0, |row| { + row.iter().map(|c| UnicodeWidthStr::width(c.as_str())).sum() + }); + Self { + cells, + width, + height, + baseline, + } + } + + /// Create a block containing a single character. + pub fn from_char(ch: char) -> Self { + let s = ch.to_string(); + let width = UnicodeWidthStr::width(s.as_str()).max(1); + Self { + cells: vec![vec![s]], + width, + height: 1, + baseline: 0, + } + } + + /// Create a block from a string of text (single row). + pub fn from_text(text: &str) -> Self { + if text.is_empty() { + return Self::empty(); + } + let cells: Vec = text.chars().map(|c| c.to_string()).collect(); + let width = UnicodeWidthStr::width(text); + Self { + cells: vec![cells], + width, + height: 1, + baseline: 0, + } + } + + /// Create an empty block with zero dimensions. + pub fn empty() -> Self { + Self { + cells: vec![], + width: 0, + height: 0, + baseline: 0, + } + } + + pub fn width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.height + } + + pub fn baseline(&self) -> usize { + self.baseline + } + + pub fn cells(&self) -> &[Vec] { + &self.cells + } + + pub fn is_empty(&self) -> bool { + self.height == 0 || self.width == 0 + } + + /// Place two blocks side-by-side, aligned on baselines. + /// Pads the shorter block with empty rows above/below as needed. + pub fn beside(&self, other: &RenderedBlock) -> RenderedBlock { + if self.is_empty() { + return other.clone(); + } + if other.is_empty() { + return self.clone(); + } + + let baseline = self.baseline.max(other.baseline); + let above_baseline = baseline; + + let self_below = self.height.saturating_sub(self.baseline + 1); + let other_below = other.height.saturating_sub(other.baseline + 1); + let below_baseline = self_below.max(other_below); + + let total_height = above_baseline + 1 + below_baseline; + let total_width = self.width + other.width; + + let self_top_pad = above_baseline - self.baseline; + let other_top_pad = above_baseline - other.baseline; + + let mut rows = Vec::with_capacity(total_height); + for row_idx in 0..total_height { + let mut row = Vec::new(); + + // Left block cells + let self_row = row_idx.checked_sub(self_top_pad); + if let Some(sr) = self_row { + if sr < self.height { + row.extend(self.cells[sr].iter().cloned()); + } else { + row.extend(std::iter::repeat_n(" ".to_string(), self.width)); + } + } else { + row.extend(std::iter::repeat_n(" ".to_string(), self.width)); + } + + // Right block cells + let other_row = row_idx.checked_sub(other_top_pad); + if let Some(or_idx) = other_row { + if or_idx < other.height { + row.extend(other.cells[or_idx].iter().cloned()); + } else { + row.extend(std::iter::repeat_n(" ".to_string(), other.width)); + } + } else { + row.extend(std::iter::repeat_n(" ".to_string(), other.width)); + } + + rows.push(row); + } + + RenderedBlock { + cells: rows, + width: total_width, + height: total_height, + baseline, + } + } + + /// Stack two blocks vertically. The baseline is set to `baseline_row` + /// (typically the dividing row between them, or top/bottom block's baseline). + pub fn above( + top: &RenderedBlock, + bottom: &RenderedBlock, + baseline_row: usize, + ) -> RenderedBlock { + let width = top.width.max(bottom.width); + let mut rows = Vec::with_capacity(top.height + bottom.height); + + for r in 0..top.height { + rows.push(Self::pad_row_to_width(&top.cells[r], top.width, width)); + } + for r in 0..bottom.height { + rows.push(Self::pad_row_to_width( + &bottom.cells[r], + bottom.width, + width, + )); + } + + RenderedBlock { + cells: rows, + width, + height: top.height + bottom.height, + baseline: baseline_row, + } + } + + /// Add empty space around a block. + pub fn pad(&self, left: usize, right: usize, top: usize, bottom: usize) -> RenderedBlock { + let new_width = left + self.width + right; + let new_height = top + self.height + bottom; + + let mut rows = Vec::with_capacity(new_height); + + // Top padding + for _ in 0..top { + rows.push(vec![" ".to_string(); new_width]); + } + + // Content rows with left/right padding + for r in 0..self.height { + let mut row = Vec::with_capacity(new_width); + row.extend(std::iter::repeat_n(" ".to_string(), left)); + row.extend(self.cells[r].iter().cloned()); + row.extend(std::iter::repeat_n(" ".to_string(), right)); + rows.push(row); + } + + // Bottom padding + for _ in 0..bottom { + rows.push(vec![" ".to_string(); new_width]); + } + + RenderedBlock { + cells: rows, + width: new_width, + height: new_height, + baseline: self.baseline + top, + } + } + + /// Horizontally centre a block within a given width. + pub fn center_in(&self, target_width: usize) -> RenderedBlock { + if target_width <= self.width { + return self.clone(); + } + let total_pad = target_width - self.width; + let left_pad = total_pad / 2; + let right_pad = total_pad - left_pad; + self.pad(left_pad, right_pad, 0, 0) + } + + /// Helper: pad a row of cells to a target width by appending spaces. + fn pad_row_to_width(row: &[String], current_width: usize, target_width: usize) -> Vec { + let mut result = row.to_vec(); + let pad = target_width.saturating_sub(current_width); + result.extend(std::iter::repeat_n(" ".to_string(), pad)); + result + } + + /// Create a horizontal line of a given character and width. + pub fn hline(ch: char, width: usize) -> RenderedBlock { + let cells = vec![vec![ch.to_string(); width]]; + RenderedBlock { + cells, + width, + height: 1, + baseline: 0, + } + } +} + +impl fmt::Display for RenderedBlock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, row) in self.cells.iter().enumerate() { + if i > 0 { + writeln!(f)?; + } + for cell in row { + write!(f, "{}", cell)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_char() { + let block = RenderedBlock::from_char('x'); + assert_eq!(block.width(), 1); + assert_eq!(block.height(), 1); + assert_eq!(block.baseline(), 0); + assert_eq!(format!("{}", block), "x"); + } + + #[test] + fn test_from_text() { + let block = RenderedBlock::from_text("hello"); + assert_eq!(block.width(), 5); + assert_eq!(block.height(), 1); + assert_eq!(format!("{}", block), "hello"); + } + + #[test] + fn test_beside_baseline_aligned() { + // Two single-row blocks + let a = RenderedBlock::from_text("ab"); + let b = RenderedBlock::from_text("cd"); + let result = a.beside(&b); + assert_eq!(result.width(), 4); + assert_eq!(result.height(), 1); + assert_eq!(format!("{}", result), "abcd"); + } + + #[test] + fn test_beside_different_heights() { + // a is 3 rows tall with baseline at row 1 + let a = RenderedBlock::new( + vec![vec!["a".into()], vec!["b".into()], vec!["c".into()]], + 1, + ); + // d is 1 row tall with baseline at row 0 + let d = RenderedBlock::from_char('d'); + let result = a.beside(&d); + assert_eq!(result.height(), 3); + assert_eq!(result.baseline(), 1); + // d should be on the baseline row (row 1) + let output = format!("{}", result); + let lines: Vec<&str> = output.lines().collect(); + assert_eq!(lines[0], "a "); + assert_eq!(lines[1], "bd"); + assert_eq!(lines[2], "c "); + } + + #[test] + fn test_center_in() { + let block = RenderedBlock::from_text("ab"); + let centered = block.center_in(6); + assert_eq!(centered.width(), 6); + assert_eq!(format!("{}", centered), " ab "); + } + + #[test] + fn test_above() { + let top = RenderedBlock::from_text("abc"); + let bottom = RenderedBlock::from_text("de"); + let result = RenderedBlock::above(&top, &bottom, 0); + assert_eq!(result.height(), 2); + assert_eq!(result.width(), 3); + let output = format!("{}", result); + let lines: Vec<&str> = output.lines().collect(); + assert_eq!(lines[0], "abc"); + assert_eq!(lines[1], "de "); + } + + #[test] + fn test_pad() { + let block = RenderedBlock::from_char('x'); + let padded = block.pad(1, 1, 1, 1); + assert_eq!(padded.width(), 3); + assert_eq!(padded.height(), 3); + assert_eq!(padded.baseline(), 1); + let output = format!("{}", padded); + let lines: Vec<&str> = output.lines().collect(); + assert_eq!(lines[0], " "); + assert_eq!(lines[1], " x "); + assert_eq!(lines[2], " "); + } +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..940ac36 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,25 @@ +//! Output backend trait and implementations. + +use rust_latex_parser::EqNode; + +use crate::rendered_block::RenderedBlock; + +/// Trait for rendering an `EqNode` AST into a target output format. +pub trait MathRenderer { + type Output; + + /// Render an equation AST node into the target output. + fn render(&self, node: &EqNode) -> Self::Output; +} + +/// Default renderer that produces a `RenderedBlock` (2D character grid). +/// Always available — no feature gates required. +pub struct TerminalRenderer; + +impl MathRenderer for TerminalRenderer { + type Output = RenderedBlock; + + fn render(&self, node: &EqNode) -> RenderedBlock { + crate::layout::layout(node) + } +} diff --git a/tests/layout_tests.rs b/tests/layout_tests.rs new file mode 100644 index 0000000..6fedc32 --- /dev/null +++ b/tests/layout_tests.rs @@ -0,0 +1,421 @@ +use term_maths::render; + +/// Helper: render and collect output lines, trimming trailing whitespace per line. +fn render_lines(latex: &str) -> Vec { + let block = render(latex); + let output = format!("{}", block); + output.lines().map(|l| l.trim_end().to_string()).collect() +} + +#[test] +fn test_simple_fraction() { + let lines = render_lines(r"\frac{a}{b}"); + assert_eq!(lines, vec![" a", "───", " b"]); +} + +#[test] +fn test_nested_fraction() { + let lines = render_lines(r"\frac{1}{1+\frac{1}{x}}"); + // Numerator "1" centered over denominator "1 + 1/x" + assert_eq!(lines.len(), 5); + // Top line: centered "1" + assert!(lines[0].contains('1')); + // Bar line + assert!(lines[1].chars().all(|c| c == '─')); + // Denominator contains nested fraction + assert!(lines[3].contains('+')); +} + +#[test] +fn test_superscript() { + // Simple digits use inline Unicode superscript + let lines = render_lines(r"x^2"); + assert_eq!(lines.len(), 1); + assert_eq!(lines[0], "x²"); +} + +#[test] +fn test_subscript() { + // Simple chars use inline Unicode subscript + let lines = render_lines(r"a_n"); + assert_eq!(lines.len(), 1); + assert_eq!(lines[0], "aₙ"); +} + +#[test] +fn test_supsub() { + // Both scripts inline when possible + let lines = render_lines(r"x_i^2"); + assert_eq!(lines.len(), 1); + assert_eq!(lines[0], "x²ᵢ"); +} + +#[test] +fn test_superscript_fallback() { + // Complex superscripts fall back to multi-row + let lines = render_lines(r"e^{i\pi}"); + assert!(lines.len() >= 2); + let joined = lines.join("\n"); + assert!(joined.contains('π')); + assert!(joined.contains('e')); +} + +#[test] +fn test_horizontal_sequence() { + let block = render(r"a + b"); + // Should be a single row with spaces around operators + assert_eq!(block.height(), 1); + let output = format!("{}", block); + assert!(output.contains("a")); + assert!(output.contains("+")); + assert!(output.contains("b")); +} + +#[test] +fn test_sqrt() { + let lines = render_lines(r"\sqrt{x}"); + // Should have overline and radical + assert!(lines[0].contains('─')); + assert!(lines.iter().any(|l| l.contains('√'))); + assert!(lines.iter().any(|l| l.contains('x'))); +} + +#[test] +fn test_euler_identity() { + let lines = render_lines(r"e^{i\pi} + 1 = 0"); + // Should render as 2 rows (e with superscript iπ, then + 1 = 0) + assert!(lines.len() >= 2); + // Top row should have iπ + let joined = lines.join("\n"); + assert!(joined.contains('π')); + assert!(joined.contains('0')); +} + +#[test] +fn test_quadratic_formula() { + let lines = render_lines(r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"); + // Should be a multi-line fraction with sqrt in numerator + assert!(lines.len() >= 3); // at least num + bar + den + // Contains the fraction bar + assert!(lines.iter().any(|l| l.contains('─') && !l.contains('√'))); + // Contains sqrt + let joined = lines.join("\n"); + assert!(joined.contains('√')); + assert!(joined.contains("2a")); +} + +#[test] +fn test_empty_input() { + let block = render(""); + assert!(block.is_empty() || block.height() <= 1); +} + +#[test] +fn test_single_symbol() { + let block = render(r"\alpha"); + assert_eq!(block.height(), 1); + let output = format!("{}", block); + assert!(output.contains('α')); +} + +#[test] +fn test_fraction_baseline_alignment() { + // When a fraction appears beside other content, baselines should align + let lines = render_lines(r"x + \frac{a}{b}"); + // x and + should be on the fraction bar row + let bar_row = lines.iter().position(|l| l.contains('─')).unwrap(); + assert!(lines[bar_row].contains('x') || lines[bar_row].contains('+')); +} + +// ── Sprint 2: DSP Reference Equations ────────────────────────────────── + +#[test] +fn test_dft_summation() { + // X[k] = Σ_{n=0}^{N-1} x[n] · e^{-j 2π/N kn} + let lines = render_lines(r"X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j \frac{2\pi}{N} kn}"); + let joined = lines.join("\n"); + + // Must contain the summation symbol + assert!(joined.contains('∑'), "missing Σ"); + // Must contain upper limit N-1 and lower limit n=0 + assert!( + joined.contains("N - 1") || joined.contains("N-1"), + "missing upper limit" + ); + assert!( + joined.contains("n = 0") || joined.contains("n=0"), + "missing lower limit" + ); + // Must contain the exponent's fraction 2π/N + assert!(joined.contains('π'), "missing π in exponent"); + // Multi-line output + assert!(lines.len() >= 3, "DFT should be at least 3 lines tall"); +} + +#[test] +fn test_convolution_integral() { + // (f * g)(t) = ∫_{-∞}^{∞} f(τ) g(t - τ) dτ + let lines = render_lines(r"(f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) \, d\tau"); + let joined = lines.join("\n"); + + // Must contain integral pieces + assert!( + joined.contains('⌠') || joined.contains('∫'), + "missing integral symbol" + ); + // Must contain limits + assert!(joined.contains('∞'), "missing infinity"); + assert!(joined.contains("-∞"), "missing negative infinity"); + // Must contain tau + assert!(joined.contains('τ'), "missing tau"); + // Multi-line output (integral is 3+ rows) + assert!( + lines.len() >= 3, + "convolution integral should be at least 3 lines" + ); +} + +#[test] +fn test_transfer_function() { + // H(z) = (b₀ + b₁z⁻¹ + b₂z⁻²) / (1 + a₁z⁻¹ + a₂z⁻²) + let lines = + render_lines(r"H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}}"); + let joined = lines.join("\n"); + + // Must contain fraction bar + assert!( + lines.iter().any(|l| l.contains('─')), + "missing fraction bar" + ); + // Must contain subscripted coefficients + assert!(joined.contains('₀') || joined.contains("b_0"), "missing b₀"); + assert!(joined.contains('₁') || joined.contains("b_1"), "missing b₁"); + // Must contain z⁻¹ (inline superscript) + assert!(joined.contains("z⁻¹"), "missing z⁻¹"); + // Three lines minimum (num + bar + den) + assert!( + lines.len() >= 3, + "transfer function should be at least 3 lines" + ); +} + +#[test] +fn test_hann_window() { + // w(n) = 0.5(1 - cos(2πn / (N-1))) + let lines = render_lines(r"w(n) = 0.5 \left(1 - \cos\left(\frac{2\pi n}{N - 1}\right)\right)"); + let joined = lines.join("\n"); + + // Must contain cos + assert!(joined.contains("cos"), "missing cos"); + // Must contain π + assert!(joined.contains('π'), "missing π"); + // Must contain scaled delimiters + assert!( + joined.contains('⎛') || joined.contains('('), + "missing delimiter" + ); + // Must contain N - 1 in denominator + assert!(joined.contains("N - 1"), "missing N - 1 denominator"); + // Multi-line (fraction inside delimiters) + assert!(lines.len() >= 2, "Hann window should be at least 2 lines"); +} + +// ── Sprint 2: Component Tests ────────────────────────────────────────── + +#[test] +fn test_integral_multirow() { + let lines = render_lines(r"\int_{0}^{1}"); + let joined = lines.join("\n"); + // Should use multi-row integral characters + assert!(joined.contains('⌠'), "missing ⌠ top piece"); + assert!(joined.contains('⌡'), "missing ⌡ bottom piece"); +} + +#[test] +fn test_sum_with_limits() { + let lines = render_lines(r"\sum_{i=1}^{n}"); + let joined = lines.join("\n"); + assert!(joined.contains('∑'), "missing Σ"); + // Upper limit above, lower limit below + assert!( + lines.len() >= 3, + "sum with limits should be at least 3 lines" + ); +} + +#[test] +fn test_scaled_delimiters() { + let lines = render_lines(r"\left(\frac{a}{b}\right)"); + let joined = lines.join("\n"); + // Should use bracket piece characters for 3-row fraction + assert!( + joined.contains('⎛') && joined.contains('⎝'), + "missing scaled parentheses" + ); + assert!(joined.contains('─'), "missing fraction bar"); +} + +#[test] +fn test_overline() { + let lines = render_lines(r"\overline{abc}"); + assert_eq!(lines.len(), 2); + assert!(lines[0].contains('‾'), "missing overline character"); + assert!(lines[1].contains("abc"), "missing body"); +} + +#[test] +fn test_accent_hat() { + let lines = render_lines(r"\hat{x}"); + assert_eq!(lines.len(), 2); + assert!(lines[0].contains('^'), "missing hat"); + assert!(lines[1].contains('x'), "missing body"); +} + +#[test] +fn test_accent_vec() { + let lines = render_lines(r"\vec{v}"); + assert_eq!(lines.len(), 2); + assert!(lines[0].contains('→'), "missing arrow"); + assert!(lines[1].contains('v'), "missing body"); +} + +#[test] +fn test_sqrt_of_fraction() { + let lines = render_lines(r"\sqrt{\frac{a}{b}}"); + let joined = lines.join("\n"); + assert!(joined.contains('√'), "missing radical"); + assert!(joined.contains('─'), "missing overline or fraction bar"); + assert!(lines.len() >= 3, "sqrt of fraction should be multi-line"); +} + +// ── Sprint 3: Matrix and Symbol Coverage ─────────────────────────────── + +#[test] +fn test_pmatrix_2x2() { + let lines = render_lines(r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}"); + let joined = lines.join("\n"); + // Parenthesis delimiters + assert!( + joined.contains('⎛') || joined.contains('('), + "missing left paren" + ); + assert!( + joined.contains('⎞') || joined.contains(')'), + "missing right paren" + ); + // All entries present + for ch in ['a', 'b', 'c', 'd'] { + assert!(joined.contains(ch), "missing entry {}", ch); + } +} + +#[test] +fn test_bmatrix_identity() { + let lines = render_lines(r"\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}"); + let joined = lines.join("\n"); + // Bracket delimiters + assert!( + joined.contains('⎡') || joined.contains('['), + "missing left bracket" + ); + assert!( + joined.contains('⎤') || joined.contains(']'), + "missing right bracket" + ); +} + +#[test] +fn test_vmatrix_determinant() { + let lines = render_lines(r"\begin{vmatrix} a & b \\ c & d \end{vmatrix}"); + let joined = lines.join("\n"); + assert!(joined.contains('│'), "missing vertical bar delimiter"); +} + +#[test] +fn test_matrix_with_fractions() { + let lines = render_lines(r"\begin{pmatrix} \frac{1}{2} & 0 \\ 0 & \frac{3}{4} \end{pmatrix}"); + let joined = lines.join("\n"); + // Fraction bars inside cells + assert!(joined.contains('─'), "missing fraction bar"); + // Both fractions present + assert!(joined.contains('1') && joined.contains('2'), "missing 1/2"); + assert!(joined.contains('3') && joined.contains('4'), "missing 3/4"); + // Multi-line (fractions make rows taller) + assert!( + lines.len() >= 4, + "matrix with fractions should be at least 4 lines" + ); +} + +#[test] +fn test_3x3_matrix() { + let lines = render_lines(r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}"); + let joined = lines.join("\n"); + // All digits present + for d in '1'..='9' { + assert!(joined.contains(d), "missing digit {}", d); + } + assert!(lines.len() >= 3, "3x3 matrix should be at least 3 lines"); +} + +// ── Math Font Tests ──────────────────────────────────────────────────── + +#[test] +fn test_mathbb_double_struck() { + let block = render(r"\mathbb{R}"); + let output = format!("{}", block); + assert_eq!(output.trim(), "ℝ"); +} + +#[test] +fn test_mathbb_integers() { + let block = render(r"\mathbb{Z}"); + let output = format!("{}", block); + assert_eq!(output.trim(), "ℤ"); +} + +#[test] +fn test_mathcal_script() { + let block = render(r"\mathcal{L}"); + let output = format!("{}", block); + assert_eq!(output.trim(), "ℒ"); +} + +#[test] +fn test_mathbf_bold() { + let block = render(r"\mathbf{x}"); + let output = format!("{}", block); + assert_eq!(output.trim(), "𝐱"); +} + +#[test] +fn test_mathfrak_fraktur() { + let block = render(r"\mathfrak{g}"); + let output = format!("{}", block); + assert_eq!(output.trim(), "𝔤"); +} + +#[test] +fn test_mathbb_with_superscript() { + let lines = render_lines(r"\mathbb{R}^n"); + assert_eq!(lines.len(), 1); + let output = &lines[0]; + assert!(output.contains('ℝ'), "missing double-struck R"); + assert!(output.contains('ⁿ'), "missing superscript n"); +} + +#[test] +fn test_mathsf_sans_serif() { + let block = render(r"\mathsf{ABC}"); + let output = format!("{}", block); + assert!(output.contains('𝖠'), "missing sans-serif A"); + assert!(output.contains('𝖡'), "missing sans-serif B"); + assert!(output.contains('𝖢'), "missing sans-serif C"); +} + +#[test] +fn test_mathtt_monospace() { + let block = render(r"\mathtt{code}"); + let output = format!("{}", block); + assert!(output.contains('𝚌'), "missing monospace c"); +} From 262df2ebb53c7b2cc74398bf2361d10687673e28 Mon Sep 17 00:00:00 2001 From: liwangping Date: Sat, 15 Aug 2026 02:02:50 +0800 Subject: [PATCH 2/2] feat: use TXM for display math Co-authored-by: Qwen-Coder --- dist/render.js | 5 +- dist/txm-wasm/package.json | 3 + dist/txm-wasm/txm.d.ts | 4 + dist/txm-wasm/txm.js | 134 + dist/txm-wasm/txm_bg.wasm | Bin 0 -> 181118 bytes dist/txm-wasm/txm_bg.wasm.d.ts | 10 + dist/txm.d.ts | 1 + dist/txm.js | 17 + package.json | 6 +- scripts/copy-txm-wasm.mjs | 9 + src/render.ts | 5 +- src/txm-wasm/package.json | 3 + src/txm-wasm/txm.d.ts | 4 + src/txm-wasm/txm.js | 134 + src/txm-wasm/txm_bg.wasm | Bin 0 -> 181118 bytes src/txm-wasm/txm_bg.wasm.d.ts | 10 + src/txm.ts | 23 + test/render.test.ts | 21 +- vendor/term-maths/.github/workflows/ci.yml | 61 - vendor/term-maths/.github/workflows/docs.yml | 74 - .../term-maths/.github/workflows/release.yml | 163 -- vendor/term-maths/.gitignore | 30 - vendor/term-maths/Cargo.lock | 2145 ----------------- vendor/term-maths/Cargo.toml | 40 - vendor/term-maths/LICENSE-APACHE | 190 -- vendor/term-maths/LICENSE-MIT | 21 - vendor/term-maths/README.md | 283 --- vendor/term-maths/examples/crossterm_demo.rs | 49 - vendor/term-maths/examples/debug_ast.rs | 20 - vendor/term-maths/examples/dsp_equations.rs | 58 - vendor/term-maths/examples/latex_roundtrip.rs | 25 - vendor/term-maths/examples/matrix_demo.rs | 41 - vendor/term-maths/examples/ratatui_demo.rs | 37 - vendor/term-maths/examples/render_demo.rs | 22 - vendor/term-maths/pyproject.toml | 46 - vendor/term-maths/python/docs/Makefile | 14 - vendor/term-maths/python/docs/api.rst | 22 - vendor/term-maths/python/docs/conf.py | 61 - vendor/term-maths/python/docs/examples.rst | 52 - vendor/term-maths/python/docs/index.rst | 50 - .../term-maths/python/docs/requirements.txt | 3 - .../python/examples/block_composition.py | 100 - .../python/examples/dsp_equations.py | 46 - .../term-maths/python/examples/math_fonts.py | 66 - .../term-maths/python/examples/render_demo.py | 21 - .../term-maths/python/term_maths/__init__.py | 33 - vendor/term-maths/python/term_maths/py.typed | 0 vendor/term-maths/src/bin/stub_gen.rs | 16 - vendor/term-maths/src/crossterm_renderer.rs | 36 - vendor/term-maths/src/latex_renderer.rs | 301 --- vendor/term-maths/src/layout.rs | 927 ------- vendor/term-maths/src/lib.rs | 72 - vendor/term-maths/src/mathfont.rs | 194 -- vendor/term-maths/src/python.rs | 320 --- vendor/term-maths/src/ratatui_widget.rs | 50 - vendor/term-maths/src/rendered_block.rs | 352 --- vendor/term-maths/src/renderer.rs | 25 - vendor/term-maths/tests/layout_tests.rs | 421 ---- vendor/txm-wasm/Cargo.lock | 231 ++ vendor/txm-wasm/Cargo.toml | 12 + vendor/txm-wasm/src/lib.rs | 39 + vendor/txm/Cargo.lock | 340 +++ vendor/txm/Cargo.toml | 20 + vendor/txm/LICENSE-APACHE | 201 ++ vendor/txm/LICENSE-MIT | 9 + vendor/txm/README.md | 57 + vendor/txm/UPSTREAM.md | 2 + vendor/txm/src/ast.rs | 36 + vendor/txm/src/backend.rs | 14 + vendor/txm/src/backends/generic_backend.rs | 495 ++++ vendor/txm/src/backends/mod.rs | 2 + vendor/txm/src/backends/terminal.rs | 104 + vendor/txm/src/error.rs | 92 + vendor/txm/src/glyph.rs | 537 +++++ vendor/txm/src/layout_tree.rs | 697 ++++++ vendor/txm/src/lib.rs | 256 ++ vendor/txm/src/main.rs | 185 ++ vendor/txm/src/parser.rs | 678 ++++++ vendor/txm/src/ratatui.rs | 286 +++ vendor/txm/src/render.rs | 241 ++ vendor/txm/src/style.rs | 299 +++ vendor/txm/src/token.rs | 156 ++ vendor/txm/tests/ratatui.rs | 53 + vendor/txm/tests/smoke.rs | 239 ++ 84 files changed, 5657 insertions(+), 6500 deletions(-) create mode 100644 dist/txm-wasm/package.json create mode 100644 dist/txm-wasm/txm.d.ts create mode 100644 dist/txm-wasm/txm.js create mode 100644 dist/txm-wasm/txm_bg.wasm create mode 100644 dist/txm-wasm/txm_bg.wasm.d.ts create mode 100644 dist/txm.d.ts create mode 100644 dist/txm.js create mode 100644 scripts/copy-txm-wasm.mjs create mode 100644 src/txm-wasm/package.json create mode 100644 src/txm-wasm/txm.d.ts create mode 100644 src/txm-wasm/txm.js create mode 100644 src/txm-wasm/txm_bg.wasm create mode 100644 src/txm-wasm/txm_bg.wasm.d.ts create mode 100644 src/txm.ts delete mode 100644 vendor/term-maths/.github/workflows/ci.yml delete mode 100644 vendor/term-maths/.github/workflows/docs.yml delete mode 100644 vendor/term-maths/.github/workflows/release.yml delete mode 100644 vendor/term-maths/.gitignore delete mode 100644 vendor/term-maths/Cargo.lock delete mode 100644 vendor/term-maths/Cargo.toml delete mode 100644 vendor/term-maths/LICENSE-APACHE delete mode 100644 vendor/term-maths/LICENSE-MIT delete mode 100644 vendor/term-maths/README.md delete mode 100644 vendor/term-maths/examples/crossterm_demo.rs delete mode 100644 vendor/term-maths/examples/debug_ast.rs delete mode 100644 vendor/term-maths/examples/dsp_equations.rs delete mode 100644 vendor/term-maths/examples/latex_roundtrip.rs delete mode 100644 vendor/term-maths/examples/matrix_demo.rs delete mode 100644 vendor/term-maths/examples/ratatui_demo.rs delete mode 100644 vendor/term-maths/examples/render_demo.rs delete mode 100644 vendor/term-maths/pyproject.toml delete mode 100644 vendor/term-maths/python/docs/Makefile delete mode 100644 vendor/term-maths/python/docs/api.rst delete mode 100644 vendor/term-maths/python/docs/conf.py delete mode 100644 vendor/term-maths/python/docs/examples.rst delete mode 100644 vendor/term-maths/python/docs/index.rst delete mode 100644 vendor/term-maths/python/docs/requirements.txt delete mode 100644 vendor/term-maths/python/examples/block_composition.py delete mode 100644 vendor/term-maths/python/examples/dsp_equations.py delete mode 100644 vendor/term-maths/python/examples/math_fonts.py delete mode 100644 vendor/term-maths/python/examples/render_demo.py delete mode 100644 vendor/term-maths/python/term_maths/__init__.py delete mode 100644 vendor/term-maths/python/term_maths/py.typed delete mode 100644 vendor/term-maths/src/bin/stub_gen.rs delete mode 100644 vendor/term-maths/src/crossterm_renderer.rs delete mode 100644 vendor/term-maths/src/latex_renderer.rs delete mode 100644 vendor/term-maths/src/layout.rs delete mode 100644 vendor/term-maths/src/lib.rs delete mode 100644 vendor/term-maths/src/mathfont.rs delete mode 100644 vendor/term-maths/src/python.rs delete mode 100644 vendor/term-maths/src/ratatui_widget.rs delete mode 100644 vendor/term-maths/src/rendered_block.rs delete mode 100644 vendor/term-maths/src/renderer.rs delete mode 100644 vendor/term-maths/tests/layout_tests.rs create mode 100644 vendor/txm-wasm/Cargo.lock create mode 100644 vendor/txm-wasm/Cargo.toml create mode 100644 vendor/txm-wasm/src/lib.rs create mode 100644 vendor/txm/Cargo.lock create mode 100644 vendor/txm/Cargo.toml create mode 100644 vendor/txm/LICENSE-APACHE create mode 100644 vendor/txm/LICENSE-MIT create mode 100644 vendor/txm/README.md create mode 100644 vendor/txm/UPSTREAM.md create mode 100644 vendor/txm/src/ast.rs create mode 100644 vendor/txm/src/backend.rs create mode 100644 vendor/txm/src/backends/generic_backend.rs create mode 100644 vendor/txm/src/backends/mod.rs create mode 100644 vendor/txm/src/backends/terminal.rs create mode 100644 vendor/txm/src/error.rs create mode 100644 vendor/txm/src/glyph.rs create mode 100644 vendor/txm/src/layout_tree.rs create mode 100644 vendor/txm/src/lib.rs create mode 100644 vendor/txm/src/main.rs create mode 100644 vendor/txm/src/parser.rs create mode 100644 vendor/txm/src/ratatui.rs create mode 100644 vendor/txm/src/render.rs create mode 100644 vendor/txm/src/style.rs create mode 100644 vendor/txm/src/token.rs create mode 100644 vendor/txm/tests/ratatui.rs create mode 100644 vendor/txm/tests/smoke.rs diff --git a/dist/render.js b/dist/render.js index d50d626..1ee30a5 100644 --- a/dist/render.js +++ b/dist/render.js @@ -4,6 +4,7 @@ import stripAnsi from "strip-ansi"; import { hyperlinkSupported, osc8 } from "./hyperlink.js"; import { parse } from "./parser.js"; import { convertLatexToUnicode } from "./latex.js"; +import { renderDisplayLatex } from "./txm.js"; import { createStyler, themes } from "./theme.js"; import { visibleWidth, wrapText, wrapWithPrefix } from "./wrap.js"; function dedent(markdown) { @@ -461,8 +462,8 @@ function renderCodeBlock(node, ctx) { return [`${top}\n${boxLines.join("\n")}\n${bottom}\n\n`]; } function renderDisplayMath(node, ctx) { - const converted = convertLatexToUnicode(node.value); - const styled = ctx.style(converted, ctx.options.theme.math || ctx.options.theme.inlineCode); + const rendered = renderDisplayLatex(node.value); + const styled = ctx.style(rendered, ctx.options.theme.math || ctx.options.theme.inlineCode); const lines = styled.split("\n"); return [`\n${lines.map((l) => ` ${l}`).join("\n")}\n`]; } diff --git a/dist/txm-wasm/package.json b/dist/txm-wasm/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/dist/txm-wasm/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/dist/txm-wasm/txm.d.ts b/dist/txm-wasm/txm.d.ts new file mode 100644 index 0000000..b9b5a16 --- /dev/null +++ b/dist/txm-wasm/txm.d.ts @@ -0,0 +1,4 @@ +/* tslint:disable */ +/* eslint-disable */ + +export function render_latex(latex: string): string; diff --git a/dist/txm-wasm/txm.js b/dist/txm-wasm/txm.js new file mode 100644 index 0000000..7775be4 --- /dev/null +++ b/dist/txm-wasm/txm.js @@ -0,0 +1,134 @@ +/* @ts-self-types="./txm.d.ts" */ + +/** + * @param {string} latex + * @returns {string} + */ +function render_latex(latex) { + let deferred3_0; + let deferred3_1; + try { + const ptr0 = passStringToWasm0(latex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.render_latex(ptr0, len0); + var ptr2 = ret[0]; + var len2 = ret[1]; + if (ret[3]) { + ptr2 = 0; len2 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred3_0 = ptr2; + deferred3_1 = len2; + return getStringFromWasm0(ptr2, len2); + } finally { + wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); + } +} +exports.render_latex = render_latex; + +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbindgen_cast_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./txm_bg.js": import0, + }; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +function decodeText(ptr, len) { + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +const wasmPath = `${__dirname}/txm_bg.wasm`; +const wasmBytes = require('fs').readFileSync(wasmPath); +const wasmModule = new WebAssembly.Module(wasmBytes); +let wasm = new WebAssembly.Instance(wasmModule, __wbg_get_imports()).exports; +wasm.__wbindgen_start(); diff --git a/dist/txm-wasm/txm_bg.wasm b/dist/txm-wasm/txm_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..aaec9ac61def01e596d1601d172ee06e045151c1 GIT binary patch literal 181118 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 [number, number, number, number]; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/dist/txm.d.ts b/dist/txm.d.ts new file mode 100644 index 0000000..49ed953 --- /dev/null +++ b/dist/txm.d.ts @@ -0,0 +1 @@ +export declare function renderDisplayLatex(latex: string): string; diff --git a/dist/txm.js b/dist/txm.js new file mode 100644 index 0000000..7029138 --- /dev/null +++ b/dist/txm.js @@ -0,0 +1,17 @@ +import { createRequire } from "node:module"; +import { convertLatexToUnicode } from "./latex.js"; +const require = createRequire(import.meta.url); +const txm = require("./txm-wasm/txm.js"); +export function renderDisplayLatex(latex) { + try { + return txm + .render_latex(latex) + .trimEnd() + .split("\n") + .map((line) => line.trimEnd()) + .join("\n"); + } + catch { + return convertLatexToUnicode(latex); + } +} diff --git a/package.json b/package.json index ddd6cae..ea96617 100644 --- a/package.json +++ b/package.json @@ -46,14 +46,14 @@ "scripts": { "build": "pnpm lint && pnpm typecheck && pnpm test && pnpm compile", "clean": "rm -rf dist", - "format": "oxfmt --write .", - "format:check": "oxfmt --check .", + "format": "oxfmt --write src/*.ts test scripts package.json", + "format:check": "oxfmt --check src/*.ts test scripts package.json", "lint": "rm -rf dist coverage && pnpm format:check && oxlint --deny-warnings src test", "test": "vitest run", "test:coverage": "vitest run --coverage", "typecheck": "tsc -p tsconfig.json --noEmit", "types": "tsc -p tsconfig.json --emitDeclarationOnly", - "compile": "tsc -p tsconfig.json", + "compile": "tsc -p tsconfig.json && node scripts/copy-txm-wasm.mjs", "markdansi": "tsx src/cli.ts", "release": "node scripts/release.mjs" }, diff --git a/scripts/copy-txm-wasm.mjs b/scripts/copy-txm-wasm.mjs new file mode 100644 index 0000000..27861a7 --- /dev/null +++ b/scripts/copy-txm-wasm.mjs @@ -0,0 +1,9 @@ +import { cp } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +await cp(path.join(projectRoot, "src", "txm-wasm"), path.join(projectRoot, "dist", "txm-wasm"), { + recursive: true, +}); diff --git a/src/render.ts b/src/render.ts index 7786ed7..e21f7df 100644 --- a/src/render.ts +++ b/src/render.ts @@ -16,6 +16,7 @@ import { hyperlinkSupported, osc8 } from "./hyperlink.js"; import { parse } from "./parser.js"; import { convertLatexToUnicode } from "./latex.js"; import type { Styler } from "./theme.js"; +import { renderDisplayLatex } from "./txm.js"; import { createStyler, themes } from "./theme.js"; import type { RenderOptions, StyleIntent, Theme } from "./types.js"; import { visibleWidth, wrapText, wrapWithPrefix } from "./wrap.js"; @@ -590,8 +591,8 @@ function renderCodeBlock(node: Code, ctx: RenderContext): string[] { } function renderDisplayMath(node: { value: string }, ctx: RenderContext): string[] { - const converted = convertLatexToUnicode(node.value); - const styled = ctx.style(converted, ctx.options.theme.math || ctx.options.theme.inlineCode); + const rendered = renderDisplayLatex(node.value); + const styled = ctx.style(rendered, ctx.options.theme.math || ctx.options.theme.inlineCode); const lines = styled.split("\n"); return [`\n${lines.map((l) => ` ${l}`).join("\n")}\n`]; } diff --git a/src/txm-wasm/package.json b/src/txm-wasm/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/src/txm-wasm/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/src/txm-wasm/txm.d.ts b/src/txm-wasm/txm.d.ts new file mode 100644 index 0000000..b9b5a16 --- /dev/null +++ b/src/txm-wasm/txm.d.ts @@ -0,0 +1,4 @@ +/* tslint:disable */ +/* eslint-disable */ + +export function render_latex(latex: string): string; diff --git a/src/txm-wasm/txm.js b/src/txm-wasm/txm.js new file mode 100644 index 0000000..7775be4 --- /dev/null +++ b/src/txm-wasm/txm.js @@ -0,0 +1,134 @@ +/* @ts-self-types="./txm.d.ts" */ + +/** + * @param {string} latex + * @returns {string} + */ +function render_latex(latex) { + let deferred3_0; + let deferred3_1; + try { + const ptr0 = passStringToWasm0(latex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.render_latex(ptr0, len0); + var ptr2 = ret[0]; + var len2 = ret[1]; + if (ret[3]) { + ptr2 = 0; len2 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred3_0 = ptr2; + deferred3_1 = len2; + return getStringFromWasm0(ptr2, len2); + } finally { + wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); + } +} +exports.render_latex = render_latex; + +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbindgen_cast_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./txm_bg.js": import0, + }; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +function decodeText(ptr, len) { + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +const wasmPath = `${__dirname}/txm_bg.wasm`; +const wasmBytes = require('fs').readFileSync(wasmPath); +const wasmModule = new WebAssembly.Module(wasmBytes); +let wasm = new WebAssembly.Instance(wasmModule, __wbg_get_imports()).exports; +wasm.__wbindgen_start(); diff --git a/src/txm-wasm/txm_bg.wasm b/src/txm-wasm/txm_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..aaec9ac61def01e596d1601d172ee06e045151c1 GIT binary patch literal 181118 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 [number, number, number, number]; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/src/txm.ts b/src/txm.ts new file mode 100644 index 0000000..710362d --- /dev/null +++ b/src/txm.ts @@ -0,0 +1,23 @@ +import { createRequire } from "node:module"; + +import { convertLatexToUnicode } from "./latex.js"; + +type Txm = { + render_latex(latex: string): string; +}; + +const require = createRequire(import.meta.url); +const txm = require("./txm-wasm/txm.js") as Txm; + +export function renderDisplayLatex(latex: string): string { + try { + return txm + .render_latex(latex) + .trimEnd() + .split("\n") + .map((line) => line.trimEnd()) + .join("\n"); + } catch { + return convertLatexToUnicode(latex); + } +} diff --git a/test/render.test.ts b/test/render.test.ts index b174371..5484033 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -50,17 +50,26 @@ describe("inline formatting", () => { expect(out).not.toContain("\\mathrm"); }); - it("renders display LaTeX math as an indented block", () => { - const out = strip( - "Before\n\n$$\n\\partial\\Gamma\\coloneqq\\Gamma\\times(\\Gamma\\to\\Gamma)\\tag{5}\n$$\n\nAfter", - noColor, - ); + it("renders display LaTeX math as an indented two-dimensional Unicode grid", () => { + const out = strip("Before\n\n$$\n\\frac{a}{b}\n$$\n\nAfter", noColor); expect(out).toContain("Before"); - expect(out).toContain(" ∂Γ≔Γ×(Γ→Γ) (5)"); + expect(out).toContain(" a\n ───\n b"); expect(out).toContain("After"); expect(out).not.toContain("$$"); }); + it("renders display matrices as a two-dimensional Unicode grid", () => { + const out = strip("$$\\begin{bmatrix}a&b\\\\c&d\\end{bmatrix}$$", noColor); + expect(out).toContain(" ⎡ a b ⎤"); + expect(out).toContain(" ⎢"); + expect(out).toContain(" ⎣ c d ⎦"); + }); + + it("falls back to inline Unicode conversion for unsupported display LaTeX", () => { + const out = strip("$$\\unknown{x}$$", noColor); + expect(out).toContain(" unknownx"); + }); + it("leaves code spans, fenced code, and dollar amounts unchanged", () => { const out = strip("Use `$PATH` and pay $100.\n\n```ts\nconst formula = '$x$'\n```", noColor); expect(out).toContain("Use $PATH and pay $100."); diff --git a/vendor/term-maths/.github/workflows/ci.yml b/vendor/term-maths/.github/workflows/ci.yml deleted file mode 100644 index 5416738..0000000 --- a/vendor/term-maths/.github/workflows/ci.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -env: - CARGO_TERM_COLOR: always - -jobs: - check: - name: Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo check --all-features - - test: - name: Test - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo test - - run: cargo test --all-features - - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - run: cargo clippy --all-features -- -D warnings - - fmt: - name: Format - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt - - run: cargo fmt -- --check - - doc: - name: Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo doc --features ratatui,crossterm,pulldown-latex --no-deps - env: - RUSTDOCFLAGS: -D warnings diff --git a/vendor/term-maths/.github/workflows/docs.yml b/vendor/term-maths/.github/workflows/docs.yml deleted file mode 100644 index 79f411c..0000000 --- a/vendor/term-maths/.github/workflows/docs.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Build and Deploy Documentation - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Create virtual environment - run: | - python -m venv .venv - - - name: Install documentation dependencies - run: | - source .venv/bin/activate - python -m pip install --upgrade pip - pip install maturin - pip install -r python/docs/requirements.txt - - - name: Build and install term-maths package - run: | - source .venv/bin/activate - maturin develop --release --features python - - - name: Build documentation - run: | - source .venv/bin/activate - cd python/docs - make html - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: 'python/docs/_build/html' - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - if: github.ref == 'refs/heads/main' - - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/vendor/term-maths/.github/workflows/release.yml b/vendor/term-maths/.github/workflows/release.yml deleted file mode 100644 index fca1771..0000000 --- a/vendor/term-maths/.github/workflows/release.yml +++ /dev/null @@ -1,163 +0,0 @@ -name: Release - -# Triggered by pushing a version tag: git tag v1.0.0 && git push --tags -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - -permissions: - contents: write # create GitHub Release - id-token: write # PyPI Trusted Publishing (OIDC) - -jobs: - # --------------------------------------------------------------------------- - # Build Python wheels for each platform - # --------------------------------------------------------------------------- - build-wheels: - name: Build wheels (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - - - name: Add macOS cross-compile targets - if: matrix.os == 'macos-latest' - run: rustup target add x86_64-apple-darwin aarch64-apple-darwin - - - name: Build wheels (Linux — manylinux x86_64 + aarch64) - if: matrix.os == 'ubuntu-latest' - uses: PyO3/maturin-action@v1 - with: - command: build - args: --release --features python --out dist - manylinux: auto # builds manylinux2014-compatible wheels - target: x86_64 - - - name: Build wheels (Linux — manylinux aarch64) - if: matrix.os == 'ubuntu-latest' - uses: PyO3/maturin-action@v1 - with: - command: build - args: --release --features python --out dist - manylinux: auto - target: aarch64 - - - name: Build wheels (macOS universal2) - if: matrix.os == 'macos-latest' - uses: PyO3/maturin-action@v1 - with: - command: build - args: --release --features python --out dist --target universal2-apple-darwin - - - name: Build wheels (Windows x86_64) - if: matrix.os == 'windows-latest' - uses: PyO3/maturin-action@v1 - with: - command: build - args: --release --features python --out dist - - - name: Upload wheel artifacts - uses: actions/upload-artifact@v4 - with: - name: wheels-${{ matrix.os }} - path: dist/ - - # --------------------------------------------------------------------------- - # Build the source distribution (sdist) - # --------------------------------------------------------------------------- - build-sdist: - name: Build sdist - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: PyO3/maturin-action@v1 - with: - command: sdist - args: --out dist - - - uses: actions/upload-artifact@v4 - with: - name: sdist - path: dist/ - - # --------------------------------------------------------------------------- - # Publish Python package to PyPI (Trusted Publishing — no API key required) - # Configure on PyPI: add this repo as a trusted publisher for term-maths - # --------------------------------------------------------------------------- - publish-pypi: - name: Publish to PyPI - runs-on: ubuntu-latest - needs: [build-wheels, build-sdist] - environment: - name: pypi - url: https://pypi.org/p/term-maths - - steps: - - uses: actions/download-artifact@v4 - with: - pattern: wheels-* - path: dist/ - merge-multiple: true - - - uses: actions/download-artifact@v4 - with: - name: sdist - path: dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - # No username/password needed — Trusted Publishing uses OIDC - - # --------------------------------------------------------------------------- - # Publish Rust crate to crates.io - # Requires secret: CARGO_REGISTRY_TOKEN - # --------------------------------------------------------------------------- - publish-crate: - name: Publish to crates.io - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - - - name: Publish crate - run: cargo publish --no-verify - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - - # --------------------------------------------------------------------------- - # Create a GitHub Release with release notes - # --------------------------------------------------------------------------- - github-release: - name: Create GitHub Release - runs-on: ubuntu-latest - needs: [publish-pypi, publish-crate] - - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - pattern: wheels-* - path: dist/ - merge-multiple: true - - - uses: actions/download-artifact@v4 - with: - name: sdist - path: dist/ - - - name: Create release - uses: softprops/action-gh-release@v2 - with: - files: dist/* - generate_release_notes: true diff --git a/vendor/term-maths/.gitignore b/vendor/term-maths/.gitignore deleted file mode 100644 index 521934d..0000000 --- a/vendor/term-maths/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# Rust -/target/ - -# Python -__pycache__/ -*.pyc -*.pyo -*.pyd -*.egg-info/ -*.egg -dist/ -build/ -.venv/ - -# Compiled native extensions (produced by maturin develop) -*.so -*.dylib -*.dll - -# Sphinx build output -python/docs/_build/ - -# Root-level dev notes (design.md, research.md — not for the repo) -/docs/ - -# Editor / OS -*.log -*.tmp -*.bak -.DS_Store \ No newline at end of file diff --git a/vendor/term-maths/Cargo.lock b/vendor/term-maths/Cargo.lock deleted file mode 100644 index b69dbc8..0000000 --- a/vendor/term-maths/Cargo.lock +++ /dev/null @@ -1,2145 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "lab", - "phf", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "deltae" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set", - "regex", -] - -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "finl_unicode" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indexmap" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "inventory" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" -dependencies = [ - "rustversion", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kasuari" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" -dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.18", -] - -[[package]] -name = "lab" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.184" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" - -[[package]] -name = "line-clipping" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix", - "winapi", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys", -] - -[[package]] -name = "ndarray" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "portable-atomic", - "portable-atomic-util", - "rawpointer", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "numpy" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94caae805f998a07d33af06e6a3891e38556051b8045c615470a71590e13e78" -dependencies = [ - "libc", - "ndarray", - "num-complex", - "num-integer", - "num-traits", - "pyo3", - "rustc-hash", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pulldown-latex" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b8bc0583825170e3f560701d966dc2f0e3a16946da371e37d30d3ad7207fb7e" -dependencies = [ - "bumpalo", -] - -[[package]] -name = "pyo3" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pyo3-stub-gen" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7c2d6e22cba51cc9766b6dee4087218cc445fdf99db62fa4f269e074351b46" -dependencies = [ - "anyhow", - "chrono", - "inventory", - "itertools 0.13.0", - "log", - "maplit", - "num-complex", - "numpy", - "pyo3", - "pyo3-stub-gen-derive", - "serde", - "toml", -] - -[[package]] -name = "pyo3-stub-gen-derive" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee49d727163163a0c6fc3fee4636c8b5c82e1bb868e85cf411be7ae9e4e5b40" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "ratatui" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-macros", - "ratatui-termwiz", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" -dependencies = [ - "bitflags 2.11.0", - "compact_str", - "hashbrown 0.16.1", - "indoc", - "itertools 0.14.0", - "kasuari", - "lru", - "strum", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" -dependencies = [ - "ratatui-core", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-termwiz" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" -dependencies = [ - "ratatui-core", - "termwiz", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools 0.14.0", - "line-clipping", - "ratatui-core", - "strum", - "time", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "rust-latex-parser" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b0949524549f0f83d8a14c71e9b378c45307a77bb6ea6bb969d1399d5ae134" - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "term-maths" -version = "1.0.0" -dependencies = [ - "crossterm", - "pulldown-latex", - "pyo3", - "pyo3-stub-gen", - "ratatui", - "rust-latex-parser", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom", - "phf", - "phf_codegen", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - -[[package]] -name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" -dependencies = [ - "anyhow", - "base64", - "bitflags 2.11.0", - "fancy-regex", - "filedescriptor", - "finl_unicode", - "fixedbitset", - "hex", - "lazy_static", - "libc", - "log", - "memmem", - "nix", - "num-derive", - "num-traits", - "ordered-float", - "pest", - "pest_derive", - "phf", - "sha2", - "signal-hook", - "siphasher", - "terminfo", - "termios", - "thiserror 1.0.69", - "ucd-trie", - "unicode-segmentation", - "vtparse", - "wezterm-bidi", - "wezterm-blob-leases", - "wezterm-color-types", - "wezterm-dynamic", - "wezterm-input-types", - "winapi", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools 0.14.0", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" -dependencies = [ - "atomic", - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vtparse" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wezterm-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" -dependencies = [ - "log", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-blob-leases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" -dependencies = [ - "getrandom 0.3.4", - "mac_address", - "sha2", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "wezterm-color-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" -dependencies = [ - "csscolorparser", - "deltae", - "lazy_static", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-dynamic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" -dependencies = [ - "log", - "ordered-float", - "strsim", - "thiserror 1.0.69", - "wezterm-dynamic-derive", -] - -[[package]] -name = "wezterm-dynamic-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "wezterm-input-types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" -dependencies = [ - "bitflags 1.3.2", - "euclid", - "lazy_static", - "serde", - "wezterm-dynamic", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/vendor/term-maths/Cargo.toml b/vendor/term-maths/Cargo.toml deleted file mode 100644 index dc05c24..0000000 --- a/vendor/term-maths/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "term-maths" -version = "1.0.0" -edition = "2024" -description = "Character-grid mathematical notation renderer for terminals --- LaTeX math to 2D Unicode art" -license = "MIT OR Apache-2.0" -keywords = ["math", "latex", "terminal", "unicode", "rendering"] -categories = ["command-line-interface", "text-processing", "visualization"] -exclude = [ - "python/", - "pyproject.toml", - ".github/", - "docs/", - ".venv/", -] - -[lib] -# rlib for Rust consumers; cdylib for the Python extension (required by maturin) -crate-type = ["rlib", "cdylib"] - -[dependencies] -crossterm = { version = "0.29.0", optional = true } -pulldown-latex = { version = "0.7.1", optional = true } -ratatui = { version = "0.30.0", optional = true } -rust-latex-parser = "0.1.0" -unicode-segmentation = "1.13.2" -unicode-width = "0.2.2" -pyo3 = { version = "0.23", features = ["experimental-inspect", "abi3-py310"], optional = true } -pyo3-stub-gen = { version = "0.7", optional = true } - -[features] -ratatui = ["dep:ratatui"] -crossterm = ["dep:crossterm"] -pulldown-latex = ["dep:pulldown-latex"] -python = ["dep:pyo3", "dep:pyo3-stub-gen"] - -[[bin]] -name = "stub_gen" -path = "src/bin/stub_gen.rs" -required-features = ["python"] diff --git a/vendor/term-maths/LICENSE-APACHE b/vendor/term-maths/LICENSE-APACHE deleted file mode 100644 index d0c491f..0000000 --- a/vendor/term-maths/LICENSE-APACHE +++ /dev/null @@ -1,190 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to the Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by the Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding any notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -Copyright 2026 Jack Geraghty - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/vendor/term-maths/LICENSE-MIT b/vendor/term-maths/LICENSE-MIT deleted file mode 100644 index f507e99..0000000 --- a/vendor/term-maths/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Jack Geraghty - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/term-maths/README.md b/vendor/term-maths/README.md deleted file mode 100644 index c4cbce1..0000000 --- a/vendor/term-maths/README.md +++ /dev/null @@ -1,283 +0,0 @@ -# term-maths - -Character-grid mathematical notation renderer for terminals, implemented in Rust. - -Accepts LaTeX math input and renders it as 2D Unicode character art in a terminal. Targets [JuliaMono](https://juliamono.netlify.app/) as the recommended font for full Unicode math symbol coverage. - -Available as both a **Rust crate** and a **Python package** (via PyO3 + Maturin). - -## Rust Usage - -Add to your `Cargo.toml`: - -```toml -[dependencies] -term-maths = "0.1" -``` - -Render a LaTeX expression: - -```rust -let block = term_maths::render(r"\frac{a}{b}"); -println!("{}", block); -``` - -Output: - -```text - a -─── - b -``` - -## Python Usage - -Requires a Rust toolchain and [maturin](https://www.maturin.rs): - -```sh -pip install maturin -maturin develop --features python # from the repo root -``` - -```python -import term_maths - -block = term_maths.render(r"\frac{a}{b}") -print(block) -# a -# ─── -# b - -# Compose blocks side-by-side (baseline-aligned) -lhs = term_maths.render(r"\frac{a}{b}") -rhs = term_maths.render(r"\frac{c}{d}") -sep = term_maths.RenderedBlock.from_text(" = ") -print(lhs.beside(sep).beside(rhs)) - -# Unicode math fonts -print(term_maths.map_str("blackboard", "NZQRC")) # ℕℤℚℝℂ - -# LaTeX round-trip -print(term_maths.to_latex(r"x^2 + y^2")) -``` - -See `python/examples/` for more: `render_demo.py`, `dsp_equations.py`, -`block_composition.py`, and `math_fonts.py`. - -## Rendering Examples - -All output below is produced directly by the library. - -### Fractions and Arithmetic - -```text ---- \frac{a}{b} --- - - a -─── - b - ---- \frac{1}{1+\frac{1}{x}} --- - - 1 -───────── - 1 - 1 + ─── - x - ---- \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} --- - - ──────── - -b ± √b² - 4ac -──────────────── - 2a -``` - -### Superscripts, Subscripts, and Inline Unicode - -```text -x^2 → x² -a_n → aₙ -x_i^2 → x²ᵢ -a + b = c → a + b = c -x^2 + y^2 → x² + y² = z² -``` - -### Big Operators with Limits - -```text ---- \sum_{n=0}^{N-1} --- - -N - 1 - ∑ -n = 0 - ---- \int_{0}^{1} --- - -1 -⌠ -⎮ -⌡ -0 - ---- \prod_{i=1}^{n} --- - - n - ∏ -i = 1 -``` - -### DSP Reference Equations - -```text ---- DFT Summation: X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j \frac{2\pi}{N} kn} --- - - 2π - -j ──── kn - N - 1 N -X[k] = ∑ x[n] ·e - n = 0 - ---- Convolution Integral --- - - ∞ - ⌠ -(f · g)(t) = ⎮ f(τ) g(t - τ) dτ - ⌡ - -∞ - ---- Transfer Function --- - - b₀ + b₁ z⁻¹ + b₂ z⁻² -H(z) = ────────────────────── - 1 + a₁ z⁻¹ + a₂ z⁻² - ---- Hann Window --- - - ⎛ ⎛ 2πn ⎞⎞ -w(n) = 0.5 ⎜1 - cos⎜───────⎟⎟ - ⎝ ⎝ N - 1 ⎠⎠ -``` - -### Matrices - -```text ---- pmatrix --- --- bmatrix --- --- vmatrix --- - -⎛a b⎞ ⎡1 0⎤ │a b│ -⎝c d⎠ ⎣0 1⎦ │c d│ - ---- 3x3 bmatrix --- - -⎡1 2 3⎤ -⎢4 5 6⎥ -⎣7 8 9⎦ - ---- Matrix with fractions --- - -⎛ 1 ⎞ -⎜─── 0 ⎟ -⎜ 2 ⎟ -⎜ 3 ⎟ -⎜ 0 ───⎟ -⎝ 4 ⎠ -``` - -### Delimiters, Sqrt, and Accents - -```text ---- \left(\frac{a}{b}\right) --- - -⎛ a ⎞ -⎜───⎟ -⎝ b ⎠ - ---- \sqrt{\frac{a}{b}} --- - - ─── -│ a -│─── -√ b - ---- \overline{x + y} --- - -‾‾‾‾‾ -x + y -``` - -### Math Fonts (Unicode Mathematical Alphanumeric Symbols) - -```text -\mathbb{R} → ℝ -\mathbb{Z} → ℤ -\mathcal{L} → ℒ -\mathbf{x} → 𝐱 -\mathfrak{g} → 𝔤 -\mathbb{R}^n → ℝⁿ -``` - -## Output Backends - -**Core** (always available): - -- Plain text via `render()` and `Display` -- LaTeX round-trip via `to_latex()` - -**Optional** (feature-gated): - -```toml -[dependencies] -term-maths = { version = "0.1", features = ["crossterm", "ratatui"] } -``` - -| Feature | Backend | Description | -|-------------|---------------------|--------------------------------------------------------| -| `crossterm` | `CrosstermRenderer` | Direct terminal output with cursor positioning | -| `ratatui` | `MathWidget` | TUI widget implementing `ratatui::Widget` | -| `python` | PyO3 extension | Python bindings (`maturin build --features python`) | - -### Crossterm - -```rust -use term_maths::{render, CrosstermRenderer}; - -let block = render(r"\sum_{i=0}^{n} x_i"); -CrosstermRenderer::print_at(&block, 0, 0)?; -``` - -### Ratatui - -```rust -use term_maths::{render, MathWidget}; - -let block = render(r"\frac{a}{b}"); -let widget = MathWidget::new(&block); -widget.render(area, buf); -``` - -### LaTeX Round-Trip - -```rust -let latex = term_maths::to_latex(r"x^2 + y^2"); -// "x^{2} \;+\; y^{2}" -``` - -## Font Recommendation - -For best results, use [JuliaMono](https://juliamono.netlify.app/). It provides complete coverage of: - -- Mathematical Alphanumeric Symbols (U+1D400-U+1D7FF) for bold, italic, script, fraktur, double-struck, sans-serif variants -- Box-drawing and bracket piece characters for delimiters and integrals -- Full Greek alphabet and mathematical operators -- Superscript/subscript digits and letters - -Other monospace fonts will work but may show fallback glyphs for some mathematical symbols. - -## License - -Licensed under either of - -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) -- MIT License ([LICENSE-MIT](LICENSE-MIT) or ) - -at your option. diff --git a/vendor/term-maths/examples/crossterm_demo.rs b/vendor/term-maths/examples/crossterm_demo.rs deleted file mode 100644 index a5576c4..0000000 --- a/vendor/term-maths/examples/crossterm_demo.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Demonstrates the crossterm renderer backend. -//! -//! Run with: cargo run --example crossterm_demo --features crossterm -//! -//! This example uses cursor positioning to render the equation at a specific -//! location in the terminal. It must be run in a real terminal (not piped). - -#[cfg(feature = "crossterm")] -fn main() -> std::io::Result<()> { - use crossterm::{cursor, execute, tty::IsTty}; - use std::io::{Write, stdout}; - use term_maths::{CrosstermRenderer, render}; - - let block = render(r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"); - - let mut stdout = stdout(); - - if !stdout.is_tty() { - // Fallback: just print via Display when not in a real terminal - println!("Crossterm renderer demo — quadratic formula:\n"); - println!("{}", block); - return Ok(()); - } - - println!("Crossterm renderer demo — quadratic formula:\n"); - - // Reserve vertical space by printing blank lines, then move back up - for _ in 0..block.height() { - println!(); - } - - // Move cursor back to the start of the reserved space - let (col, row) = cursor::position()?; - let start_row = row.saturating_sub(block.height() as u16); - CrosstermRenderer::render_at(&mut stdout, &block, col, start_row)?; - - // Move cursor below the rendered block - execute!(stdout, cursor::MoveTo(0, row))?; - stdout.flush()?; - println!(); - - Ok(()) -} - -#[cfg(not(feature = "crossterm"))] -fn main() { - eprintln!("This example requires the `crossterm` feature."); - eprintln!("Run with: cargo run --example crossterm_demo --features crossterm"); -} diff --git a/vendor/term-maths/examples/debug_ast.rs b/vendor/term-maths/examples/debug_ast.rs deleted file mode 100644 index 7d4d8b6..0000000 --- a/vendor/term-maths/examples/debug_ast.rs +++ /dev/null @@ -1,20 +0,0 @@ -use rust_latex_parser::parse_equation; - -fn main() { - for expr in std::env::args().skip(1) { - println!("=== {} ===", expr); - println!("{:#?}", parse_equation(&expr)); - println!(); - } - if std::env::args().len() <= 1 { - // Defaults - for expr in [ - r"a + b = c", - r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", - ] { - println!("=== {} ===", expr); - println!("{:#?}", parse_equation(expr)); - println!(); - } - } -} diff --git a/vendor/term-maths/examples/dsp_equations.rs b/vendor/term-maths/examples/dsp_equations.rs deleted file mode 100644 index 508de96..0000000 --- a/vendor/term-maths/examples/dsp_equations.rs +++ /dev/null @@ -1,58 +0,0 @@ -use term_maths::render; - -fn main() { - let equations = [ - // DFT summation - ( - r"X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j \frac{2\pi}{N} kn}", - "DFT Summation", - ), - // Convolution integral - ( - r"(f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) \, d\tau", - "Convolution Integral", - ), - // Transfer function - ( - r"H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}}", - "Transfer Function", - ), - // Hann window - ( - r"w(n) = 0.5 \left(1 - \cos\left(\frac{2\pi n}{N - 1}\right)\right)", - "Hann Window", - ), - ]; - - for (latex, label) in &equations { - println!("=== {} ===", label); - println!("LaTeX: {}", latex); - println!(); - println!("{}", render(latex)); - println!(); - } - - // Also test individual components - println!("=== Standalone tests ===\n"); - - println!("--- Sum with limits ---"); - println!("{}\n", render(r"\sum_{n=0}^{N-1}")); - - println!("--- Integral with limits ---"); - println!("{}\n", render(r"\int_{0}^{1}")); - - println!("--- Product with limits ---"); - println!("{}\n", render(r"\prod_{i=1}^{n}")); - - println!("--- Delimited fraction ---"); - println!("{}\n", render(r"\left(\frac{a}{b}\right)")); - - println!("--- Overline ---"); - println!("{}\n", render(r"\overline{x + y}")); - - println!("--- Hat ---"); - println!("{}\n", render(r"\hat{x}")); - - println!("--- Sqrt of fraction ---"); - println!("{}\n", render(r"\sqrt{\frac{a}{b}}")); -} diff --git a/vendor/term-maths/examples/latex_roundtrip.rs b/vendor/term-maths/examples/latex_roundtrip.rs deleted file mode 100644 index 1d5e947..0000000 --- a/vendor/term-maths/examples/latex_roundtrip.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Demonstrates the LaTeX renderer (round-trip serialisation). -//! -//! Run with: cargo run --example latex_roundtrip - -use term_maths::{render, to_latex}; - -fn main() { - let examples = [ - r"\frac{a}{b}", - r"x^2 + y^2 = z^2", - r"\sum_{i=1}^{n} x_i", - r"\sqrt{b^2 - 4ac}", - r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", - r"\mathbb{R}^n", - ]; - - for latex in &examples { - println!("Original: {}", latex); - let roundtrip = to_latex(latex); - println!("Round-trip: {}", roundtrip.trim()); - println!("Rendered:"); - println!("{}", render(latex)); - println!(); - } -} diff --git a/vendor/term-maths/examples/matrix_demo.rs b/vendor/term-maths/examples/matrix_demo.rs deleted file mode 100644 index 8e3c30a..0000000 --- a/vendor/term-maths/examples/matrix_demo.rs +++ /dev/null @@ -1,41 +0,0 @@ -use term_maths::render; - -fn main() { - let examples = [ - ( - r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", - "2x2 pmatrix", - ), - ( - r"\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}", - "2x2 identity bmatrix", - ), - ( - r"\begin{vmatrix} a & b \\ c & d \end{vmatrix}", - "2x2 determinant", - ), - ( - r"\begin{pmatrix} \frac{1}{2} & 0 \\ 0 & \frac{3}{4} \end{pmatrix}", - "Matrix with fractions", - ), - ( - r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}", - "3x3 bmatrix", - ), - // Math font tests - (r"\mathbb{R}", "Blackboard bold R"), - (r"\mathbb{Z}", "Blackboard bold Z"), - (r"\mathcal{L}", "Calligraphic L"), - (r"\mathbf{x}", "Bold x"), - (r"\mathfrak{g}", "Fraktur g"), - (r"\mathbb{R}^n", "R^n"), - ]; - - for (latex, label) in &examples { - println!("--- {} ---", label); - println!("LaTeX: {}", latex); - println!(); - println!("{}", render(latex)); - println!(); - } -} diff --git a/vendor/term-maths/examples/ratatui_demo.rs b/vendor/term-maths/examples/ratatui_demo.rs deleted file mode 100644 index 506d4d5..0000000 --- a/vendor/term-maths/examples/ratatui_demo.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Demonstrates the ratatui widget backend. -//! -//! Run with: cargo run --example ratatui_demo --features ratatui - -#[cfg(feature = "ratatui")] -fn main() { - use ratatui::buffer::Buffer; - use ratatui::layout::Rect; - use ratatui::widgets::Widget; - use term_maths::{MathWidget, render}; - - let block = render(r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"); - - // Create a buffer large enough to hold the rendered block - let area = Rect::new(0, 0, block.width() as u16 + 2, block.height() as u16 + 1); - let mut buf = Buffer::empty(area); - - // Render the widget into the buffer - let widget = MathWidget::new(&block); - widget.render(area, &mut buf); - - // Print the buffer contents (simulating what ratatui would display) - println!("Ratatui widget demo — quadratic formula:\n"); - for y in 0..area.height { - for x in 0..area.width { - let cell = &buf[(x, y)]; - print!("{}", cell.symbol()); - } - println!(); - } -} - -#[cfg(not(feature = "ratatui"))] -fn main() { - eprintln!("This example requires the `ratatui` feature."); - eprintln!("Run with: cargo run --example ratatui_demo --features ratatui"); -} diff --git a/vendor/term-maths/examples/render_demo.rs b/vendor/term-maths/examples/render_demo.rs deleted file mode 100644 index 4bd5b9b..0000000 --- a/vendor/term-maths/examples/render_demo.rs +++ /dev/null @@ -1,22 +0,0 @@ -use term_maths::render; - -fn main() { - let examples = [ - (r"\frac{a}{b}", "Simple fraction"), - (r"\frac{1}{1+\frac{1}{x}}", "Nested fraction"), - (r"x^2", "Superscript"), - (r"a_n", "Subscript"), - (r"x_i^2", "Super + subscript"), - (r"a + b = c", "Sequence"), - (r"e^{i\pi} + 1 = 0", "Euler's identity"), - (r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}", "Quadratic formula"), - ]; - - for (latex, label) in &examples { - println!("--- {} ---", label); - println!("LaTeX: {}", latex); - println!(); - println!("{}", render(latex)); - println!(); - } -} diff --git a/vendor/term-maths/pyproject.toml b/vendor/term-maths/pyproject.toml deleted file mode 100644 index ad183f5..0000000 --- a/vendor/term-maths/pyproject.toml +++ /dev/null @@ -1,46 +0,0 @@ -[build-system] -requires = ["maturin>=1.7,<2"] -build-backend = "maturin" - -[project] -name = "term-maths" -dynamic = ["version"] -description = "Character-grid mathematical notation renderer for terminals — LaTeX math to 2D Unicode art" -license = { text = "MIT OR Apache-2.0" } -requires-python = ">=3.10" -readme = "README.md" -keywords = ["math", "latex", "terminal", "unicode", "rendering"] -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Rust", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Terminals", - "Topic :: Text Processing :: Markup :: LaTeX", -] - -[tool.maturin] -# Enables the `python` Cargo feature when building the extension -features = ["python"] -# The dotted Python module path of the compiled extension. -# maturin places the .so file at python/term_maths/_term_maths.so -module-name = "term_maths._term_maths" -# The directory containing the Python package source -python-source = "python" -manifest-path = "Cargo.toml" -# Exclude from sdist: build artefacts, dev tools, and internal notes -exclude = [ - "target/", - ".venv/", - "docs/", - "python/docs/_build/", - ".github/", -] diff --git a/vendor/term-maths/python/docs/Makefile b/vendor/term-maths/python/docs/Makefile deleted file mode 100644 index 31b02ab..0000000 --- a/vendor/term-maths/python/docs/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -# Minimal Sphinx Makefile - -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -.PHONY: help Makefile - -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/vendor/term-maths/python/docs/api.rst b/vendor/term-maths/python/docs/api.rst deleted file mode 100644 index b5ba485..0000000 --- a/vendor/term-maths/python/docs/api.rst +++ /dev/null @@ -1,22 +0,0 @@ -API Reference -============= - -.. automodule:: term_maths - :members: - :undoc-members: - :special-members: __str__, __repr__ - :show-inheritance: - -.. rubric:: Functions - -.. autofunction:: term_maths.render -.. autofunction:: term_maths.to_latex -.. autofunction:: term_maths.map_char -.. autofunction:: term_maths.map_str - -.. rubric:: Classes - -.. autoclass:: term_maths.RenderedBlock - :members: - :undoc-members: - :special-members: __str__, __repr__ diff --git a/vendor/term-maths/python/docs/conf.py b/vendor/term-maths/python/docs/conf.py deleted file mode 100644 index 370b50b..0000000 --- a/vendor/term-maths/python/docs/conf.py +++ /dev/null @@ -1,61 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -import os -import sys - -# Make the term_maths package importable. -# The compiled extension (_term_maths.so) must be installed first: -# pip install -e . (from repo root, with maturin installed) -# or: -# maturin develop --features python -sys.path.insert(0, os.path.abspath("../../python")) - -# --------------------------------------------------------------------------- -# Project information -# --------------------------------------------------------------------------- - -project = "term-maths" -author = "Jack Geraghty" -copyright = f"2024, {author}" -release = "0.1.0" - -# --------------------------------------------------------------------------- -# General configuration -# --------------------------------------------------------------------------- - -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", # NumPy / Google-style docstrings - "sphinx_autodoc_typehints", # type hints from annotations / stubs - "sphinx.ext.viewcode", -] - -# sphinx-autodoc-typehints settings -always_document_param_types = True -typehints_fully_qualified = False -simplify_optional_unions = True - -# autodoc settings -autoclass_content = "both" # include both class and __init__ docstrings -autodoc_typehints = "description" # render type hints in the description, not signature -autodoc_member_order = "bysource" - -# --------------------------------------------------------------------------- -# HTML output -# --------------------------------------------------------------------------- - -html_theme = "furo" -html_title = "term-maths" -html_theme_options = { - "source_repository": "https://github.com/jmg049/term-maths", - "source_branch": "main", - "source_directory": "python/docs/", -} - -# --------------------------------------------------------------------------- -# Source files -# --------------------------------------------------------------------------- - -templates_path = ["_templates"] -exclude_patterns = ["_build"] diff --git a/vendor/term-maths/python/docs/examples.rst b/vendor/term-maths/python/docs/examples.rst deleted file mode 100644 index 8b22a39..0000000 --- a/vendor/term-maths/python/docs/examples.rst +++ /dev/null @@ -1,52 +0,0 @@ -Examples -======== - -All examples are in ``python/examples/`` and can be run after installing the package: - -.. code-block:: sh - - maturin develop --features python - python python/examples/render_demo.py - - -Render demo ------------ - -Basic rendering of common mathematical expressions. - -.. literalinclude:: ../examples/render_demo.py - :language: python - :caption: python/examples/render_demo.py - -DSP equations -------------- - -Signal-processing formulae including the DFT, convolution integral, IIR transfer -function, and Hann window. - -.. literalinclude:: ../examples/dsp_equations.py - :language: python - :caption: python/examples/dsp_equations.py - -Block composition ------------------ - -Demonstrates how to combine :class:`~term_maths.RenderedBlock` objects using -:meth:`~term_maths.RenderedBlock.beside`, :meth:`~term_maths.RenderedBlock.pad`, -:meth:`~term_maths.RenderedBlock.center_in`, :meth:`~term_maths.RenderedBlock.above`, -and :meth:`~term_maths.RenderedBlock.hline`. - -.. literalinclude:: ../examples/block_composition.py - :language: python - :caption: python/examples/block_composition.py - -Unicode math fonts ------------------- - -Shows :func:`~term_maths.map_char` and :func:`~term_maths.map_str` in action across -all supported font styles: bold, blackboard (double-struck), calligraphic, fraktur, -roman, sans-serif, and monospace. - -.. literalinclude:: ../examples/math_fonts.py - :language: python - :caption: python/examples/math_fonts.py diff --git a/vendor/term-maths/python/docs/index.rst b/vendor/term-maths/python/docs/index.rst deleted file mode 100644 index e488e61..0000000 --- a/vendor/term-maths/python/docs/index.rst +++ /dev/null @@ -1,50 +0,0 @@ -term-maths Python API -===================== - -**term-maths** renders LaTeX math expressions as 2D Unicode art for terminals. - -.. code-block:: python - - import term_maths - - block = term_maths.render(r"\frac{a}{b}") - print(block) - # a - # ─── - # b - - # Compose blocks - lhs = term_maths.render(r"x^2") - rhs = term_maths.render(r"y^2") - sep = term_maths.RenderedBlock.from_text(" + ") - combined = lhs.beside(sep).beside(rhs) - print(combined) - - # Unicode math fonts - print(term_maths.map_str("blackboard", "NZQRC")) # ℕℤℚℝℂ - -Contents --------- - -.. toctree:: - :maxdepth: 2 - - api - examples - -Installation ------------- - -Requires `maturin `_ and a Rust toolchain. - -.. code-block:: sh - - pip install maturin - maturin develop --features python # from the repo root - - -Indices -------- - -* :ref:`genindex` -* :ref:`modindex` diff --git a/vendor/term-maths/python/docs/requirements.txt b/vendor/term-maths/python/docs/requirements.txt deleted file mode 100644 index c4d1136..0000000 --- a/vendor/term-maths/python/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx>=7.0 -furo>=2024.0 -sphinx-autodoc-typehints>=2.0 diff --git a/vendor/term-maths/python/examples/block_composition.py b/vendor/term-maths/python/examples/block_composition.py deleted file mode 100644 index 7c7e0e8..0000000 --- a/vendor/term-maths/python/examples/block_composition.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Block composition demo. - -Shows how to build composite expressions by combining RenderedBlock objects -using beside(), pad(), center_in(), above(), and hline(). -""" - -import term_maths -from term_maths import RenderedBlock - - -def sep(text: str = " ") -> RenderedBlock: - """Convenience: create a separator block from plain text.""" - return RenderedBlock.from_text(text) - - -# --------------------------------------------------------------------------- -# 1. Side-by-side composition aligned on baselines -# --------------------------------------------------------------------------- -print("=== Side-by-side (baseline-aligned) ===\n") - -lhs = term_maths.render(r"\frac{a}{b}") -eq = sep(" = ") -rhs = term_maths.render(r"\frac{c}{d}") - -print(lhs.beside(eq).beside(rhs)) -print() - -# A tall block beside a short one — short block sits on the baseline -tall = term_maths.render(r"\frac{1}{1 + \frac{1}{x}}") -plus = sep(" + ") -short = term_maths.render(r"y") - -print(tall.beside(plus).beside(short)) -print() - -# --------------------------------------------------------------------------- -# 2. Horizontal centering under a fraction bar -# --------------------------------------------------------------------------- -print("=== Manual fraction construction ===\n") - -numerator = term_maths.render(r"a + b") -denominator = term_maths.render(r"c + d") -bar_width = max(numerator.width, denominator.width) + 2 -bar = RenderedBlock.hline("─", bar_width) - -num_c = numerator.center_in(bar_width) -den_c = denominator.center_in(bar_width) - -# Stack: numerator / bar / denominator; baseline is the bar row -fraction = RenderedBlock.above(num_c, bar, baseline_row=num_c.height) -fraction = RenderedBlock.above(fraction, den_c, baseline_row=num_c.height) - -print(fraction) -print() - -# --------------------------------------------------------------------------- -# 3. Padding and alignment -# --------------------------------------------------------------------------- -print("=== Padding ===\n") - -block = term_maths.render(r"x^2 + y^2") -padded = block.pad(left=2, right=2, top=1, bottom=1) -print(f"Original ({block.width}×{block.height}):") -print(block) -print(f"\nPadded ({padded.width}×{padded.height}):") -print(padded) -print() - -# --------------------------------------------------------------------------- -# 4. Accessing cells programmatically -# --------------------------------------------------------------------------- -print("=== Cell grid access ===\n") - -block = term_maths.render(r"\frac{1}{2}") -print(f"RenderedBlock: width={block.width}, height={block.height}, baseline={block.baseline}") -print(f"repr: {block!r}") -print() - -cells = block.cells() -for row_idx, row in enumerate(cells): - marker = " <-- baseline" if row_idx == block.baseline else "" - print(f" row {row_idx}: {row}{marker}") -print() - -# --------------------------------------------------------------------------- -# 5. Building a table of expressions -# --------------------------------------------------------------------------- -print("=== Expression table ===\n") - -expressions = [ - r"\sum_{k=0}^{n} k", - r"\frac{n(n+1)}{2}", -] - -blocks = [term_maths.render(e) for e in expressions] -eq_sep = sep(" = ") -composed = blocks[0].beside(eq_sep).beside(blocks[1]) -print(composed) -print() diff --git a/vendor/term-maths/python/examples/dsp_equations.py b/vendor/term-maths/python/examples/dsp_equations.py deleted file mode 100644 index b7f3e27..0000000 --- a/vendor/term-maths/python/examples/dsp_equations.py +++ /dev/null @@ -1,46 +0,0 @@ -"""DSP equations demo — Python equivalent of examples/dsp_equations.rs.""" - -import term_maths - -EQUATIONS = [ - ( - r"X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j \frac{2\pi}{N} kn}", - "DFT Summation", - ), - ( - r"(f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) \, d\tau", - "Convolution Integral", - ), - ( - r"H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}}", - "Transfer Function", - ), - ( - r"w(n) = 0.5 \left(1 - \cos\left(\frac{2\pi n}{N - 1}\right)\right)", - "Hann Window", - ), -] - -for latex, label in EQUATIONS: - print(f"=== {label} ===") - print(f"LaTeX: {latex}") - print() - print(term_maths.render(latex)) - print() - -print("=== Standalone operators ===\n") - -standalone = [ - (r"\sum_{n=0}^{N-1}", "Sum with limits"), - (r"\int_{0}^{1}", "Integral with limits"), - (r"\prod_{i=1}^{n}", "Product with limits"), - (r"\left(\frac{a}{b}\right)", "Delimited fraction"), - (r"\overline{x + y}", "Overline"), - (r"\hat{x}", "Hat"), - (r"\sqrt{\frac{a}{b}}", "Sqrt of fraction"), -] - -for latex, label in standalone: - print(f"--- {label} ---") - print(term_maths.render(latex)) - print() diff --git a/vendor/term-maths/python/examples/math_fonts.py b/vendor/term-maths/python/examples/math_fonts.py deleted file mode 100644 index 3f53251..0000000 --- a/vendor/term-maths/python/examples/math_fonts.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Unicode mathematical font demo. - -Shows how map_char() and map_str() transform ASCII letters and digits into -their Unicode Mathematical Alphanumeric Symbols equivalents. -""" - -import term_maths - -FONTS = ["bold", "blackboard", "calligraphic", "fraktur", "roman", "sans_serif", "monospace"] - -ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - -print("=== Font map (uppercase A–Z) ===\n") -sample = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -for font in FONTS: - mapped = term_maths.map_str(font, sample) - print(f" {font:<12}: {mapped}") - -print() - -print("=== Font map (lowercase a–z) ===\n") -sample = "abcdefghijklmnopqrstuvwxyz" -for font in FONTS: - mapped = term_maths.map_str(font, sample) - print(f" {font:<12}: {mapped}") - -print() - -print("=== Font map (digits 0–9) ===\n") -sample = "0123456789" -for font in FONTS: - mapped = term_maths.map_str(font, sample) - print(f" {font:<12}: {mapped}") - -print() - -print("=== Common mathematical sets ===\n") -sets = { - "Naturals ℕ": ("blackboard", "N"), - "Integers ℤ": ("blackboard", "Z"), - "Rationals ℚ": ("blackboard", "Q"), - "Reals ℝ": ("blackboard", "R"), - "Complex ℂ": ("blackboard", "C"), -} -for label, (font, ch) in sets.items(): - print(f" {label} → {term_maths.map_char(font, ch)}") - -print() - -print("=== Rendered with \\mathbb (via LaTeX parser) ===\n") -for expr, label in [ - (r"\mathbb{NZQRC}", "Common sets"), - (r"\mathbf{v}", "Bold vector"), - (r"\mathcal{L}", "Calligraphic L (Laplace)"), - (r"\mathfrak{g}", "Fraktur g (Lie algebra)"), -]: - print(f" {label}: {term_maths.render(expr)}") - -print() - -print("=== Error handling ===\n") -try: - term_maths.map_str("unknown_font", "hello") -except ValueError as e: - print(f" ValueError: {e}") diff --git a/vendor/term-maths/python/examples/render_demo.py b/vendor/term-maths/python/examples/render_demo.py deleted file mode 100644 index 631310e..0000000 --- a/vendor/term-maths/python/examples/render_demo.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Basic rendering demo — Python equivalent of examples/render_demo.rs.""" - -import term_maths - -EXAMPLES = [ - (r"\frac{a}{b}", "Simple fraction"), - (r"\frac{1}{1+\frac{1}{x}}", "Nested fraction"), - (r"x^2", "Superscript"), - (r"a_n", "Subscript"), - (r"x_i^2", "Super + subscript"), - (r"a + b = c", "Sequence"), - (r"e^{i\pi} + 1 = 0", "Euler's identity"), - (r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}", "Quadratic formula"), -] - -for latex, label in EXAMPLES: - print(f"--- {label} ---") - print(f"LaTeX: {latex}") - print() - print(term_maths.render(latex)) - print() diff --git a/vendor/term-maths/python/term_maths/__init__.py b/vendor/term-maths/python/term_maths/__init__.py deleted file mode 100644 index 78e7535..0000000 --- a/vendor/term-maths/python/term_maths/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -term_maths — Character-grid mathematical notation renderer. - -Renders LaTeX math expressions as 2D Unicode art suitable for terminal display. - -Quick start:: - - >>> import term_maths - >>> print(term_maths.render(r"\\frac{a}{b}")) - a - ─── - b - -The :class:`RenderedBlock` returned by :func:`render` can be composed further -using methods like :meth:`~RenderedBlock.beside`, :meth:`~RenderedBlock.pad`, -and :meth:`~RenderedBlock.center_in`. -""" - -from ._term_maths import ( - RenderedBlock, - render, - to_latex, - map_char, - map_str, -) - -__all__ = [ - "RenderedBlock", - "render", - "to_latex", - "map_char", - "map_str", -] diff --git a/vendor/term-maths/python/term_maths/py.typed b/vendor/term-maths/python/term_maths/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/vendor/term-maths/src/bin/stub_gen.rs b/vendor/term-maths/src/bin/stub_gen.rs deleted file mode 100644 index c5e605b..0000000 --- a/vendor/term-maths/src/bin/stub_gen.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Generates Python type stub files (.pyi) for the term_maths extension module. -//! -//! Run with: -//! -//! ```sh -//! cargo run --features python --bin stub_gen -//! ``` -//! -//! The stubs are written to `python/term_maths/_term_maths.pyi` (relative to -//! the workspace root). The output path is determined automatically by -//! pyo3-stub-gen by scanning upward for `pyproject.toml`. - -fn main() { - let stub = term_maths::python::stub_info_gatherer().expect("Failed to collect stub info"); - stub.generate().expect("Failed to generate Python stubs"); -} diff --git a/vendor/term-maths/src/crossterm_renderer.rs b/vendor/term-maths/src/crossterm_renderer.rs deleted file mode 100644 index b20def4..0000000 --- a/vendor/term-maths/src/crossterm_renderer.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Crossterm output backend — writes a `RenderedBlock` to the terminal -//! at a specified cursor position. -//! -//! Feature-gated behind `crossterm`. - -use std::io::Write; - -use crossterm::{cursor::MoveTo, execute, style::Print}; - -use crate::rendered_block::RenderedBlock; - -/// Renders a `RenderedBlock` to a terminal writer using crossterm commands. -pub struct CrosstermRenderer; - -impl CrosstermRenderer { - /// Write a rendered block to the terminal at the given (col, row) position. - pub fn render_at( - writer: &mut W, - block: &RenderedBlock, - col: u16, - row: u16, - ) -> std::io::Result<()> { - for (r, cells) in block.cells().iter().enumerate() { - execute!(writer, MoveTo(col, row + r as u16))?; - let line: String = cells.iter().map(|s| s.as_str()).collect(); - execute!(writer, Print(&line))?; - } - Ok(()) - } - - /// Write a rendered block to stdout at the given position. - pub fn print_at(block: &RenderedBlock, col: u16, row: u16) -> std::io::Result<()> { - let mut stdout = std::io::stdout(); - Self::render_at(&mut stdout, block, col, row) - } -} diff --git a/vendor/term-maths/src/latex_renderer.rs b/vendor/term-maths/src/latex_renderer.rs deleted file mode 100644 index dd770c1..0000000 --- a/vendor/term-maths/src/latex_renderer.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! LaTeX renderer — serialises an `EqNode` AST back to a LaTeX string. - -use rust_latex_parser::{AccentKind, EqNode, MathFontKind, MatrixKind}; - -use crate::renderer::MathRenderer; - -/// Serialises an `EqNode` back to a LaTeX math string. -pub struct LatexRenderer; - -impl MathRenderer for LatexRenderer { - type Output = String; - - fn render(&self, node: &EqNode) -> String { - node_to_latex(node) - } -} - -fn node_to_latex(node: &EqNode) -> String { - match node { - EqNode::Text(s) => latex_escape_text(s), - EqNode::Space(pts) => space_to_latex(*pts), - EqNode::Seq(children) => children.iter().map(node_to_latex).collect(), - EqNode::Frac(num, den) => { - format!(r"\frac{{{}}}{{{}}}", node_to_latex(num), node_to_latex(den)) - } - EqNode::Sup(base, sup) => { - format!("{}^{{{}}}", node_to_latex(base), node_to_latex(sup)) - } - EqNode::Sub(base, sub) => { - format!("{}_{{{}}} ", node_to_latex(base), node_to_latex(sub)) - } - EqNode::SupSub(base, sup, sub) => { - format!( - "{}^{{{}}}_{{{}}}", - node_to_latex(base), - node_to_latex(sup), - node_to_latex(sub) - ) - } - EqNode::Sqrt(body) => format!(r"\sqrt{{{}}}", node_to_latex(body)), - EqNode::BigOp { - symbol, - lower, - upper, - } => { - let sym = unicode_to_latex_op(symbol); - let mut s = sym; - if let Some(lo) = lower { - s.push_str(&format!("_{{{}}}", node_to_latex(lo))); - } - if let Some(up) = upper { - s.push_str(&format!("^{{{}}}", node_to_latex(up))); - } - s - } - EqNode::Accent(body, kind) => { - let cmd = match kind { - AccentKind::Hat => r"\hat", - AccentKind::Bar => r"\overline", - AccentKind::Dot => r"\dot", - AccentKind::DoubleDot => r"\ddot", - AccentKind::Tilde => r"\tilde", - AccentKind::Vec => r"\vec", - }; - format!("{}{{{}}}", cmd, node_to_latex(body)) - } - EqNode::Limit { name, lower } => { - let latex_name = format!(r"\{}", name); - if let Some(lo) = lower { - format!("{}_{{{}}}", latex_name, node_to_latex(lo)) - } else { - latex_name - } - } - EqNode::TextBlock(s) => format!(r"\text{{{}}}", s), - EqNode::MathFont { kind, content } => { - let cmd = match kind { - MathFontKind::Bold => r"\mathbf", - MathFontKind::Blackboard => r"\mathbb", - MathFontKind::Calligraphic => r"\mathcal", - MathFontKind::Roman => r"\mathrm", - MathFontKind::Fraktur => r"\mathfrak", - MathFontKind::SansSerif => r"\mathsf", - MathFontKind::Monospace => r"\mathtt", - }; - format!("{}{{{}}}", cmd, node_to_latex(content)) - } - EqNode::Delimited { - left, - right, - content, - } => { - format!( - r"\left{} {} \right{}", - latex_delim(left), - node_to_latex(content), - latex_delim(right) - ) - } - EqNode::Matrix { kind, rows } => { - let env = match kind { - MatrixKind::Plain => "matrix", - MatrixKind::Paren => "pmatrix", - MatrixKind::Bracket => "bmatrix", - MatrixKind::Brace => "Bmatrix", - MatrixKind::VBar => "vmatrix", - MatrixKind::DoubleVBar => "Vmatrix", - }; - let rows_str: Vec = rows - .iter() - .map(|row| { - row.iter() - .map(node_to_latex) - .collect::>() - .join(" & ") - }) - .collect(); - format!( - r"\begin{{{}}} {} \end{{{}}}", - env, - rows_str.join(r" \\ "), - env - ) - } - EqNode::Cases { rows } => { - let rows_str: Vec = rows - .iter() - .map(|(val, cond)| { - if let Some(c) = cond { - format!("{} & {}", node_to_latex(val), node_to_latex(c)) - } else { - node_to_latex(val) - } - }) - .collect(); - format!(r"\begin{{cases}} {} \end{{cases}}", rows_str.join(r" \\ ")) - } - EqNode::Binom(top, bottom) => { - format!( - r"\binom{{{}}}{{{}}}", - node_to_latex(top), - node_to_latex(bottom) - ) - } - EqNode::Brace { - content, - label, - over, - } => { - let cmd = if *over { r"\overbrace" } else { r"\underbrace" }; - let mut s = format!("{}{{{}}}", cmd, node_to_latex(content)); - if let Some(lbl) = label { - if *over { - s.push_str(&format!("^{{{}}}", node_to_latex(lbl))); - } else { - s.push_str(&format!("_{{{}}}", node_to_latex(lbl))); - } - } - s - } - EqNode::StackRel { - base, - annotation, - over, - } => { - let cmd = if *over { r"\overset" } else { r"\underset" }; - format!( - "{}{{{}}}{{{}}}", - cmd, - node_to_latex(annotation), - node_to_latex(base) - ) - } - } -} - -/// Escape special LaTeX characters in text content. -fn latex_escape_text(s: &str) -> String { - // Map common Unicode back to LaTeX commands - let mut result = String::new(); - for ch in s.chars() { - match ch { - 'α' => result.push_str(r"\alpha "), - 'β' => result.push_str(r"\beta "), - 'γ' => result.push_str(r"\gamma "), - 'δ' => result.push_str(r"\delta "), - 'ε' => result.push_str(r"\epsilon "), - 'ζ' => result.push_str(r"\zeta "), - 'η' => result.push_str(r"\eta "), - 'θ' => result.push_str(r"\theta "), - 'ι' => result.push_str(r"\iota "), - 'κ' => result.push_str(r"\kappa "), - 'λ' => result.push_str(r"\lambda "), - 'μ' => result.push_str(r"\mu "), - 'ν' => result.push_str(r"\nu "), - 'ξ' => result.push_str(r"\xi "), - 'π' => result.push_str(r"\pi "), - 'ρ' => result.push_str(r"\rho "), - 'σ' => result.push_str(r"\sigma "), - 'τ' => result.push_str(r"\tau "), - 'υ' => result.push_str(r"\upsilon "), - 'φ' => result.push_str(r"\phi "), - 'χ' => result.push_str(r"\chi "), - 'ψ' => result.push_str(r"\psi "), - 'ω' => result.push_str(r"\omega "), - '∞' => result.push_str(r"\infty "), - '∑' => result.push_str(r"\sum "), - '∏' => result.push_str(r"\prod "), - '∫' => result.push_str(r"\int "), - '±' => result.push_str(r"\pm "), - '·' => result.push_str(r"\cdot "), - '→' => result.push_str(r"\rightarrow "), - '←' => result.push_str(r"\leftarrow "), - '≤' => result.push_str(r"\leq "), - '≥' => result.push_str(r"\geq "), - '≠' => result.push_str(r"\neq "), - '∈' => result.push_str(r"\in "), - '∀' => result.push_str(r"\forall "), - '∃' => result.push_str(r"\exists "), - '∂' => result.push_str(r"\partial "), - '∇' => result.push_str(r"\nabla "), - _ => result.push(ch), - } - } - result -} - -fn space_to_latex(pts: f32) -> String { - if pts < 0.0 { - r"\!".to_string() - } else if pts < 3.0 { - r"\,".to_string() - } else if pts < 5.0 { - r"\;".to_string() - } else if pts >= 18.0 { - r"\quad ".to_string() - } else { - " ".to_string() - } -} - -fn unicode_to_latex_op(symbol: &str) -> String { - match symbol { - "∑" => r"\sum".to_string(), - "∏" => r"\prod".to_string(), - "∫" => r"\int".to_string(), - "∬" => r"\iint".to_string(), - "∮" => r"\oint".to_string(), - "⋃" => r"\bigcup".to_string(), - "⋂" => r"\bigcap".to_string(), - "⊕" => r"\bigoplus".to_string(), - "⊗" => r"\bigotimes".to_string(), - _ => symbol.to_string(), - } -} - -fn latex_delim(d: &str) -> String { - match d { - "." => ".".to_string(), - "(" | ")" | "[" | "]" | "|" => d.to_string(), - "{" => r"\{".to_string(), - "}" => r"\}".to_string(), - "‖" => r"\|".to_string(), - _ => d.to_string(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::renderer::MathRenderer; - use rust_latex_parser::parse_equation; - - #[test] - fn test_simple_fraction_roundtrip() { - let renderer = LatexRenderer; - let ast = parse_equation(r"\frac{a}{b}"); - let latex = renderer.render(&ast); - assert!(latex.contains(r"\frac")); - assert!(latex.contains('a')); - assert!(latex.contains('b')); - } - - #[test] - fn test_superscript_roundtrip() { - let renderer = LatexRenderer; - let ast = parse_equation(r"x^2"); - let latex = renderer.render(&ast); - assert!(latex.contains("x^")); - assert!(latex.contains('2')); - } - - #[test] - fn test_matrix_roundtrip() { - let renderer = LatexRenderer; - let ast = parse_equation(r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}"); - let latex = renderer.render(&ast); - assert!(latex.contains("pmatrix")); - assert!(latex.contains('&')); - } -} diff --git a/vendor/term-maths/src/layout.rs b/vendor/term-maths/src/layout.rs deleted file mode 100644 index e72ed0f..0000000 --- a/vendor/term-maths/src/layout.rs +++ /dev/null @@ -1,927 +0,0 @@ -use rust_latex_parser::{AccentKind, EqNode, MathFontKind, MatrixKind}; - -use crate::mathfont; -use crate::rendered_block::RenderedBlock; - -/// Render an `EqNode` AST into a `RenderedBlock`. -pub fn layout(node: &EqNode) -> RenderedBlock { - match node { - EqNode::Text(s) => layout_text(s), - EqNode::Space(pts) => layout_space(*pts), - EqNode::Seq(children) => layout_seq(children), - EqNode::Frac(num, den) => layout_frac(num, den), - EqNode::Sup(base, sup) => layout_sup(base, sup), - EqNode::Sub(base, sub) => layout_sub(base, sub), - EqNode::SupSub(base, sup, sub) => layout_supsub(base, sup, sub), - EqNode::Sqrt(body) => layout_sqrt(body), - EqNode::BigOp { - symbol, - lower, - upper, - } => layout_bigop(symbol, lower, upper), - EqNode::Accent(body, kind) => layout_accent(body, kind), - EqNode::Limit { name, lower } => layout_limit(name, lower), - EqNode::TextBlock(s) => RenderedBlock::from_text(s), - EqNode::MathFont { kind, content } => layout_mathfont(kind, content), - EqNode::Delimited { - left, - right, - content, - } => layout_delimited(left, right, content), - EqNode::Matrix { kind, rows } => layout_matrix(kind, rows), - EqNode::Cases { rows } => layout_cases(rows), - EqNode::Binom(top, bottom) => layout_binom(top, bottom), - EqNode::Brace { - content, - label, - over, - } => layout_brace(content, label, over), - EqNode::StackRel { - base, - annotation, - over, - } => layout_stackrel(base, annotation, over), - } -} - -fn layout_text(s: &str) -> RenderedBlock { - RenderedBlock::from_text(s) -} - -/// Map a character to its Unicode superscript equivalent, if one exists. -fn to_superscript_char(ch: char) -> Option { - match ch { - '0' => Some('⁰'), - '1' => Some('¹'), - '2' => Some('²'), - '3' => Some('³'), - '4' => Some('⁴'), - '5' => Some('⁵'), - '6' => Some('⁶'), - '7' => Some('⁷'), - '8' => Some('⁸'), - '9' => Some('⁹'), - '+' => Some('⁺'), - '-' => Some('⁻'), - '=' => Some('⁼'), - '(' => Some('⁽'), - ')' => Some('⁾'), - 'n' => Some('ⁿ'), - 'i' => Some('ⁱ'), - _ => None, - } -} - -/// Map a character to its Unicode subscript equivalent, if one exists. -fn to_subscript_char(ch: char) -> Option { - match ch { - '0' => Some('₀'), - '1' => Some('₁'), - '2' => Some('₂'), - '3' => Some('₃'), - '4' => Some('₄'), - '5' => Some('₅'), - '6' => Some('₆'), - '7' => Some('₇'), - '8' => Some('₈'), - '9' => Some('₉'), - '+' => Some('₊'), - '-' => Some('₋'), - '=' => Some('₌'), - '(' => Some('₍'), - ')' => Some('₎'), - 'a' => Some('ₐ'), - 'e' => Some('ₑ'), - 'h' => Some('ₕ'), - 'i' => Some('ᵢ'), - 'j' => Some('ⱼ'), - 'k' => Some('ₖ'), - 'l' => Some('ₗ'), - 'm' => Some('ₘ'), - 'n' => Some('ₙ'), - 'o' => Some('ₒ'), - 'p' => Some('ₚ'), - 'r' => Some('ᵣ'), - 's' => Some('ₛ'), - 't' => Some('ₜ'), - 'u' => Some('ᵤ'), - 'v' => Some('ᵥ'), - 'x' => Some('ₓ'), - _ => None, - } -} - -/// Try to convert a node's text content to Unicode superscript characters. -/// Returns None if any character lacks a superscript form. -fn try_unicode_superscript(node: &EqNode) -> Option { - let text = extract_flat_text(node)?; - text.chars().map(to_superscript_char).collect() -} - -/// Try to convert a node's text content to Unicode subscript characters. -fn try_unicode_subscript(node: &EqNode) -> Option { - let text = extract_flat_text(node)?; - text.chars().map(to_subscript_char).collect() -} - -/// Extract flat text from simple nodes (Text, Seq of Text). -fn extract_flat_text(node: &EqNode) -> Option { - match node { - EqNode::Text(s) => Some(s.clone()), - EqNode::Seq(children) => { - let mut result = String::new(); - for child in children { - match child { - EqNode::Text(s) => result.push_str(s), - EqNode::Space(_) => {} // skip spaces in scripts - _ => return None, - } - } - if result.is_empty() { - None - } else { - Some(result) - } - } - _ => None, - } -} - -/// Render a Space node. The parser auto-inserts Space nodes around operators. -/// Negative and very small spaces collapse. Standard operator spaces (3–5pt) -/// become a single space. Larger explicit spaces (\quad etc.) grow accordingly. -fn layout_space(pts: f32) -> RenderedBlock { - if pts <= 0.0 || pts < 2.0 { - RenderedBlock::empty() - } else if pts >= 18.0 { - // \quad or larger - RenderedBlock::from_text(" ") - } else { - RenderedBlock::from_char(' ') - } -} - -/// Check if a node is whitespace-like (Space node or Text containing only spaces). -fn is_space_like(node: &EqNode) -> bool { - match node { - EqNode::Space(_) => true, - EqNode::Text(s) => s.chars().all(|c| c == ' '), - _ => false, - } -} - -fn layout_seq(children: &[EqNode]) -> RenderedBlock { - // Flatten nested Seqs so we can handle spacing uniformly. - let flat = flatten_seq(children); - // Collapse consecutive whitespace-like nodes into a single space. - let mut result = RenderedBlock::empty(); - let mut prev_was_space = false; - for child in &flat { - if is_space_like(child) { - if !prev_was_space { - prev_was_space = true; - result = result.beside(&RenderedBlock::from_char(' ')); - } - continue; - } - prev_was_space = false; - let block = layout(child); - result = result.beside(&block); - } - result -} - -/// Trim leading/trailing whitespace from a node. -/// Strips Space nodes and whitespace-only Text nodes from Seq boundaries. -fn trim_node(node: &EqNode) -> EqNode { - match node { - EqNode::Seq(children) => { - let trimmed: Vec = children - .iter() - .map(|c| match c { - EqNode::Text(s) => EqNode::Text(s.trim().to_string()), - other => other.clone(), - }) - .filter(|c| !is_space_like(c) || !matches!(c, EqNode::Text(s) if s.is_empty())) - .collect(); - // Remove leading/trailing space-like nodes - let start = trimmed.iter().position(|c| !is_space_like(c)).unwrap_or(0); - let end = trimmed - .iter() - .rposition(|c| !is_space_like(c)) - .map_or(0, |i| i + 1); - if start >= end { - return EqNode::Seq(vec![]); - } - EqNode::Seq(trimmed[start..end].to_vec()) - } - EqNode::Text(s) => EqNode::Text(s.trim().to_string()), - other => other.clone(), - } -} - -/// Recursively flatten nested Seq nodes into a single flat list. -fn flatten_seq(children: &[EqNode]) -> Vec<&EqNode> { - let mut result = Vec::new(); - for child in children { - if let EqNode::Seq(inner) = child { - result.extend(flatten_seq(inner)); - } else { - result.push(child); - } - } - result -} - -fn layout_frac(num: &EqNode, den: &EqNode) -> RenderedBlock { - let num_block = layout(num); - let den_block = layout(den); - - let bar_width = num_block.width().max(den_block.width()) + 2; // +2 for padding - let bar = RenderedBlock::hline('─', bar_width); - - let num_centered = num_block.center_in(bar_width); - let den_centered = den_block.center_in(bar_width); - - // Stack: numerator, bar, denominator. Baseline is the bar row. - let top = RenderedBlock::above(&num_centered, &bar, 0); - let baseline_row = top.height() - 1; // bar is the last row of 'top' - RenderedBlock::above(&top, &den_centered, baseline_row) -} - -fn layout_sup(base: &EqNode, sup: &EqNode) -> RenderedBlock { - // Try inline Unicode superscript first - if let Some(sup_text) = try_unicode_superscript(sup) { - let base_block = layout(base); - let sup_block = RenderedBlock::from_text(&sup_text); - return base_block.beside(&sup_block); - } - - let base_block = layout(base); - let sup_block = layout(sup); - - let can_overlap = base_block.height() > 1; - let sup_above = if can_overlap { - sup_block.height().saturating_sub(1) - } else { - sup_block.height() - }; - - let rows = build_sup_sub_grid( - base_block.cells(), - base_block.width(), - base_block.baseline(), - sup_block.cells(), - sup_block.width(), - None, - 0, - ); - - let total_height = rows.len(); - let baseline = sup_above + base_block.baseline(); - - RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1))) -} - -fn layout_sub(base: &EqNode, sub: &EqNode) -> RenderedBlock { - // Try inline Unicode subscript first - if let Some(sub_text) = try_unicode_subscript(sub) { - let base_block = layout(base); - let sub_block = RenderedBlock::from_text(&sub_text); - return base_block.beside(&sub_block); - } - - let base_block = layout(base); - let sub_block = layout(sub); - - let rows = build_sup_sub_grid( - base_block.cells(), - base_block.width(), - base_block.baseline(), - &[], - 0, - Some((sub_block.cells(), sub_block.width())), - 0, - ); - - let baseline = base_block.baseline(); - let total_height = rows.len(); - - RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1))) -} - -fn layout_supsub(base: &EqNode, sup: &EqNode, sub: &EqNode) -> RenderedBlock { - // Try inline Unicode for both scripts - let sup_inline = try_unicode_superscript(sup); - let sub_inline = try_unicode_subscript(sub); - - if let (Some(sup_text), Some(sub_text)) = (&sup_inline, &sub_inline) { - let base_block = layout(base); - let scripts = format!("{}{}", sup_text, sub_text); - // Subscript chars go right after superscript chars, all inline - // Actually stack them: sup on same line, sub on same line - // For compactness: base followed by sup_text on top row, sub_text on bottom - // Simplest: just append both inline - return base_block.beside(&RenderedBlock::from_text(&scripts)); - } - - // Fall back to multi-row layout - let base_block = layout(base); - let sup_block = layout(sup); - let sub_block = layout(sub); - - let can_overlap_sup = base_block.height() > 1; - let sup_above = if can_overlap_sup { - sup_block.height().saturating_sub(1) - } else { - sup_block.height() - }; - - let rows = build_sup_sub_grid( - base_block.cells(), - base_block.width(), - base_block.baseline(), - sup_block.cells(), - sup_block.width(), - Some((sub_block.cells(), sub_block.width())), - 0, - ); - - let total_height = rows.len(); - let baseline = sup_above + base_block.baseline(); - - RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1))) -} - -/// Build a grid for base with optional superscript above-right and subscript below-right. -/// -/// Layout: -/// ```text -/// [sup rows] -/// [base] [overlap ] -/// [sub rows] -/// ``` -/// -/// The superscript's last row overlaps with the base's first row (right side). -/// The subscript's first row overlaps with the base's last row (right side). -/// Build a grid for base with optional superscript above-right and subscript below-right. -/// -/// For a single-row base like `x`: -/// - `x^2` renders as: ` 2` / `x ` -/// - `x_i` renders as: `x ` / ` i` -/// - `x_i^2` renders as: ` 2` / `x ` / ` i` -/// -/// For multi-row bases, sup overlaps with the top row and sub with the bottom row. -fn build_sup_sub_grid( - base_cells: &[Vec], - base_width: usize, - _base_baseline: usize, - sup_cells: &[Vec], - sup_width: usize, - sub: Option<(&[Vec], usize)>, - _sub_baseline: usize, -) -> Vec> { - let base_height = base_cells.len(); - let sup_height = sup_cells.len(); - let (sub_cells, sub_width) = sub.unwrap_or((&[], 0)); - let sub_height = sub_cells.len(); - let has_sup = sup_height > 0; - let has_sub = sub_height > 0; - - let script_width = sup_width.max(sub_width); - - // For single-row bases with both sup and sub, don't overlap — stack all three. - // For multi-row bases or single script, allow 1 row of overlap. - let can_overlap_sup = has_sup && base_height > 1; - let can_overlap_sub = has_sub && base_height > 1 && !(has_sup && base_height <= 2); - - let sup_above = if can_overlap_sup { - sup_height.saturating_sub(1) - } else { - sup_height - }; - - let sub_below = if can_overlap_sub { - sub_height.saturating_sub(1) - } else { - sub_height - }; - - let total_height = sup_above + base_height + sub_below; - let mut rows = Vec::with_capacity(total_height); - - let empty_script = || std::iter::repeat_n(" ".to_string(), script_width); - - // Helper to append a script row (or padding) to a row - fn append_script_row( - row: &mut Vec, - cells: &[Vec], - idx: usize, - script_width: usize, - ) { - if idx < cells.len() { - row.extend(cells[idx].iter().cloned()); - let used = cells[idx].len(); - row.extend(std::iter::repeat_n( - " ".to_string(), - script_width.saturating_sub(used), - )); - } else { - row.extend(std::iter::repeat_n(" ".to_string(), script_width)); - } - } - - // Phase 1: sup-only rows above the base - for r in 0..sup_above { - let mut row = vec![" ".to_string(); base_width]; - append_script_row(&mut row, sup_cells, r, script_width); - rows.push(row); - } - - // Phase 2: base rows (with possible script overlap) - for (r, base_row) in base_cells.iter().enumerate().take(base_height) { - let mut row = base_row.clone(); - - // Check if a sup row overlaps here - let sup_idx = if can_overlap_sup { - sup_above + r - } else { - usize::MAX - }; - // Check if a sub row overlaps here - let sub_overlap_start = if can_overlap_sub { - base_height.saturating_sub(sub_height) - } else { - usize::MAX - }; - let sub_idx = if r >= sub_overlap_start && can_overlap_sub { - r - sub_overlap_start - } else { - usize::MAX - }; - - if sup_idx < sup_height { - append_script_row(&mut row, sup_cells, sup_idx, script_width); - } else if sub_idx < sub_height { - append_script_row(&mut row, sub_cells, sub_idx, script_width); - } else { - row.extend(empty_script()); - } - - rows.push(row); - } - - // Phase 3: sub-only rows below the base - let sub_start = if can_overlap_sub { - sub_height.min(base_height) - } else { - 0 - }; - for r in sub_start..sub_height { - let mut row = vec![" ".to_string(); base_width]; - append_script_row(&mut row, sub_cells, r, script_width); - rows.push(row); - } - - rows -} - -fn layout_sqrt(body: &EqNode) -> RenderedBlock { - let body_block = layout(body); - let body_h = body_block.height(); - let body_w = body_block.width(); - - // Single-row body: ___ - // √abc - // - // Multi-row body: ________ - // ╱ num - // ╱ ───── - // √ den - - if body_h == 1 { - // Simple case: √ prefix with overline above - let mut rows = Vec::with_capacity(2); - // Overline row - let mut top = vec![" ".to_string()]; - top.extend(std::iter::repeat_n("─".to_string(), body_w)); - rows.push(top); - // Body row with √ - let mut bot = vec!["√".to_string()]; - bot.extend(body_block.cells()[0].iter().cloned()); - rows.push(bot); - RenderedBlock::new(rows, 1) // baseline at body row - } else { - // Multi-row: radical extends upward - let mut rows = Vec::with_capacity(body_h + 1); - - // Overline row - let mut top = vec![" ".to_string()]; - top.extend(std::iter::repeat_n("─".to_string(), body_w)); - rows.push(top); - - // Body rows with radical on the left - for r in 0..body_h { - let radical_char = if r == body_h - 1 { "√" } else { "│" }; - let mut row = vec![radical_char.to_string()]; - row.extend(body_block.cells()[r].iter().cloned()); - rows.push(row); - } - - let baseline = 1 + body_block.baseline(); - RenderedBlock::new(rows, baseline) - } -} - -/// Build a multi-row operator symbol for integrals (⌠⎮⌡) and large Σ/∏. -fn build_bigop_symbol(symbol: &str) -> RenderedBlock { - match symbol { - "∫" => { - // 3-row integral using bracket pieces - let rows = vec![ - vec!["⌠".to_string()], - vec!["⎮".to_string()], - vec!["⌡".to_string()], - ]; - RenderedBlock::new(rows, 1) // baseline at middle - } - "∬" => { - let rows = vec![ - vec!["⌠".to_string(), "⌠".to_string()], - vec!["⎮".to_string(), "⎮".to_string()], - vec!["⌡".to_string(), "⌡".to_string()], - ]; - RenderedBlock::new(rows, 1) - } - "∮" => { - // Contour integral — use single char since no multi-row form exists - let rows = vec![ - vec!["⌠".to_string()], - vec!["⎮".to_string()], - vec!["⌡".to_string()], - ]; - RenderedBlock::new(rows, 1) - } - _ => { - // Σ, ∏, etc. — single character is fine, they're already wide enough - RenderedBlock::from_text(symbol) - } - } -} - -fn layout_bigop( - symbol: &str, - lower: &Option>, - upper: &Option>, -) -> RenderedBlock { - let op_block = build_bigop_symbol(symbol); - - let upper_block = upper.as_ref().map(|u| layout(u)); - let lower_block = lower.as_ref().map(|l| layout(l)); - - let max_width = [ - op_block.width(), - upper_block.as_ref().map_or(0, |b| b.width()), - lower_block.as_ref().map_or(0, |b| b.width()), - ] - .into_iter() - .max() - .unwrap_or(1); - - let op_centered = op_block.center_in(max_width); - - let mut result = if let Some(ub) = &upper_block { - let ub_centered = ub.center_in(max_width); - let baseline = ub_centered.height(); // op starts after upper limit - RenderedBlock::above(&ub_centered, &op_centered, baseline) - } else { - op_centered.clone() - }; - - // Baseline at the middle of the operator symbol - let op_mid = upper_block.as_ref().map_or(0, |b| b.height()) + op_block.height() / 2; - - if let Some(lb) = &lower_block { - let lb_centered = lb.center_in(max_width); - result = RenderedBlock::above(&result, &lb_centered, op_mid); - } - - RenderedBlock::new(result.cells().to_vec(), op_mid) -} - -fn layout_accent(body: &EqNode, kind: &AccentKind) -> RenderedBlock { - let body_block = layout(body); - let w = body_block.width(); - - let accent_block = match kind { - AccentKind::Bar => { - // Overline: use ‾ repeated across full width - RenderedBlock::hline('‾', w) - } - AccentKind::Hat => { - if w <= 1 { - RenderedBlock::from_char('^') - } else if w <= 3 { - RenderedBlock::from_text("/\\").center_in(w) - } else { - // Wide hat: /‾‾‾\ shape - let inner = w.saturating_sub(2); - let hat_str: String = std::iter::once('/') - .chain(std::iter::repeat_n('‾', inner)) - .chain(std::iter::once('\\')) - .collect(); - RenderedBlock::from_text(&hat_str) - } - } - AccentKind::Tilde => { - if w <= 1 { - RenderedBlock::from_char('~') - } else { - // Wide tilde using ˜ repeated or ~ centered - RenderedBlock::hline('~', w) - } - } - AccentKind::Vec => { - if w <= 1 { - RenderedBlock::from_char('→') - } else { - // Arrow spanning width: ──→ - let shaft = w.saturating_sub(1); - let arrow_str: String = std::iter::repeat_n('─', shaft) - .chain(std::iter::once('→')) - .collect(); - RenderedBlock::from_text(&arrow_str) - } - } - AccentKind::Dot => RenderedBlock::from_char('˙').center_in(w), - AccentKind::DoubleDot => RenderedBlock::from_text("¨").center_in(w), - }; - - let baseline = accent_block.height() + body_block.baseline(); - RenderedBlock::above(&accent_block, &body_block, baseline) -} - -fn layout_limit(name: &str, lower: &Option>) -> RenderedBlock { - let name_block = RenderedBlock::from_text(name); - - if let Some(low) = lower { - let low_block = layout(low); - let max_width = name_block.width().max(low_block.width()); - let name_centered = name_block.center_in(max_width); - let low_centered = low_block.center_in(max_width); - let baseline = name_centered.height() - 1; - RenderedBlock::above(&name_centered, &low_centered, baseline) - } else { - name_block - } -} - -fn layout_mathfont(kind: &MathFontKind, content: &EqNode) -> RenderedBlock { - // Extract text and apply Unicode math font mapping - if let Some(text) = extract_flat_text(content) { - let mapped = mathfont::map_str(kind, &text); - RenderedBlock::from_text(&mapped) - } else { - // Complex content inside font command — render normally - layout(content) - } -} - -fn layout_delimited(left: &str, right: &str, content: &EqNode) -> RenderedBlock { - let content_block = layout(content); - let h = content_block.height(); - - let left_block = build_delimiter(left, h); - let right_block = build_delimiter(right, h); - - left_block.beside(&content_block).beside(&right_block) -} - -/// Build a vertically-scaled delimiter. -fn build_delimiter(delim: &str, height: usize) -> RenderedBlock { - if delim == "." || delim.is_empty() { - // Invisible delimiter - return RenderedBlock::new(vec![vec![" ".to_string()]; height], height / 2); - } - - if height <= 1 { - return RenderedBlock::new(vec![vec![delim.to_string()]], 0); - } - - let (top, mid, bot) = match delim { - "(" => ("⎛", "⎜", "⎝"), - ")" => ("⎞", "⎟", "⎠"), - "[" => ("⎡", "⎢", "⎣"), - "]" => ("⎤", "⎥", "⎦"), - "{" => ("⎧", "⎨", "⎩"), - "}" => ("⎫", "⎬", "⎭"), - "|" => ("│", "│", "│"), - "‖" => ("‖", "‖", "‖"), - _ => (delim, delim, delim), - }; - - let mut rows = Vec::with_capacity(height); - rows.push(vec![top.to_string()]); - for _ in 1..height.saturating_sub(1) { - rows.push(vec![mid.to_string()]); - } - if height > 1 { - rows.push(vec![bot.to_string()]); - } - - RenderedBlock::new(rows, height / 2) -} - -fn layout_matrix(kind: &MatrixKind, matrix_rows: &[Vec]) -> RenderedBlock { - if matrix_rows.is_empty() { - return RenderedBlock::empty(); - } - - // Render all cells, trimming whitespace from cell content - let rendered: Vec> = matrix_rows - .iter() - .map(|row| row.iter().map(|cell| layout(&trim_node(cell))).collect()) - .collect(); - - let num_cols = rendered.iter().map(|r| r.len()).max().unwrap_or(0); - - // Compute column widths - let mut col_widths = vec![0usize; num_cols]; - for row in &rendered { - for (c, cell) in row.iter().enumerate() { - col_widths[c] = col_widths[c].max(cell.width()); - } - } - - // Build each matrix row using beside() for proper baseline alignment. - // Then stack rows vertically with a separator gap. - let col_sep = 2; // spaces between columns - let separator = RenderedBlock::from_text(&" ".repeat(col_sep)); - - let mut row_blocks: Vec = Vec::new(); - - for row in &rendered { - let mut row_block = RenderedBlock::empty(); - for (c, cell) in row.iter().enumerate() { - let padded = cell.center_in(col_widths[c]); - if !row_block.is_empty() { - row_block = row_block.beside(&separator); - } - row_block = row_block.beside(&padded); - } - // Pad to fill missing columns - for w in col_widths.iter().take(num_cols).skip(row.len()) { - row_block = row_block.beside(&separator); - row_block = row_block.beside(&RenderedBlock::from_text(&" ".repeat(*w))); - } - row_blocks.push(row_block); - } - - // Stack rows vertically. Each row_block already has correct baselines from beside(). - let grid_width = row_blocks.iter().map(|r| r.width()).max().unwrap_or(0); - - let mut grid = RenderedBlock::empty(); - for row_block in &row_blocks { - let padded = row_block.center_in(grid_width); - if grid.is_empty() { - grid = padded; - } else { - let baseline = grid.height() / 2; // intermediate baseline - grid = RenderedBlock::above(&grid, &padded, baseline); - } - } - - // Set baseline to middle of entire grid - let total_height = grid.height(); - let grid = RenderedBlock::new(grid.cells().to_vec(), total_height / 2); - - // Wrap with delimiters based on matrix kind - let (left, right) = match kind { - MatrixKind::Paren => ("(", ")"), - MatrixKind::Bracket => ("[", "]"), - MatrixKind::Brace => ("{", "}"), - MatrixKind::VBar => ("|", "|"), - MatrixKind::DoubleVBar => ("‖", "‖"), - MatrixKind::Plain => ("", ""), - }; - - if left.is_empty() { - grid - } else { - let left_d = build_delimiter(left, total_height); - let right_d = build_delimiter(right, total_height); - left_d.beside(&grid).beside(&right_d) - } -} - -fn layout_cases(rows: &[(EqNode, Option)]) -> RenderedBlock { - // Render as a left-brace delimited set of rows - let rendered: Vec = rows - .iter() - .map(|(val, cond)| { - let val_block = layout(val); - if let Some(c) = cond { - let cond_block = layout(c); - val_block - .beside(&RenderedBlock::from_text(" if ")) - .beside(&cond_block) - } else { - val_block - } - }) - .collect(); - - let max_width = rendered.iter().map(|b| b.width()).max().unwrap_or(0); - let mut grid = RenderedBlock::empty(); - for row_block in &rendered { - let padded = RenderedBlock::new(row_block.cells().to_vec(), row_block.baseline()); - // Pad to max width - let full_row = RenderedBlock::new( - padded - .cells() - .iter() - .map(|r| { - let mut r = r.clone(); - r.extend(std::iter::repeat_n( - " ".to_string(), - max_width.saturating_sub(r.len()), - )); - r - }) - .collect(), - padded.baseline(), - ); - if grid.is_empty() { - grid = full_row; - } else { - grid = RenderedBlock::above(&grid, &full_row, grid.height() / 2); - } - } - - let total_height = grid.height(); - let grid = RenderedBlock::new(grid.cells().to_vec(), total_height / 2); - let left_brace = build_delimiter("{", total_height); - left_brace.beside(&grid) -} - -fn layout_binom(top: &EqNode, bottom: &EqNode) -> RenderedBlock { - // Render as a fraction with parentheses instead of a bar - let top_block = layout(top); - let bot_block = layout(bottom); - - let inner_width = top_block.width().max(bot_block.width()); - let top_centered = top_block.center_in(inner_width); - let bot_centered = bot_block.center_in(inner_width); - - let baseline = top_centered.height(); - let stacked = RenderedBlock::above(&top_centered, &bot_centered, baseline - 1); - - let h = stacked.height(); - let left = build_delimiter("(", h); - let right = build_delimiter(")", h); - left.beside(&stacked).beside(&right) -} - -fn layout_brace(content: &EqNode, label: &Option>, over: &bool) -> RenderedBlock { - let content_block = layout(content); - let w = content_block.width(); - - // Build a horizontal brace - let brace_str = if *over { "⏞" } else { "⏟" }; - let brace_block = RenderedBlock::hline(brace_str.chars().next().unwrap(), w); - - if let Some(lbl) = label { - let label_block = layout(lbl).center_in(w); - if *over { - let top = RenderedBlock::above(&label_block, &brace_block, label_block.height()); - let baseline = top.height() + content_block.baseline(); - RenderedBlock::above(&top, &content_block, baseline) - } else { - let bottom = RenderedBlock::above(&brace_block, &label_block, 0); - let baseline = content_block.baseline(); - RenderedBlock::above(&content_block, &bottom, baseline) - } - } else if *over { - let baseline = brace_block.height() + content_block.baseline(); - RenderedBlock::above(&brace_block, &content_block, baseline) - } else { - let baseline = content_block.baseline(); - RenderedBlock::above(&content_block, &brace_block, baseline) - } -} - -fn layout_stackrel(base: &EqNode, annotation: &EqNode, over: &bool) -> RenderedBlock { - let base_block = layout(base); - let ann_block = layout(annotation); - let w = base_block.width().max(ann_block.width()); - let base_centered = base_block.center_in(w); - let ann_centered = ann_block.center_in(w); - - if *over { - let baseline = ann_centered.height() + base_block.baseline(); - RenderedBlock::above(&ann_centered, &base_centered, baseline) - } else { - let baseline = base_block.baseline(); - RenderedBlock::above(&base_centered, &ann_centered, baseline) - } -} diff --git a/vendor/term-maths/src/lib.rs b/vendor/term-maths/src/lib.rs deleted file mode 100644 index ccf5dd2..0000000 --- a/vendor/term-maths/src/lib.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! # term-maths -//! -//! Character-grid mathematical notation renderer for terminals. -//! -//! Accepts LaTeX math input and renders it as 2D Unicode character art suitable -//! for display in a terminal. Targets JuliaMono as the recommended font. -//! -//! ## Quick Start -//! -//! ```rust -//! let block = term_maths::render(r"\frac{a}{b}"); -//! println!("{}", block); -//! // a -//! // ─── -//! // b -//! ``` -//! -//! ## Output Backends -//! -//! - **Plain text** — always available via [`render()`] and [`Display`](std::fmt::Display) -//! - **crossterm** — direct terminal output (feature `crossterm`) -//! - **ratatui** — TUI widget (feature `ratatui`) -//! - **LaTeX round-trip** — serialise back to LaTeX via [`to_latex()`] - -pub mod latex_renderer; -pub mod layout; -pub mod mathfont; -pub mod rendered_block; -pub mod renderer; - -#[cfg(feature = "crossterm")] -pub mod crossterm_renderer; - -#[cfg(feature = "ratatui")] -pub mod ratatui_widget; - -#[cfg(feature = "python")] -pub mod python; - -pub use latex_renderer::LatexRenderer; -pub use rendered_block::RenderedBlock; -pub use renderer::{MathRenderer, TerminalRenderer}; - -#[cfg(feature = "crossterm")] -pub use crossterm_renderer::CrosstermRenderer; - -#[cfg(feature = "ratatui")] -pub use ratatui_widget::MathWidget; - -use rust_latex_parser::parse_equation; - -/// Parse a LaTeX math string and render it as a 2D character grid. -/// -/// This is the primary entry point for the library. -/// -/// ```rust -/// let block = term_maths::render(r"x^2 + y^2 = z^2"); -/// assert_eq!(format!("{}", block), "x² + y² = z²"); -/// ``` -pub fn render(latex: &str) -> RenderedBlock { - let ast = parse_equation(latex); - layout::layout(&ast) -} - -/// Parse a LaTeX math string and serialise it back to LaTeX (round-trip). -/// -/// Useful for normalising LaTeX input or for the LaTeX output backend. -pub fn to_latex(latex: &str) -> String { - let ast = parse_equation(latex); - let renderer = LatexRenderer; - renderer.render(&ast) -} diff --git a/vendor/term-maths/src/mathfont.rs b/vendor/term-maths/src/mathfont.rs deleted file mode 100644 index 61b0548..0000000 --- a/vendor/term-maths/src/mathfont.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Unicode Mathematical Alphanumeric Symbols mapping (U+1D400–U+1D7FF). -//! -//! Maps ASCII Latin letters (and digits) to their styled variants in the -//! Unicode Mathematical Alphanumeric Symbols block, keyed by `MathFontKind`. - -use rust_latex_parser::MathFontKind; - -/// Convert a character to its mathematical font variant. -/// Returns the original character if no mapping exists. -pub fn map_char(kind: &MathFontKind, ch: char) -> char { - match kind { - MathFontKind::Bold => to_bold(ch), - MathFontKind::Blackboard => to_double_struck(ch), - MathFontKind::Calligraphic => to_script(ch), - MathFontKind::Fraktur => to_fraktur(ch), - MathFontKind::Roman => ch, // upright, no transformation - MathFontKind::SansSerif => to_sans_serif(ch), - MathFontKind::Monospace => to_monospace(ch), - } -} - -/// Convert a string by mapping each character through the font transform. -pub fn map_str(kind: &MathFontKind, s: &str) -> String { - s.chars().map(|c| map_char(kind, c)).collect() -} - -// U+1D400 MATHEMATICAL BOLD CAPITAL A .. U+1D419 MATHEMATICAL BOLD CAPITAL Z -// U+1D41A MATHEMATICAL BOLD SMALL A .. U+1D433 MATHEMATICAL BOLD SMALL Z -// U+1D7CE MATHEMATICAL BOLD DIGIT ZERO .. U+1D7D7 MATHEMATICAL BOLD DIGIT NINE -fn to_bold(ch: char) -> char { - match ch { - 'A'..='Z' => char::from_u32(0x1D400 + (ch as u32 - 'A' as u32)).unwrap_or(ch), - 'a'..='z' => char::from_u32(0x1D41A + (ch as u32 - 'a' as u32)).unwrap_or(ch), - '0'..='9' => char::from_u32(0x1D7CE + (ch as u32 - '0' as u32)).unwrap_or(ch), - // Bold Greek uppercase: U+1D6A8–U+1D6C0 - 'Α'..='Ω' => char::from_u32(0x1D6A8 + (ch as u32 - 'Α' as u32)).unwrap_or(ch), - // Bold Greek lowercase: U+1D6C2–U+1D6DA - 'α'..='ω' => char::from_u32(0x1D6C2 + (ch as u32 - 'α' as u32)).unwrap_or(ch), - _ => ch, - } -} - -// U+1D538 MATHEMATICAL DOUBLE-STRUCK CAPITAL A .. U+1D551 -// Exceptions: C=ℂ, H=ℍ, N=ℕ, P=ℙ, Q=ℚ, R=ℝ, Z=ℤ (in Letterlike Symbols block) -// U+1D552 MATHEMATICAL DOUBLE-STRUCK SMALL A .. U+1D56B -// U+1D7D8 MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO .. U+1D7E1 -fn to_double_struck(ch: char) -> char { - match ch { - 'C' => 'ℂ', - 'H' => 'ℍ', - 'N' => 'ℕ', - 'P' => 'ℙ', - 'Q' => 'ℚ', - 'R' => 'ℝ', - 'Z' => 'ℤ', - 'A' | 'B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y' => { - char::from_u32(0x1D538 + (ch as u32 - 'A' as u32)).unwrap_or(ch) - } - 'a'..='z' => char::from_u32(0x1D552 + (ch as u32 - 'a' as u32)).unwrap_or(ch), - '0'..='9' => char::from_u32(0x1D7D8 + (ch as u32 - '0' as u32)).unwrap_or(ch), - _ => ch, - } -} - -// U+1D49C MATHEMATICAL SCRIPT CAPITAL A .. U+1D4B5 -// Exceptions: B=ℬ, E=ℰ, F=ℱ, H=ℋ, I=ℐ, L=ℒ, M=ℳ, R=ℛ (Letterlike Symbols) -// U+1D4B6 MATHEMATICAL SCRIPT SMALL A .. U+1D4CF -// Exceptions: e=ℯ, g=ℊ, o=ℴ -fn to_script(ch: char) -> char { - match ch { - 'B' => 'ℬ', - 'E' => 'ℰ', - 'F' => 'ℱ', - 'H' => 'ℋ', - 'I' => 'ℐ', - 'L' => 'ℒ', - 'M' => 'ℳ', - 'R' => 'ℛ', - 'e' => 'ℯ', - 'g' => 'ℊ', - 'o' => 'ℴ', - 'A' | 'C' | 'D' | 'G' | 'J' | 'K' | 'N'..='Q' | 'S'..='Z' => { - char::from_u32(0x1D49C + (ch as u32 - 'A' as u32)).unwrap_or(ch) - } - 'a'..='d' | 'f' | 'h'..='n' | 'p'..='z' => { - char::from_u32(0x1D4B6 + (ch as u32 - 'a' as u32)).unwrap_or(ch) - } - _ => ch, - } -} - -// U+1D504 MATHEMATICAL FRAKTUR CAPITAL A .. U+1D51C -// Exceptions: C=ℭ, H=ℌ, I=ℑ, R=ℜ, Z=ℨ -// U+1D51E MATHEMATICAL FRAKTUR SMALL A .. U+1D537 -fn to_fraktur(ch: char) -> char { - match ch { - 'C' => 'ℭ', - 'H' => 'ℌ', - 'I' => 'ℑ', - 'R' => 'ℜ', - 'Z' => 'ℨ', - 'A' | 'B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y' => { - char::from_u32(0x1D504 + (ch as u32 - 'A' as u32)).unwrap_or(ch) - } - 'a'..='z' => char::from_u32(0x1D51E + (ch as u32 - 'a' as u32)).unwrap_or(ch), - _ => ch, - } -} - -// U+1D5A0 MATHEMATICAL SANS-SERIF CAPITAL A .. U+1D5B9 -// U+1D5BA MATHEMATICAL SANS-SERIF SMALL A .. U+1D5D3 -// U+1D7E2 MATHEMATICAL SANS-SERIF DIGIT ZERO .. U+1D7EB -fn to_sans_serif(ch: char) -> char { - match ch { - 'A'..='Z' => char::from_u32(0x1D5A0 + (ch as u32 - 'A' as u32)).unwrap_or(ch), - 'a'..='z' => char::from_u32(0x1D5BA + (ch as u32 - 'a' as u32)).unwrap_or(ch), - '0'..='9' => char::from_u32(0x1D7E2 + (ch as u32 - '0' as u32)).unwrap_or(ch), - _ => ch, - } -} - -// U+1D670 MATHEMATICAL MONOSPACE CAPITAL A .. U+1D689 -// U+1D68A MATHEMATICAL MONOSPACE SMALL A .. U+1D6A3 -// U+1D7F6 MATHEMATICAL MONOSPACE DIGIT ZERO .. U+1D7FF -fn to_monospace(ch: char) -> char { - match ch { - 'A'..='Z' => char::from_u32(0x1D670 + (ch as u32 - 'A' as u32)).unwrap_or(ch), - 'a'..='z' => char::from_u32(0x1D68A + (ch as u32 - 'a' as u32)).unwrap_or(ch), - '0'..='9' => char::from_u32(0x1D7F6 + (ch as u32 - '0' as u32)).unwrap_or(ch), - _ => ch, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_bold() { - assert_eq!(to_bold('A'), '𝐀'); - assert_eq!(to_bold('Z'), '𝐙'); - assert_eq!(to_bold('a'), '𝐚'); - assert_eq!(to_bold('z'), '𝐳'); - assert_eq!(to_bold('0'), '𝟎'); - } - - #[test] - fn test_double_struck() { - assert_eq!(to_double_struck('R'), 'ℝ'); - assert_eq!(to_double_struck('Z'), 'ℤ'); - assert_eq!(to_double_struck('N'), 'ℕ'); - assert_eq!(to_double_struck('C'), 'ℂ'); - assert_eq!(to_double_struck('Q'), 'ℚ'); - // Non-exception uppercase - assert_eq!(to_double_struck('A'), '𝔸'); - } - - #[test] - fn test_script() { - assert_eq!(to_script('L'), 'ℒ'); - assert_eq!(to_script('H'), 'ℋ'); - assert_eq!(to_script('B'), 'ℬ'); - // Non-exception - assert_eq!(to_script('A'), '𝒜'); - } - - #[test] - fn test_fraktur() { - assert_eq!(to_fraktur('H'), 'ℌ'); - assert_eq!(to_fraktur('R'), 'ℜ'); - assert_eq!(to_fraktur('a'), '𝔞'); - assert_eq!(to_fraktur('g'), '𝔤'); - } - - #[test] - fn test_sans_serif() { - assert_eq!(to_sans_serif('A'), '𝖠'); - assert_eq!(to_sans_serif('a'), '𝖺'); - } - - #[test] - fn test_monospace() { - assert_eq!(to_monospace('A'), '𝙰'); - assert_eq!(to_monospace('a'), '𝚊'); - assert_eq!(to_monospace('0'), '𝟶'); - } - - #[test] - fn test_non_letter_passthrough() { - // Non-letter characters should pass through unchanged - assert_eq!(map_char(&MathFontKind::Bold, '+'), '+'); - assert_eq!(map_char(&MathFontKind::Blackboard, ' '), ' '); - } -} diff --git a/vendor/term-maths/src/python.rs b/vendor/term-maths/src/python.rs deleted file mode 100644 index 4ff3257..0000000 --- a/vendor/term-maths/src/python.rs +++ /dev/null @@ -1,320 +0,0 @@ -//! Python bindings for term-maths (enabled with the `python` feature). -//! -//! This module exposes the core API as a Python extension module named `_term_maths`. -//! It is intended to be imported through the `term_maths` Python package, which -//! re-exports everything from this compiled extension. -//! -//! ## Python usage -//! -//! ```python -//! import term_maths -//! -//! block = term_maths.render(r"\frac{a}{b}") -//! print(block) # multi-line Unicode art -//! print(block.width) # int -//! print(block.height) # int -//! print(block.baseline) # int -//! print(block.cells()) # list[list[str]] -//! ``` - -use pyo3::exceptions::PyValueError; -use pyo3::prelude::*; -use pyo3_stub_gen::define_stub_info_gatherer; -use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; - -use crate::RenderedBlock; - -// --------------------------------------------------------------------------- -// RenderedBlock Python wrapper -// --------------------------------------------------------------------------- - -/// A rectangular character grid produced by rendering a LaTeX math expression. -/// -/// Each cell contains one terminal column's worth of text. The :attr:`baseline` -/// marks the row used for horizontal alignment when composing blocks side-by-side. -/// -/// Construct via the module-level :func:`render` function or the static -/// constructors (:meth:`from_char`, :meth:`from_text`, :meth:`empty`, -/// :meth:`hline`). -#[gen_stub_pyclass] -#[pyclass(name = "RenderedBlock", module = "term_maths")] -pub struct PyRenderedBlock(pub RenderedBlock); - -#[gen_stub_pymethods] -#[pymethods] -impl PyRenderedBlock { - // ------------------------------------------------------------------ - // Static constructors - // ------------------------------------------------------------------ - - /// Create a block containing a single character. - /// - /// :param ch: A single Unicode character. - /// :type ch: str - /// :raises ValueError: If ``ch`` is not exactly one character. - #[staticmethod] - fn from_char(ch: &str) -> PyResult { - let c = ch.chars().next().ok_or_else(|| { - PyValueError::new_err("from_char expects a single character, got an empty string") - })?; - Ok(PyRenderedBlock(RenderedBlock::from_char(c))) - } - - /// Create a single-row block from a text string. - /// - /// :param text: The text to render. - /// :type text: str - #[staticmethod] - fn from_text(text: &str) -> PyRenderedBlock { - PyRenderedBlock(RenderedBlock::from_text(text)) - } - - /// Create an empty block with zero dimensions. - #[staticmethod] - fn empty() -> PyRenderedBlock { - PyRenderedBlock(RenderedBlock::empty()) - } - - /// Create a horizontal line of a given character repeated *width* times. - /// - /// :param ch: The character to repeat (e.g. ``'─'``). - /// :type ch: str - /// :param width: Number of columns. - /// :type width: int - /// :raises ValueError: If ``ch`` is not exactly one character. - #[staticmethod] - fn hline(ch: &str, width: usize) -> PyResult { - let c = ch.chars().next().ok_or_else(|| { - PyValueError::new_err("hline expects a single character, got an empty string") - })?; - Ok(PyRenderedBlock(RenderedBlock::hline(c, width))) - } - - // ------------------------------------------------------------------ - // Properties - // ------------------------------------------------------------------ - - /// Width of the block in terminal columns. - #[getter] - fn width(&self) -> usize { - self.0.width() - } - - /// Height of the block in rows. - #[getter] - fn height(&self) -> usize { - self.0.height() - } - - /// Row index (0-indexed from top) used as the alignment baseline. - #[getter] - fn baseline(&self) -> usize { - self.0.baseline() - } - - // ------------------------------------------------------------------ - // Methods - // ------------------------------------------------------------------ - - /// Return the cell grid as a list of rows, where each row is a list of - /// single-column strings. - /// - /// :rtype: list[list[str]] - fn cells(&self) -> Vec> { - self.0.cells().to_vec() - } - - /// Return ``True`` if the block has zero width or height. - fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Place *other* immediately to the right of *self*, aligning on baselines. - /// - /// Shorter blocks are padded with empty rows above or below as needed. - /// - /// :param other: The block to append on the right. - /// :type other: RenderedBlock - /// :rtype: RenderedBlock - fn beside(&self, other: &PyRenderedBlock) -> PyRenderedBlock { - PyRenderedBlock(self.0.beside(&other.0)) - } - - /// Stack *top* above *bottom* and set the baseline to *baseline_row*. - /// - /// :param top: Upper block. - /// :type top: RenderedBlock - /// :param bottom: Lower block. - /// :type bottom: RenderedBlock - /// :param baseline_row: Row index (in the combined block) for the baseline. - /// :type baseline_row: int - /// :rtype: RenderedBlock - #[staticmethod] - fn above( - top: &PyRenderedBlock, - bottom: &PyRenderedBlock, - baseline_row: usize, - ) -> PyRenderedBlock { - PyRenderedBlock(RenderedBlock::above(&top.0, &bottom.0, baseline_row)) - } - - /// Add empty space around the block. - /// - /// :param left: Columns to add on the left. - /// :param right: Columns to add on the right. - /// :param top: Rows to add on top. - /// :param bottom: Rows to add on the bottom. - /// :rtype: RenderedBlock - fn pad(&self, left: usize, right: usize, top: usize, bottom: usize) -> PyRenderedBlock { - PyRenderedBlock(self.0.pad(left, right, top, bottom)) - } - - /// Horizontally centre the block within a target width. - /// - /// If *target_width* is not larger than the current width, returns a clone. - /// - /// :param target_width: Desired total width in columns. - /// :type target_width: int - /// :rtype: RenderedBlock - fn center_in(&self, target_width: usize) -> PyRenderedBlock { - PyRenderedBlock(self.0.center_in(target_width)) - } - - // ------------------------------------------------------------------ - // Dunder methods - // ------------------------------------------------------------------ - - fn __str__(&self) -> String { - format!("{}", self.0) - } - - fn __repr__(&self) -> String { - format!( - "RenderedBlock(width={}, height={}, baseline={})", - self.0.width(), - self.0.height(), - self.0.baseline(), - ) - } -} - -// --------------------------------------------------------------------------- -// Helper: parse font kind from string -// --------------------------------------------------------------------------- - -fn parse_font_kind(font: &str) -> PyResult { - use rust_latex_parser::MathFontKind; - match font { - "bold" => Ok(MathFontKind::Bold), - "blackboard" => Ok(MathFontKind::Blackboard), - "calligraphic" => Ok(MathFontKind::Calligraphic), - "fraktur" => Ok(MathFontKind::Fraktur), - "roman" => Ok(MathFontKind::Roman), - "sans_serif" => Ok(MathFontKind::SansSerif), - "monospace" => Ok(MathFontKind::Monospace), - other => Err(PyValueError::new_err(format!( - "Unknown font kind {other:?}. \ - Valid options: bold, blackboard, calligraphic, fraktur, roman, sans_serif, monospace" - ))), - } -} - -// --------------------------------------------------------------------------- -// Python module definition (inline style — required for experimental-inspect) -// --------------------------------------------------------------------------- - -/// Python extension module ``_term_maths``. -/// -/// Import via the ``term_maths`` package rather than directly: -/// -/// .. code-block:: python -/// -/// import term_maths -/// block = term_maths.render(r"\frac{a}{b}") -#[pymodule] -pub mod _term_maths { - use super::*; - - // Re-export the class so pyclass metadata is visible to the module. - #[pymodule_export] - use super::PyRenderedBlock; - - /// Parse a LaTeX math string and render it as a 2D character grid. - /// - /// This is the primary entry point of the library. - /// - /// :param latex: A LaTeX math expression (without surrounding ``$`` delimiters). - /// :type latex: str - /// :returns: The rendered block. - /// :rtype: RenderedBlock - /// - /// Example: - /// - /// ```python - /// >>> import term_maths - /// >>> print(term_maths.render(r"\frac{a}{b}")) - /// a - /// ─── - /// b - /// ``` - #[pyfunction] - pub fn render(latex: &str) -> PyRenderedBlock { - PyRenderedBlock(crate::render(latex)) - } - - /// Parse a LaTeX math string and serialise it back to normalised LaTeX. - /// - /// Useful for round-tripping or canonicalising LaTeX input. - /// - /// :param latex: A LaTeX math expression. - /// :type latex: str - /// :rtype: str - #[pyfunction] - pub fn to_latex(latex: &str) -> String { - crate::to_latex(latex) - } - - /// Map a single character to its Unicode mathematical font variant. - /// - /// Returns the original character unchanged if no mapping exists for - /// the given font kind. - /// - /// :param font: One of ``"bold"``, ``"blackboard"``, ``"calligraphic"``, - /// ``"fraktur"``, ``"roman"``, ``"sans_serif"``, ``"monospace"``. - /// :type font: str - /// :param ch: A single Unicode character. - /// :type ch: str - /// :rtype: str - /// :raises ValueError: If *font* is not a recognised font kind, or *ch* is empty. - #[pyfunction] - pub fn map_char(font: &str, ch: &str) -> PyResult { - let kind = parse_font_kind(font)?; - let c = ch.chars().next().ok_or_else(|| { - PyValueError::new_err("map_char expects a single character, got an empty string") - })?; - Ok(crate::mathfont::map_char(&kind, c).to_string()) - } - - /// Map every character in a string to its Unicode mathematical font variant. - /// - /// Characters without a mapping are passed through unchanged. - /// - /// :param font: One of ``"bold"``, ``"blackboard"``, ``"calligraphic"``, - /// ``"fraktur"``, ``"roman"``, ``"sans_serif"``, ``"monospace"``. - /// :type font: str - /// :param s: The string to transform. - /// :type s: str - /// :rtype: str - /// :raises ValueError: If *font* is not a recognised font kind. - #[pyfunction] - pub fn map_str(font: &str, s: &str) -> PyResult { - let kind = parse_font_kind(font)?; - Ok(crate::mathfont::map_str(&kind, s)) - } -} - -// --------------------------------------------------------------------------- -// Stub generation entry point (used by src/bin/stub_gen.rs) -// --------------------------------------------------------------------------- - -define_stub_info_gatherer!(stub_info_gatherer); diff --git a/vendor/term-maths/src/ratatui_widget.rs b/vendor/term-maths/src/ratatui_widget.rs deleted file mode 100644 index 54c138e..0000000 --- a/vendor/term-maths/src/ratatui_widget.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Ratatui widget backend — renders a `RenderedBlock` into a ratatui `Buffer`. -//! -//! Feature-gated behind `ratatui`. - -use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget}; - -use crate::rendered_block::RenderedBlock; - -/// A ratatui widget that renders a `RenderedBlock` into a terminal buffer. -pub struct MathWidget<'a> { - block: &'a RenderedBlock, - style: Style, -} - -impl<'a> MathWidget<'a> { - pub fn new(block: &'a RenderedBlock) -> Self { - Self { - block, - style: Style::default(), - } - } - - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } -} - -impl Widget for MathWidget<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let max_rows = area.height as usize; - let max_cols = area.width as usize; - - for (r, row) in self.block.cells().iter().enumerate() { - if r >= max_rows { - break; - } - let y = area.y + r as u16; - let mut x_offset = 0usize; - for cell in row { - if x_offset >= max_cols { - break; - } - let x = area.x + x_offset as u16; - buf.set_string(x, y, cell, self.style); - x_offset += unicode_width::UnicodeWidthStr::width(cell.as_str()).max(1); - } - } - } -} diff --git a/vendor/term-maths/src/rendered_block.rs b/vendor/term-maths/src/rendered_block.rs deleted file mode 100644 index 5d2463c..0000000 --- a/vendor/term-maths/src/rendered_block.rs +++ /dev/null @@ -1,352 +0,0 @@ -use std::fmt; -use unicode_width::UnicodeWidthStr; - -/// A rectangular character grid with dimensional metadata. -/// -/// This is the core data structure for 2D math rendering. Each cell contains -/// a string (to handle multi-codepoint grapheme clusters). The baseline marks -/// the row used for horizontal alignment when composing blocks side-by-side. -#[derive(Debug, Clone)] -pub struct RenderedBlock { - /// Rows of character cells. Each cell is a `String` occupying one terminal column. - cells: Vec>, - /// Width in terminal columns (via unicode-width). - width: usize, - /// Height in rows. - height: usize, - /// Row index of the alignment baseline (0-indexed from top). - baseline: usize, -} - -impl RenderedBlock { - /// Create a new block from rows of cell strings. - /// - /// Width is computed from the first row (all rows must have equal width). - /// The baseline defaults to `height / 2` if not specified. - pub fn new(cells: Vec>, baseline: usize) -> Self { - let height = cells.len(); - let width = cells.first().map_or(0, |row| { - row.iter().map(|c| UnicodeWidthStr::width(c.as_str())).sum() - }); - Self { - cells, - width, - height, - baseline, - } - } - - /// Create a block containing a single character. - pub fn from_char(ch: char) -> Self { - let s = ch.to_string(); - let width = UnicodeWidthStr::width(s.as_str()).max(1); - Self { - cells: vec![vec![s]], - width, - height: 1, - baseline: 0, - } - } - - /// Create a block from a string of text (single row). - pub fn from_text(text: &str) -> Self { - if text.is_empty() { - return Self::empty(); - } - let cells: Vec = text.chars().map(|c| c.to_string()).collect(); - let width = UnicodeWidthStr::width(text); - Self { - cells: vec![cells], - width, - height: 1, - baseline: 0, - } - } - - /// Create an empty block with zero dimensions. - pub fn empty() -> Self { - Self { - cells: vec![], - width: 0, - height: 0, - baseline: 0, - } - } - - pub fn width(&self) -> usize { - self.width - } - - pub fn height(&self) -> usize { - self.height - } - - pub fn baseline(&self) -> usize { - self.baseline - } - - pub fn cells(&self) -> &[Vec] { - &self.cells - } - - pub fn is_empty(&self) -> bool { - self.height == 0 || self.width == 0 - } - - /// Place two blocks side-by-side, aligned on baselines. - /// Pads the shorter block with empty rows above/below as needed. - pub fn beside(&self, other: &RenderedBlock) -> RenderedBlock { - if self.is_empty() { - return other.clone(); - } - if other.is_empty() { - return self.clone(); - } - - let baseline = self.baseline.max(other.baseline); - let above_baseline = baseline; - - let self_below = self.height.saturating_sub(self.baseline + 1); - let other_below = other.height.saturating_sub(other.baseline + 1); - let below_baseline = self_below.max(other_below); - - let total_height = above_baseline + 1 + below_baseline; - let total_width = self.width + other.width; - - let self_top_pad = above_baseline - self.baseline; - let other_top_pad = above_baseline - other.baseline; - - let mut rows = Vec::with_capacity(total_height); - for row_idx in 0..total_height { - let mut row = Vec::new(); - - // Left block cells - let self_row = row_idx.checked_sub(self_top_pad); - if let Some(sr) = self_row { - if sr < self.height { - row.extend(self.cells[sr].iter().cloned()); - } else { - row.extend(std::iter::repeat_n(" ".to_string(), self.width)); - } - } else { - row.extend(std::iter::repeat_n(" ".to_string(), self.width)); - } - - // Right block cells - let other_row = row_idx.checked_sub(other_top_pad); - if let Some(or_idx) = other_row { - if or_idx < other.height { - row.extend(other.cells[or_idx].iter().cloned()); - } else { - row.extend(std::iter::repeat_n(" ".to_string(), other.width)); - } - } else { - row.extend(std::iter::repeat_n(" ".to_string(), other.width)); - } - - rows.push(row); - } - - RenderedBlock { - cells: rows, - width: total_width, - height: total_height, - baseline, - } - } - - /// Stack two blocks vertically. The baseline is set to `baseline_row` - /// (typically the dividing row between them, or top/bottom block's baseline). - pub fn above( - top: &RenderedBlock, - bottom: &RenderedBlock, - baseline_row: usize, - ) -> RenderedBlock { - let width = top.width.max(bottom.width); - let mut rows = Vec::with_capacity(top.height + bottom.height); - - for r in 0..top.height { - rows.push(Self::pad_row_to_width(&top.cells[r], top.width, width)); - } - for r in 0..bottom.height { - rows.push(Self::pad_row_to_width( - &bottom.cells[r], - bottom.width, - width, - )); - } - - RenderedBlock { - cells: rows, - width, - height: top.height + bottom.height, - baseline: baseline_row, - } - } - - /// Add empty space around a block. - pub fn pad(&self, left: usize, right: usize, top: usize, bottom: usize) -> RenderedBlock { - let new_width = left + self.width + right; - let new_height = top + self.height + bottom; - - let mut rows = Vec::with_capacity(new_height); - - // Top padding - for _ in 0..top { - rows.push(vec![" ".to_string(); new_width]); - } - - // Content rows with left/right padding - for r in 0..self.height { - let mut row = Vec::with_capacity(new_width); - row.extend(std::iter::repeat_n(" ".to_string(), left)); - row.extend(self.cells[r].iter().cloned()); - row.extend(std::iter::repeat_n(" ".to_string(), right)); - rows.push(row); - } - - // Bottom padding - for _ in 0..bottom { - rows.push(vec![" ".to_string(); new_width]); - } - - RenderedBlock { - cells: rows, - width: new_width, - height: new_height, - baseline: self.baseline + top, - } - } - - /// Horizontally centre a block within a given width. - pub fn center_in(&self, target_width: usize) -> RenderedBlock { - if target_width <= self.width { - return self.clone(); - } - let total_pad = target_width - self.width; - let left_pad = total_pad / 2; - let right_pad = total_pad - left_pad; - self.pad(left_pad, right_pad, 0, 0) - } - - /// Helper: pad a row of cells to a target width by appending spaces. - fn pad_row_to_width(row: &[String], current_width: usize, target_width: usize) -> Vec { - let mut result = row.to_vec(); - let pad = target_width.saturating_sub(current_width); - result.extend(std::iter::repeat_n(" ".to_string(), pad)); - result - } - - /// Create a horizontal line of a given character and width. - pub fn hline(ch: char, width: usize) -> RenderedBlock { - let cells = vec![vec![ch.to_string(); width]]; - RenderedBlock { - cells, - width, - height: 1, - baseline: 0, - } - } -} - -impl fmt::Display for RenderedBlock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (i, row) in self.cells.iter().enumerate() { - if i > 0 { - writeln!(f)?; - } - for cell in row { - write!(f, "{}", cell)?; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_from_char() { - let block = RenderedBlock::from_char('x'); - assert_eq!(block.width(), 1); - assert_eq!(block.height(), 1); - assert_eq!(block.baseline(), 0); - assert_eq!(format!("{}", block), "x"); - } - - #[test] - fn test_from_text() { - let block = RenderedBlock::from_text("hello"); - assert_eq!(block.width(), 5); - assert_eq!(block.height(), 1); - assert_eq!(format!("{}", block), "hello"); - } - - #[test] - fn test_beside_baseline_aligned() { - // Two single-row blocks - let a = RenderedBlock::from_text("ab"); - let b = RenderedBlock::from_text("cd"); - let result = a.beside(&b); - assert_eq!(result.width(), 4); - assert_eq!(result.height(), 1); - assert_eq!(format!("{}", result), "abcd"); - } - - #[test] - fn test_beside_different_heights() { - // a is 3 rows tall with baseline at row 1 - let a = RenderedBlock::new( - vec![vec!["a".into()], vec!["b".into()], vec!["c".into()]], - 1, - ); - // d is 1 row tall with baseline at row 0 - let d = RenderedBlock::from_char('d'); - let result = a.beside(&d); - assert_eq!(result.height(), 3); - assert_eq!(result.baseline(), 1); - // d should be on the baseline row (row 1) - let output = format!("{}", result); - let lines: Vec<&str> = output.lines().collect(); - assert_eq!(lines[0], "a "); - assert_eq!(lines[1], "bd"); - assert_eq!(lines[2], "c "); - } - - #[test] - fn test_center_in() { - let block = RenderedBlock::from_text("ab"); - let centered = block.center_in(6); - assert_eq!(centered.width(), 6); - assert_eq!(format!("{}", centered), " ab "); - } - - #[test] - fn test_above() { - let top = RenderedBlock::from_text("abc"); - let bottom = RenderedBlock::from_text("de"); - let result = RenderedBlock::above(&top, &bottom, 0); - assert_eq!(result.height(), 2); - assert_eq!(result.width(), 3); - let output = format!("{}", result); - let lines: Vec<&str> = output.lines().collect(); - assert_eq!(lines[0], "abc"); - assert_eq!(lines[1], "de "); - } - - #[test] - fn test_pad() { - let block = RenderedBlock::from_char('x'); - let padded = block.pad(1, 1, 1, 1); - assert_eq!(padded.width(), 3); - assert_eq!(padded.height(), 3); - assert_eq!(padded.baseline(), 1); - let output = format!("{}", padded); - let lines: Vec<&str> = output.lines().collect(); - assert_eq!(lines[0], " "); - assert_eq!(lines[1], " x "); - assert_eq!(lines[2], " "); - } -} diff --git a/vendor/term-maths/src/renderer.rs b/vendor/term-maths/src/renderer.rs deleted file mode 100644 index 940ac36..0000000 --- a/vendor/term-maths/src/renderer.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Output backend trait and implementations. - -use rust_latex_parser::EqNode; - -use crate::rendered_block::RenderedBlock; - -/// Trait for rendering an `EqNode` AST into a target output format. -pub trait MathRenderer { - type Output; - - /// Render an equation AST node into the target output. - fn render(&self, node: &EqNode) -> Self::Output; -} - -/// Default renderer that produces a `RenderedBlock` (2D character grid). -/// Always available — no feature gates required. -pub struct TerminalRenderer; - -impl MathRenderer for TerminalRenderer { - type Output = RenderedBlock; - - fn render(&self, node: &EqNode) -> RenderedBlock { - crate::layout::layout(node) - } -} diff --git a/vendor/term-maths/tests/layout_tests.rs b/vendor/term-maths/tests/layout_tests.rs deleted file mode 100644 index 6fedc32..0000000 --- a/vendor/term-maths/tests/layout_tests.rs +++ /dev/null @@ -1,421 +0,0 @@ -use term_maths::render; - -/// Helper: render and collect output lines, trimming trailing whitespace per line. -fn render_lines(latex: &str) -> Vec { - let block = render(latex); - let output = format!("{}", block); - output.lines().map(|l| l.trim_end().to_string()).collect() -} - -#[test] -fn test_simple_fraction() { - let lines = render_lines(r"\frac{a}{b}"); - assert_eq!(lines, vec![" a", "───", " b"]); -} - -#[test] -fn test_nested_fraction() { - let lines = render_lines(r"\frac{1}{1+\frac{1}{x}}"); - // Numerator "1" centered over denominator "1 + 1/x" - assert_eq!(lines.len(), 5); - // Top line: centered "1" - assert!(lines[0].contains('1')); - // Bar line - assert!(lines[1].chars().all(|c| c == '─')); - // Denominator contains nested fraction - assert!(lines[3].contains('+')); -} - -#[test] -fn test_superscript() { - // Simple digits use inline Unicode superscript - let lines = render_lines(r"x^2"); - assert_eq!(lines.len(), 1); - assert_eq!(lines[0], "x²"); -} - -#[test] -fn test_subscript() { - // Simple chars use inline Unicode subscript - let lines = render_lines(r"a_n"); - assert_eq!(lines.len(), 1); - assert_eq!(lines[0], "aₙ"); -} - -#[test] -fn test_supsub() { - // Both scripts inline when possible - let lines = render_lines(r"x_i^2"); - assert_eq!(lines.len(), 1); - assert_eq!(lines[0], "x²ᵢ"); -} - -#[test] -fn test_superscript_fallback() { - // Complex superscripts fall back to multi-row - let lines = render_lines(r"e^{i\pi}"); - assert!(lines.len() >= 2); - let joined = lines.join("\n"); - assert!(joined.contains('π')); - assert!(joined.contains('e')); -} - -#[test] -fn test_horizontal_sequence() { - let block = render(r"a + b"); - // Should be a single row with spaces around operators - assert_eq!(block.height(), 1); - let output = format!("{}", block); - assert!(output.contains("a")); - assert!(output.contains("+")); - assert!(output.contains("b")); -} - -#[test] -fn test_sqrt() { - let lines = render_lines(r"\sqrt{x}"); - // Should have overline and radical - assert!(lines[0].contains('─')); - assert!(lines.iter().any(|l| l.contains('√'))); - assert!(lines.iter().any(|l| l.contains('x'))); -} - -#[test] -fn test_euler_identity() { - let lines = render_lines(r"e^{i\pi} + 1 = 0"); - // Should render as 2 rows (e with superscript iπ, then + 1 = 0) - assert!(lines.len() >= 2); - // Top row should have iπ - let joined = lines.join("\n"); - assert!(joined.contains('π')); - assert!(joined.contains('0')); -} - -#[test] -fn test_quadratic_formula() { - let lines = render_lines(r"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"); - // Should be a multi-line fraction with sqrt in numerator - assert!(lines.len() >= 3); // at least num + bar + den - // Contains the fraction bar - assert!(lines.iter().any(|l| l.contains('─') && !l.contains('√'))); - // Contains sqrt - let joined = lines.join("\n"); - assert!(joined.contains('√')); - assert!(joined.contains("2a")); -} - -#[test] -fn test_empty_input() { - let block = render(""); - assert!(block.is_empty() || block.height() <= 1); -} - -#[test] -fn test_single_symbol() { - let block = render(r"\alpha"); - assert_eq!(block.height(), 1); - let output = format!("{}", block); - assert!(output.contains('α')); -} - -#[test] -fn test_fraction_baseline_alignment() { - // When a fraction appears beside other content, baselines should align - let lines = render_lines(r"x + \frac{a}{b}"); - // x and + should be on the fraction bar row - let bar_row = lines.iter().position(|l| l.contains('─')).unwrap(); - assert!(lines[bar_row].contains('x') || lines[bar_row].contains('+')); -} - -// ── Sprint 2: DSP Reference Equations ────────────────────────────────── - -#[test] -fn test_dft_summation() { - // X[k] = Σ_{n=0}^{N-1} x[n] · e^{-j 2π/N kn} - let lines = render_lines(r"X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j \frac{2\pi}{N} kn}"); - let joined = lines.join("\n"); - - // Must contain the summation symbol - assert!(joined.contains('∑'), "missing Σ"); - // Must contain upper limit N-1 and lower limit n=0 - assert!( - joined.contains("N - 1") || joined.contains("N-1"), - "missing upper limit" - ); - assert!( - joined.contains("n = 0") || joined.contains("n=0"), - "missing lower limit" - ); - // Must contain the exponent's fraction 2π/N - assert!(joined.contains('π'), "missing π in exponent"); - // Multi-line output - assert!(lines.len() >= 3, "DFT should be at least 3 lines tall"); -} - -#[test] -fn test_convolution_integral() { - // (f * g)(t) = ∫_{-∞}^{∞} f(τ) g(t - τ) dτ - let lines = render_lines(r"(f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) \, d\tau"); - let joined = lines.join("\n"); - - // Must contain integral pieces - assert!( - joined.contains('⌠') || joined.contains('∫'), - "missing integral symbol" - ); - // Must contain limits - assert!(joined.contains('∞'), "missing infinity"); - assert!(joined.contains("-∞"), "missing negative infinity"); - // Must contain tau - assert!(joined.contains('τ'), "missing tau"); - // Multi-line output (integral is 3+ rows) - assert!( - lines.len() >= 3, - "convolution integral should be at least 3 lines" - ); -} - -#[test] -fn test_transfer_function() { - // H(z) = (b₀ + b₁z⁻¹ + b₂z⁻²) / (1 + a₁z⁻¹ + a₂z⁻²) - let lines = - render_lines(r"H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}}"); - let joined = lines.join("\n"); - - // Must contain fraction bar - assert!( - lines.iter().any(|l| l.contains('─')), - "missing fraction bar" - ); - // Must contain subscripted coefficients - assert!(joined.contains('₀') || joined.contains("b_0"), "missing b₀"); - assert!(joined.contains('₁') || joined.contains("b_1"), "missing b₁"); - // Must contain z⁻¹ (inline superscript) - assert!(joined.contains("z⁻¹"), "missing z⁻¹"); - // Three lines minimum (num + bar + den) - assert!( - lines.len() >= 3, - "transfer function should be at least 3 lines" - ); -} - -#[test] -fn test_hann_window() { - // w(n) = 0.5(1 - cos(2πn / (N-1))) - let lines = render_lines(r"w(n) = 0.5 \left(1 - \cos\left(\frac{2\pi n}{N - 1}\right)\right)"); - let joined = lines.join("\n"); - - // Must contain cos - assert!(joined.contains("cos"), "missing cos"); - // Must contain π - assert!(joined.contains('π'), "missing π"); - // Must contain scaled delimiters - assert!( - joined.contains('⎛') || joined.contains('('), - "missing delimiter" - ); - // Must contain N - 1 in denominator - assert!(joined.contains("N - 1"), "missing N - 1 denominator"); - // Multi-line (fraction inside delimiters) - assert!(lines.len() >= 2, "Hann window should be at least 2 lines"); -} - -// ── Sprint 2: Component Tests ────────────────────────────────────────── - -#[test] -fn test_integral_multirow() { - let lines = render_lines(r"\int_{0}^{1}"); - let joined = lines.join("\n"); - // Should use multi-row integral characters - assert!(joined.contains('⌠'), "missing ⌠ top piece"); - assert!(joined.contains('⌡'), "missing ⌡ bottom piece"); -} - -#[test] -fn test_sum_with_limits() { - let lines = render_lines(r"\sum_{i=1}^{n}"); - let joined = lines.join("\n"); - assert!(joined.contains('∑'), "missing Σ"); - // Upper limit above, lower limit below - assert!( - lines.len() >= 3, - "sum with limits should be at least 3 lines" - ); -} - -#[test] -fn test_scaled_delimiters() { - let lines = render_lines(r"\left(\frac{a}{b}\right)"); - let joined = lines.join("\n"); - // Should use bracket piece characters for 3-row fraction - assert!( - joined.contains('⎛') && joined.contains('⎝'), - "missing scaled parentheses" - ); - assert!(joined.contains('─'), "missing fraction bar"); -} - -#[test] -fn test_overline() { - let lines = render_lines(r"\overline{abc}"); - assert_eq!(lines.len(), 2); - assert!(lines[0].contains('‾'), "missing overline character"); - assert!(lines[1].contains("abc"), "missing body"); -} - -#[test] -fn test_accent_hat() { - let lines = render_lines(r"\hat{x}"); - assert_eq!(lines.len(), 2); - assert!(lines[0].contains('^'), "missing hat"); - assert!(lines[1].contains('x'), "missing body"); -} - -#[test] -fn test_accent_vec() { - let lines = render_lines(r"\vec{v}"); - assert_eq!(lines.len(), 2); - assert!(lines[0].contains('→'), "missing arrow"); - assert!(lines[1].contains('v'), "missing body"); -} - -#[test] -fn test_sqrt_of_fraction() { - let lines = render_lines(r"\sqrt{\frac{a}{b}}"); - let joined = lines.join("\n"); - assert!(joined.contains('√'), "missing radical"); - assert!(joined.contains('─'), "missing overline or fraction bar"); - assert!(lines.len() >= 3, "sqrt of fraction should be multi-line"); -} - -// ── Sprint 3: Matrix and Symbol Coverage ─────────────────────────────── - -#[test] -fn test_pmatrix_2x2() { - let lines = render_lines(r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}"); - let joined = lines.join("\n"); - // Parenthesis delimiters - assert!( - joined.contains('⎛') || joined.contains('('), - "missing left paren" - ); - assert!( - joined.contains('⎞') || joined.contains(')'), - "missing right paren" - ); - // All entries present - for ch in ['a', 'b', 'c', 'd'] { - assert!(joined.contains(ch), "missing entry {}", ch); - } -} - -#[test] -fn test_bmatrix_identity() { - let lines = render_lines(r"\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}"); - let joined = lines.join("\n"); - // Bracket delimiters - assert!( - joined.contains('⎡') || joined.contains('['), - "missing left bracket" - ); - assert!( - joined.contains('⎤') || joined.contains(']'), - "missing right bracket" - ); -} - -#[test] -fn test_vmatrix_determinant() { - let lines = render_lines(r"\begin{vmatrix} a & b \\ c & d \end{vmatrix}"); - let joined = lines.join("\n"); - assert!(joined.contains('│'), "missing vertical bar delimiter"); -} - -#[test] -fn test_matrix_with_fractions() { - let lines = render_lines(r"\begin{pmatrix} \frac{1}{2} & 0 \\ 0 & \frac{3}{4} \end{pmatrix}"); - let joined = lines.join("\n"); - // Fraction bars inside cells - assert!(joined.contains('─'), "missing fraction bar"); - // Both fractions present - assert!(joined.contains('1') && joined.contains('2'), "missing 1/2"); - assert!(joined.contains('3') && joined.contains('4'), "missing 3/4"); - // Multi-line (fractions make rows taller) - assert!( - lines.len() >= 4, - "matrix with fractions should be at least 4 lines" - ); -} - -#[test] -fn test_3x3_matrix() { - let lines = render_lines(r"\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}"); - let joined = lines.join("\n"); - // All digits present - for d in '1'..='9' { - assert!(joined.contains(d), "missing digit {}", d); - } - assert!(lines.len() >= 3, "3x3 matrix should be at least 3 lines"); -} - -// ── Math Font Tests ──────────────────────────────────────────────────── - -#[test] -fn test_mathbb_double_struck() { - let block = render(r"\mathbb{R}"); - let output = format!("{}", block); - assert_eq!(output.trim(), "ℝ"); -} - -#[test] -fn test_mathbb_integers() { - let block = render(r"\mathbb{Z}"); - let output = format!("{}", block); - assert_eq!(output.trim(), "ℤ"); -} - -#[test] -fn test_mathcal_script() { - let block = render(r"\mathcal{L}"); - let output = format!("{}", block); - assert_eq!(output.trim(), "ℒ"); -} - -#[test] -fn test_mathbf_bold() { - let block = render(r"\mathbf{x}"); - let output = format!("{}", block); - assert_eq!(output.trim(), "𝐱"); -} - -#[test] -fn test_mathfrak_fraktur() { - let block = render(r"\mathfrak{g}"); - let output = format!("{}", block); - assert_eq!(output.trim(), "𝔤"); -} - -#[test] -fn test_mathbb_with_superscript() { - let lines = render_lines(r"\mathbb{R}^n"); - assert_eq!(lines.len(), 1); - let output = &lines[0]; - assert!(output.contains('ℝ'), "missing double-struck R"); - assert!(output.contains('ⁿ'), "missing superscript n"); -} - -#[test] -fn test_mathsf_sans_serif() { - let block = render(r"\mathsf{ABC}"); - let output = format!("{}", block); - assert!(output.contains('𝖠'), "missing sans-serif A"); - assert!(output.contains('𝖡'), "missing sans-serif B"); - assert!(output.contains('𝖢'), "missing sans-serif C"); -} - -#[test] -fn test_mathtt_monospace() { - let block = render(r"\mathtt{code}"); - let output = format!("{}", block); - assert!(output.contains('𝚌'), "missing monospace c"); -} diff --git a/vendor/txm-wasm/Cargo.lock b/vendor/txm-wasm/Cargo.lock new file mode 100644 index 0000000..923afe8 --- /dev/null +++ b/vendor/txm-wasm/Cargo.lock @@ -0,0 +1,231 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.119", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "txm" +version = "0.1.5" +dependencies = [ + "logos", + "thiserror", + "unicode-width", +] + +[[package]] +name = "txm-wasm" +version = "0.1.0" +dependencies = [ + "txm", + "wasm-bindgen", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] diff --git a/vendor/txm-wasm/Cargo.toml b/vendor/txm-wasm/Cargo.toml new file mode 100644 index 0000000..d1208ef --- /dev/null +++ b/vendor/txm-wasm/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "txm-wasm" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +txm = { path = "../txm" } +wasm-bindgen = "=0.2.117" diff --git a/vendor/txm-wasm/src/lib.rs b/vendor/txm-wasm/src/lib.rs new file mode 100644 index 0000000..cc3fa23 --- /dev/null +++ b/vendor/txm-wasm/src/lib.rs @@ -0,0 +1,39 @@ +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +pub fn render_latex(latex: &str) -> Result { + txm::render(latex) + .map(|rendered| strip_sgr(&rendered)) + .map_err(|error| error.to_string()) +} + +fn strip_sgr(text: &str) -> String { + let mut result = String::with_capacity(text.len()); + let mut chars = text.chars(); + + while let Some(ch) = chars.next() { + if ch == '\u{1b}' && chars.next() == Some('[') { + for parameter in chars.by_ref() { + if parameter == 'm' { + break; + } + } + } else { + result.push(ch); + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::render_latex; + + #[test] + fn renders_plain_unicode_grid() { + let rendered = render_latex(r"\color{red}{\frac{a}{b}}").unwrap(); + assert_eq!(rendered, " a \n───\n b \n"); + assert!(!rendered.contains('\u{1b}')); + } +} diff --git a/vendor/txm/Cargo.lock b/vendor/txm/Cargo.lock new file mode 100644 index 0000000..fee40f2 --- /dev/null +++ b/vendor/txm/Cargo.lock @@ -0,0 +1,340 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "lru" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags", + "compact_str", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru", + "strum", + "thiserror", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "txm" +version = "0.1.5" +dependencies = [ + "logos", + "ratatui-core", + "thiserror", + "unicode-width", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" diff --git a/vendor/txm/Cargo.toml b/vendor/txm/Cargo.toml new file mode 100644 index 0000000..d69d90a --- /dev/null +++ b/vendor/txm/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "txm" +version = "0.1.5" +edition = "2024" +license = "MIT OR Apache-2.0" +description = "Terminal math rendering engine" +authors = ["thatmagicalcat "] +repository = "https://github.com/thatmagicalcat/txm" +readme = "README.md" + +[dependencies] +logos = "0.16" +thiserror = "2" +unicode-width = "0.2" + +ratatui-core = { version = "0.1", optional = true } + +[features] +default = [] +ratatui = ["dep:ratatui-core"] diff --git a/vendor/txm/LICENSE-APACHE b/vendor/txm/LICENSE-APACHE new file mode 100644 index 0000000..79b85c0 --- /dev/null +++ b/vendor/txm/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 thatmagicalcat + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/txm/LICENSE-MIT b/vendor/txm/LICENSE-MIT new file mode 100644 index 0000000..6d20923 --- /dev/null +++ b/vendor/txm/LICENSE-MIT @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 thatmagicalcat + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/txm/README.md b/vendor/txm/README.md new file mode 100644 index 0000000..42c742f --- /dev/null +++ b/vendor/txm/README.md @@ -0,0 +1,57 @@ +
+

TXM

+

TXM (Terminal TeX Math) is a math rendering engine with LaTeX support.

+
+ +# Screenshots: +![s0](./screenshots/0.png) +![s1](./screenshots/1.png) +![s2](./screenshots/2.png) +![s3](./screenshots/3.png) +![s4](./screenshots/4.png) +![s5](./screenshots/5.png) + +### Quick run using nix +``` +nix run github:thatmagicalcat/txm -- "E = mc^2" +``` +Requires [Nix](https://nix.dev/install-nix) with flakes enabled. + +# Installation +### Arch Linux (AUR) +Install `txm-git` using an AUR helper like `yay` or `paru`: +```bash +yay -S txm-git +``` + +Or install it manually: +```bash +git clone https://aur.archlinux.org/txm-git.git +cd txm-git +makepkg -si +``` + +### Gentoo Linux (GURU) +```bash +emerge -a app-text/txm +``` + +### Cargo (Rust) +``` +$ cargo install txm +``` +Or +``` +$ cargo install --git https://github.com/thatmagicalcat/txm +``` + +### Bindings +- C/C++ bindings live in [`bindings/c/`](./bindings/c/). +- Python bindings live in [`bindings/py/`](./bindings/py/). + +# Projects using TXM: +- [**txm.nvim**](https://github.com/rv178/txm.nvim/): LaTeX preview inside NeoVim using + +## License +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE)) +- MIT license ([LICENSE-MIT](LICENSE-MIT)) diff --git a/vendor/txm/UPSTREAM.md b/vendor/txm/UPSTREAM.md new file mode 100644 index 0000000..edc39fb --- /dev/null +++ b/vendor/txm/UPSTREAM.md @@ -0,0 +1,2 @@ +Upstream: https://github.com/thatmagicalcat/txm +Revision: 26e6d3cfef4d5d040711f4c49d9147aabd350d4d diff --git a/vendor/txm/src/ast.rs b/vendor/txm/src/ast.rs new file mode 100644 index 0000000..159f694 --- /dev/null +++ b/vendor/txm/src/ast.rs @@ -0,0 +1,36 @@ +#[derive(Debug, Clone, PartialEq)] +pub enum Expr { + Ident(String), + Number(String), + Delimiter { + left: char, + right: char, + inner: Box, + }, + Neg(Box), + Command { + name: String, + opts: Vec, + args: Vec, + }, + Superscript(Box, Box), + Subscript(Box, Box), + BothScripts(Box, Box, Box), + Prime(Box, usize), + BinOp(Box, BinOp, Box), + Juxtapose(Vec), + Escape(String), + Empty, + Matrix { + name: String, + rows: Vec>, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinOp { + Add, + Sub, + Eq, + Mul, +} diff --git a/vendor/txm/src/backend.rs b/vendor/txm/src/backend.rs new file mode 100644 index 0000000..bb732fd --- /dev/null +++ b/vendor/txm/src/backend.rs @@ -0,0 +1,14 @@ +use crate::layout_tree::LayoutNode; +use crate::style::Style; + +pub trait Backend { + type Output; + type Error: std::error::Error; + + fn render(&self, tree: &LayoutNode) -> Result; +} + +pub trait RenderTarget { + fn set(&mut self, x: usize, y: usize, ch: char, style: Style); + fn fill_row(&mut self, y: usize, x_start: usize, x_end: usize, ch: char, style: Style); +} diff --git a/vendor/txm/src/backends/generic_backend.rs b/vendor/txm/src/backends/generic_backend.rs new file mode 100644 index 0000000..32f41d5 --- /dev/null +++ b/vendor/txm/src/backends/generic_backend.rs @@ -0,0 +1,495 @@ +use crate::ast::BinOp; +use crate::backend::RenderTarget; +use crate::layout_tree::{LayoutNode, LineStyle, NodeKind}; +use crate::style::Style; + +pub(crate) fn render_node(node: &LayoutNode, buf: &mut impl RenderTarget, x: usize, y: usize) { + render_inner(node, buf, x, y, Style::new()) +} + +fn render_inner( + node: &LayoutNode, + buf: &mut impl RenderTarget, + x: usize, + y: usize, + inherited: Style, +) { + let style = inherited.merge(node.style); + + match &node.kind { + NodeKind::Text { content } => { + for (i, &c) in content.iter().enumerate() { + buf.set(x + i, y, c, style); + } + } + + NodeKind::HStack { children, spacing } => { + let mut cx = x; + for child in children { + let cy = y + (node.baseline - child.baseline); + render_inner(child, buf, cx, cy, style); + cx += child.width + spacing; + } + } + + NodeKind::VStack { top, bottom, line } => { + let max_h = top.height.max(bottom.height); + let inner_w = top.width.max(bottom.width); + let pad = 1; + let top_x = x + pad + (inner_w.saturating_sub(top.width)) / 2; + let bot_x = x + pad + (inner_w.saturating_sub(bottom.width)) / 2; + + render_inner(top, buf, top_x, max_h - top.height + y, style); + render_inner(bottom, buf, bot_x, y + max_h + 1, style); + + if *line == LineStyle::Solid { + let w = node.width; + buf.fill_row(y + max_h, x, x + w, '─', style); + } + } + + NodeKind::Infix { lhs, op, rhs } => { + let baseline = lhs.baseline.max(rhs.baseline); + let lhs_y = y + (baseline - lhs.baseline); + let rhs_y = y + (baseline - rhs.baseline); + + render_inner(lhs, buf, x, lhs_y, style); + + let op_char = match op { + BinOp::Add => '+', + BinOp::Sub => '-', + BinOp::Eq => '=', + BinOp::Mul => '·', + }; + buf.set(x + lhs.width + 1, y + baseline, op_char, style); + + render_inner(rhs, buf, x + lhs.width + 3, rhs_y, style); + } + + NodeKind::Superscript { inline, base, exp } => { + let inline_shift = if !inline { exp.height } else { 0 }; + + render_inner(exp, buf, x + base.width, y, style); + render_inner(base, buf, x, y + inline_shift, style); + } + + NodeKind::Subscript { inline, base, sub } => { + let inline_shift = if !inline { base.height } else { 0 }; + + render_inner(base, buf, x, y, style); + render_inner(sub, buf, x + base.width, y + inline_shift, style); + } + + NodeKind::BothScripts { base, sub, sup } => { + render_inner(sup, buf, x + base.width, y, style); + render_inner(base, buf, x, y + sup.height, style); + let sub_y = y + sup.height + base.baseline + 1; + render_inner(sub, buf, x + base.width, sub_y, style); + } + + NodeKind::StretchyDelim { + inner, + left, + right, + fill, + } => { + if inner.height <= 1 { + let resolved_left = resolve_delimiter_single(*left); + let resolved_right = resolve_delimiter_single(*right); + buf.set(x, y, resolved_left, style); + render_inner(inner, buf, x + 1, y, style); + buf.set(x + node.width - 1, y, resolved_right, style); + } else { + let resolved = + resolve_delimiters(*left, *right, inner.height, *fill, inner.baseline); + for (row, (l, r)) in resolved.iter().enumerate().take(inner.height) { + buf.set(x, y + row, *l, style); + buf.set(x + node.width - 1, y + row, *r, style); + } + render_inner(inner, buf, x + 2, y, style); + } + } + + NodeKind::Accent { + inner, + mark, + stretch, + } => { + if *stretch { + for i in 0..node.width { + buf.set(x + i, y, *mark, style); + } + } else { + buf.set(x + node.width / 2, y, *mark, style); + } + render_inner(inner, buf, x, y + 1, style); + } + + NodeKind::Limits { base, lower, upper } => { + let max_h = upper.height.max(lower.height); + render_inner(upper, buf, x, y + (max_h - upper.height), style); + render_inner(base, buf, x, y + max_h, style); + render_inner(lower, buf, x, y + max_h + base.height, style); + } + + NodeKind::Sqrt { inner, index } => { + buf.set(x + 1, y, '┌', style); + for i in 2..node.width { + buf.set(x + i, y, '─', style); + } + for row in 1..node.height { + buf.set(x + 1, y + row, '│', style); + } + buf.set(x, y + node.height - 1, '╲', style); + render_inner(inner, buf, x + 3, y + 1, style); + + if let Some(idx) = index { + render_inner(idx, buf, x, y, style); + } + } + + NodeKind::Summation { inner } => { + render_summation(inner.as_deref(), buf, x, y, node.height, style); + } + + NodeKind::Product { inner } => { + render_product(inner.as_deref(), buf, x, y, node.height, style); + } + + NodeKind::Integral { inner } => { + render_integral(inner.as_deref(), buf, x, y, node.height, style); + } + + NodeKind::Matrix { .. } => { + render_matrix(node, buf, x, y, style); + } + + NodeKind::Neg { inner } => { + buf.set(x, y + inner.baseline, '-', style); + render_inner(inner, buf, x + 1, y, style); + } + + NodeKind::Prime { base, count } => { + render_inner(base, buf, x, y, style); + for i in 0..*count { + buf.set(x + base.width + i, y + base.baseline, '\'', style); + } + } + + NodeKind::Empty => {} + } +} + +fn resolve_delimiter_single(c: char) -> char { + match c { + '|' => '│', + _ => c, + } +} + +fn resolve_delimiters( + left: char, + right: char, + height: usize, + fill: bool, + baseline: usize, +) -> Vec<(char, char)> { + let (tl, tr, bl, br, ml, mr) = match (left, right) { + ('(', ')') => ('⎛', '⎞', '⎝', '⎠', '⎜', '⎟'), + ('[', ']') => ('⎡', '⎤', '⎣', '⎦', '⎢', '⎥'), + ('{', '}') => ('⎧', '⎫', '⎩', '⎭', '⎪', '⎪'), + ('|', '|') => ('⎪', '⎪', '⎪', '⎪', '⎪', '⎪'), + _ if fill => (left, right, left, right, left, right), + _ => (left, right, left, right, '│', '│'), + }; + + let mut result = Vec::with_capacity(height); + for row in 0..height { + let (l, r) = if row == 0 { + (tl, tr) + } else if row == height - 1 { + (bl, br) + } else if left == '{' && row == baseline { + ('⎨', '⎬') + } else { + (ml, mr) + }; + result.push((l, r)); + } + result +} + +fn render_summation( + inner: Option<&LayoutNode>, + buf: &mut impl RenderTarget, + x: usize, + y: usize, + h: usize, + style: Style, +) { + let inner = match inner { + Some(i) => i, + None => { + buf.set(x, y, '━', style); + buf.set(x + 1, y, '━', style); + buf.set(x + 2, y, '━', style); + buf.set(x + 3, y, '┓', style); + buf.set(x, y + 1, '⟩', style); + buf.set(x, y + 2, '━', style); + buf.set(x + 1, y + 2, '━', style); + buf.set(x + 2, y + 2, '━', style); + buf.set(x + 3, y + 2, '┛', style); + + return; + } + }; + + if inner.height <= 2 { + let w_sigma = 4; + buf.set(x, y, '━', style); + buf.set(x + 1, y, '━', style); + buf.set(x + 2, y, '━', style); + buf.set(x + 3, y, '┓', style); + buf.set(x, y + 1, '⟩', style); + buf.set(x, y + 2, '━', style); + buf.set(x + 1, y + 2, '━', style); + buf.set(x + 2, y + 2, '━', style); + buf.set(x + 3, y + 2, '┛', style); + let inner_y = y + (h.saturating_sub(inner.height)) / 2; + render_inner(inner, buf, x + w_sigma + 1, inner_y, style); + return; + } + + let w_sigma = ((1.5 * h as f32) as usize).max(h / 2 + 2); + + buf.fill_row(y, x, x + w_sigma - 1, '━', style); + buf.set(x + w_sigma - 1, y, '┓', style); + + buf.fill_row(y + h - 1, x, x + w_sigma - 1, '━', style); + buf.set(x + w_sigma - 1, y + h - 1, '┛', style); + + for r in 1..h - 1 { + let d = r.min(h - 1 - r); + let col = d.saturating_sub(1); + + let ch = if !h.is_power_of_two() && r == h / 2 { + '⟩' + } else if r < h / 2 { + '╲' + } else { + '╱' + }; + + buf.set(x + col, y + r, ch, style); + } + + render_inner(inner, buf, x + w_sigma + 1, y, style); +} + +fn render_product( + inner: Option<&LayoutNode>, + buf: &mut impl RenderTarget, + x: usize, + y: usize, + h: usize, + style: Style, +) { + let inner = match inner { + Some(i) => i, + None => { + buf.set(x, y, '┳', style); + buf.set(x + 1, y, '━', style); + buf.set(x + 2, y, '┳', style); + buf.set(x, y + 1, '┃', style); + buf.set(x + 2, y + 1, '┃', style); + return; + } + }; + + let w_pi = if h <= 2 { 3 } else { (h / 2 + 2).max(3) }; + + buf.set(x, y, '┳', style); + if w_pi > 2 { + buf.fill_row(y, x + 1, x + w_pi - 1, '━', style); + } + buf.set(x + w_pi - 1, y, '┳', style); + + for row in 1..h { + buf.set(x, y + row, '┃', style); + buf.set(x + w_pi - 1, y + row, '┃', style); + } + + let inner_y = y + (h.saturating_sub(inner.height)) / 2; + render_inner(inner, buf, x + w_pi + 1, inner_y, style); +} + +fn render_integral( + inner: Option<&LayoutNode>, + buf: &mut impl RenderTarget, + x: usize, + y: usize, + _h: usize, + style: Style, +) { + let inner = match inner { + Some(i) => i, + None => { + buf.set(x, y, '⎛', style); + buf.set(x, y + 1, '⎜', style); + buf.set(x, y + 2, '⎠', style); + return; + } + }; + + if inner.height <= 3 { + buf.set(x, y, '⎛', style); + buf.set(x, y + 1, '⎜', style); + buf.set(x, y + 2, '⎠', style); + let inner_y = if inner.height == 1 { y + 1 } else { y }; + render_inner(inner, buf, x + 2, inner_y, style); + } else { + buf.set(x, y, '⎛', style); + for row in 1..inner.height - 1 { + buf.set(x, y + row, '⎜', style); + } + buf.set(x, y + inner.height - 1, '⎠', style); + render_inner(inner, buf, x + 2, y, style); + } +} + +fn render_matrix( + node: &LayoutNode, + buf: &mut impl RenderTarget, + x: usize, + y: usize, + inherited: Style, +) { + let NodeKind::Matrix { name: _, rows } = &node.kind else { + return; + }; + + if rows.is_empty() || rows[0].is_empty() { + return; + } + + let num_rows = rows.len(); + + let mut row_max_depths = vec![0; num_rows]; + let mut row_max_baselines = vec![0; num_rows]; + let mut max_item_width = 0; + + for (i, row) in rows.iter().enumerate() { + let mut max_b = 0; + let mut max_d = 0; + for item in row { + max_item_width = max_item_width.max(item.width); + max_b = max_b.max(item.baseline); + max_d = max_d.max(item.height.saturating_sub(item.baseline)); + } + row_max_baselines[i] = max_b; + row_max_depths[i] = max_d; + } + + let cell_width = max_item_width; + let mut cell_height = 0; + for i in 0..num_rows { + let row_content_height = row_max_baselines[i] + row_max_depths[i]; + cell_height = cell_height.max(row_content_height); + } + + let row_padding = 1; + cell_height = cell_height.max(1); + + let active_cell_height = if num_rows > 1 { + cell_height + row_padding + } else { + cell_height + }; + + let hspacing = 4; + + let inner_x = x; + let inner_y = y; + + for (i, row) in rows.iter().enumerate() { + let row_content_height = row_max_baselines[i] + row_max_depths[i]; + let row_padding_top = (active_cell_height - row_content_height) / 2; + let row_cell_baseline = row_padding_top + row_max_baselines[i]; + + for (j, item) in row.iter().enumerate() { + let cell_x = inner_x + j * (cell_width + hspacing); + let cell_y = inner_y + i * active_cell_height; + + let item_x_in_cell = (cell_width - item.width) / 2; + let item_y_in_cell = row_cell_baseline - item.baseline; + + render_inner( + item, + buf, + cell_x + item_x_in_cell, + cell_y + item_y_in_cell, + inherited, + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::render_node; + use crate::backend::RenderTarget; + use crate::style::{Color, Style}; + + struct StyleBuf { + cells: Vec<(char, Style)>, + width: usize, + } + + impl StyleBuf { + fn new(width: usize, height: usize) -> Self { + Self { + cells: vec![(' ', Style::new()); width * height], + width, + } + } + } + + impl RenderTarget for StyleBuf { + fn set(&mut self, x: usize, y: usize, ch: char, style: Style) { + let index = y * self.width + x; + self.cells[index] = (ch, style); + } + + fn fill_row(&mut self, y: usize, x_start: usize, x_end: usize, ch: char, style: Style) { + for x in x_start..x_end { + self.set(x, y, ch, style); + } + } + } + + #[test] + fn current_color_reaches_generated_renderer_cells() { + for input in [ + r"\color{red}{x+y}", + r"\color{red}{-x}", + r"\color{red}{x'}", + r"\color{red}{\int_a^b{f'\left(x\right) dx}}", + ] { + let tree = crate::layout(input).unwrap(); + let mut buf = StyleBuf::new(tree.width, tree.height); + render_node(&tree, &mut buf, 0, 0); + + let uncolored: String = buf + .cells + .iter() + .filter(|(ch, style)| *ch != ' ' && Color::new(style.fg_color()) != Color::RED) + .map(|(ch, _)| *ch) + .collect(); + + assert!( + uncolored.is_empty(), + "expected all cells in {input:?} to be red, uncolored: {uncolored:?}" + ); + } + } +} diff --git a/vendor/txm/src/backends/mod.rs b/vendor/txm/src/backends/mod.rs new file mode 100644 index 0000000..83d5c73 --- /dev/null +++ b/vendor/txm/src/backends/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod generic_backend; +pub mod terminal; diff --git a/vendor/txm/src/backends/terminal.rs b/vendor/txm/src/backends/terminal.rs new file mode 100644 index 0000000..c1ea5f3 --- /dev/null +++ b/vendor/txm/src/backends/terminal.rs @@ -0,0 +1,104 @@ +use std::fmt; +use std::fmt::Write; + +use crate::backend::Backend; +use crate::backend::RenderTarget; +use crate::layout_tree::LayoutNode; +use crate::style::Style; + +use super::generic_backend; + +pub struct TerminalBackend; + +impl TerminalBackend { + pub fn new() -> Self { + Self + } +} + +impl Default for TerminalBackend { + fn default() -> Self { + Self::new() + } +} + +impl Backend for TerminalBackend { + type Output = String; + type Error = fmt::Error; + + fn render(&self, tree: &LayoutNode) -> Result { + let mut buf = CharBuf::new(tree.width, tree.height); + generic_backend::render_node(tree, &mut buf, 0, 0); + Ok(buf.to_string()) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct CharBuf { + pub data: Vec, + pub styles: Vec