├── .github └── workflows │ ├── ci.yml │ ├── github_pages.yml │ └── lint_pr_title.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── mitex-cli │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── utils.rs │ │ └── version.rs ├── mitex-glob │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── origin ├── mitex-lexer │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ ├── macro_engine.rs │ │ ├── snapshot_map.rs │ │ ├── stream.rs │ │ └── token.rs │ └── tests │ │ ├── common │ │ └── mod.rs │ │ └── expand_macro.rs ├── mitex-parser │ ├── Cargo.toml │ ├── benches │ │ └── simple.rs │ ├── src │ │ ├── arg_match.rs │ │ ├── lib.rs │ │ ├── parser.rs │ │ └── syntax.rs │ └── tests │ │ ├── ast.rs │ │ ├── ast │ │ ├── arg_match.rs │ │ ├── arg_parse.rs │ │ ├── attachment.rs │ │ ├── block_comment.rs │ │ ├── command.rs │ │ ├── environment.rs │ │ ├── figure.rs │ │ ├── formula.rs │ │ ├── fuzzing.rs │ │ ├── left_right.rs │ │ ├── tabular.rs │ │ └── trivia.rs │ │ ├── common │ │ └── mod.rs │ │ └── properties.rs ├── mitex-spec-gen │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── lib.rs ├── mitex-spec │ ├── Cargo.toml │ ├── benches │ │ └── spec_constructions.rs │ └── src │ │ ├── lib.rs │ │ ├── preludes.rs │ │ ├── query.rs │ │ └── stream.rs ├── mitex-wasm │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── mitex │ ├── Cargo.toml │ ├── benches │ ├── bench.ps1 │ ├── bencher.typ │ ├── convert_large_projects.rs │ ├── empty.typ │ ├── oiwiki-with-render.typ │ └── oiwiki.typ │ ├── src │ ├── converter.rs │ └── lib.rs │ └── tests │ ├── cvt.rs │ └── cvt │ ├── arg_match.rs │ ├── arg_parse.rs │ ├── attachment.rs │ ├── basic_text_mode.rs │ ├── block_comment.rs │ ├── command.rs │ ├── environment.rs │ ├── figure.rs │ ├── formula.rs │ ├── fuzzing.rs │ ├── left_right.rs │ ├── misc.rs │ ├── simple_env.rs │ ├── tabular.rs │ └── trivia.rs ├── docs ├── .gitignore ├── logo.typ ├── pageless.typ └── spec.typ ├── fixtures └── underleaf │ └── ieee │ ├── ieee.typ │ ├── main.tex │ ├── main.typ │ ├── refs.bib │ └── styling.typ ├── fuzz └── mitex │ ├── Cargo.toml │ └── src │ └── main.rs ├── packages ├── mitex-web │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── global.d.ts │ │ ├── index.html │ │ ├── loader.mjs │ │ ├── main.ts │ │ ├── style.css │ │ ├── tools │ │ │ ├── typst.css │ │ │ ├── typst.ts │ │ │ ├── underleaf-editor.ts │ │ │ ├── underleaf-fs.ts │ │ │ ├── underleaf-preview.ts │ │ │ ├── underleaf.css │ │ │ ├── underleaf.html │ │ │ └── underleaf.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── vite.config.mjs │ └── yarn.lock ├── mitex │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── examples │ │ ├── bench.typ │ │ ├── example.png │ │ └── example.typ │ ├── lib.typ │ ├── mitex.typ │ ├── specs │ │ ├── README.md │ │ ├── latex │ │ │ └── standard.typ │ │ ├── mod.typ │ │ └── prelude.typ │ └── typst.toml └── typst-dom │ ├── .gitignore │ ├── package.json │ ├── src │ ├── global.d.ts │ ├── index.mts │ ├── traits │ │ ├── base.mts │ │ └── base.test.mts │ ├── typst-animation.mts │ ├── typst-debug-info.mts │ ├── typst-doc.mts │ ├── typst-outline.mts │ ├── typst-patch.mts │ └── typst-patch.test.mts │ ├── tsconfig.json │ └── vite.config.js └── scripts ├── bench.ps1 ├── build.ps1 ├── build.sh └── publish.ps1 /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: mitex::ci 2 | on: [push, pull_request] 3 | 4 | env: 5 | RUSTFLAGS: "-Dwarnings" 6 | RUSTDOCFLAGS: "-Dwarnings" 7 | SCCACHE_GHA_ENABLED: "true" 8 | RUSTC_WRAPPER: "sccache" 9 | CARGO_INCREMENTAL: "0" 10 | 11 | jobs: 12 | tests: 13 | name: Tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: { submodules: recursive } 18 | - uses: typst-community/setup-typst@v3 19 | with: { typst-version: "0.10.0" } 20 | - uses: rui314/setup-mold@v1 21 | - uses: dtolnay/rust-toolchain@stable 22 | with: { targets: wasm32-unknown-unknown } 23 | - uses: mozilla-actions/sccache-action@v0.0.3 24 | - run: cargo test --workspace --no-fail-fast 25 | checks: 26 | name: Check clippy, formatting, and documentation 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: { submodules: recursive } 31 | - uses: typst-community/setup-typst@v3 32 | with: { typst-version: "0.10.0" } 33 | - uses: rui314/setup-mold@v1 34 | - uses: dtolnay/rust-toolchain@stable 35 | with: { targets: wasm32-unknown-unknown } 36 | - uses: mozilla-actions/sccache-action@v0.0.3 37 | - run: cargo clippy --workspace --all-targets --all-features 38 | - run: cargo fmt --check --all 39 | - run: cargo doc --workspace --no-deps 40 | build-wasm-plugin: 41 | name: Build Wasm modules 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: { submodules: recursive } 46 | - uses: typst-community/setup-typst@v3 47 | with: { typst-version: "0.10.0" } 48 | - uses: rui314/setup-mold@v1 49 | - uses: dtolnay/rust-toolchain@stable 50 | with: { targets: wasm32-unknown-unknown } 51 | - uses: jetli/wasm-pack-action@v0.4.0 52 | with: 53 | version: "v0.12.1" 54 | - uses: mozilla-actions/sccache-action@v0.0.3 55 | - run: scripts/build.sh 56 | -------------------------------------------------------------------------------- /.github/workflows/github_pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | permissions: 9 | pages: write 10 | id-token: write 11 | contents: read 12 | 13 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 14 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | build-gh-pages: 21 | runs-on: ubuntu-latest 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: { submodules: recursive } 28 | - uses: typst-community/setup-typst@v3 29 | with: { typst-version: "0.10.0" } 30 | - uses: rui314/setup-mold@v1 31 | - uses: dtolnay/rust-toolchain@stable 32 | with: { targets: wasm32-unknown-unknown } 33 | - uses: jetli/wasm-pack-action@v0.4.0 34 | with: 35 | version: "v0.12.1" 36 | - uses: mozilla-actions/sccache-action@v0.0.3 37 | - uses: actions/setup-node@v4 38 | with: 39 | node-version: 18 40 | - run: scripts/build.sh 41 | - run: | # Run tests 42 | cd packages/typst-dom 43 | yarn 44 | yarn build 45 | - run: | # Run tests 46 | cd packages/mitex-web 47 | yarn 48 | yarn build --base=/mitex 49 | - name: Setup Pages 50 | uses: actions/configure-pages@v3 51 | - name: Upload artifact 52 | uses: actions/upload-pages-artifact@v1 53 | with: 54 | # Upload `/github-pages` sub directory 55 | path: "./packages/mitex-web/dist" 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v2 59 | -------------------------------------------------------------------------------- /.github/workflows/lint_pr_title.yml: -------------------------------------------------------------------------------- 1 | name: mitex::lint_pr_title 2 | on: 3 | pull_request: 4 | types: [opened, edited, synchronize] 5 | 6 | permissions: 7 | pull-requests: write 8 | 9 | jobs: 10 | main: 11 | name: Validate PR title 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: amannn/action-semantic-pull-request@v5 15 | id: lint_pr_title 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | # Configure which types are allowed (newline-delimited). 20 | # Default: https://github.com/commitizen/conventional-commit-types 21 | # extraType: dev: internal development 22 | types: | 23 | dev 24 | feat 25 | fix 26 | docs 27 | style 28 | refactor 29 | perf 30 | test 31 | build 32 | ci 33 | chore 34 | revert 35 | ignoreLabels: | 36 | bot 37 | ignore-semantic-pull-request 38 | - uses: marocchino/sticky-pull-request-comment@v2 39 | # When the previous steps fails, the workflow would stop. By adding this 40 | # condition you can continue the execution with the populated error message. 41 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 42 | with: 43 | header: pr-title-lint-error 44 | message: | 45 | Hey there and thank you for opening this pull request! 👋🏼 46 | 47 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 48 | 49 | Details: 50 | 51 | ``` 52 | ${{ steps.lint_pr_title.outputs.error_message }} 53 | ``` 54 | # Delete a previous comment when the issue has been resolved 55 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 56 | uses: marocchino/sticky-pull-request-comment@v2 57 | with: 58 | header: pr-title-lint-error 59 | delete: true 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /local 3 | 4 | *.pdf 5 | *.wasm -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "crates/mitex-spec-gen/assets/artifacts"] 2 | path = crates/mitex-spec-gen/assets/artifacts 3 | url = https://github.com/mitex-rs/artifacts 4 | branch = v0.2.4 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.2.5 4 | 5 | - Fix: assign correct class for lvert and rvert (https://github.com/mitex-rs/mitex/pull/172) 6 | - Fix: convert `bf` to `bold(upright(..))` (https://github.com/mitex-rs/mitex/pull/183) 7 | - Fix: remove xarrow and use stretch in typst v0.12.0 (https://github.com/mitex-rs/mitex/pull/184) 8 | - Fix: fix some symbols `\not`, `\gtreqqless` and `\gtrapprox` (https://github.com/mitex-rs/mitex/pull/185) 9 | 10 | 11 | ## 0.2.4 12 | 13 | - Fix `\boxed` command. 14 | - Support basic figure and tabular environments. 15 | - Add escape symbols for plus, minus and percent. 16 | - Fix `\left` and `\right`: size of lr use em. 17 | 18 | 19 | ## 0.2.3 20 | 21 | - Support nesting math equation like `\text{$x$}`. 22 | - Fix broken `\left` and `\right` commands. #143 23 | - Correct the symbol mapping for `\varphi`, `phi`, `\epsilon` and `\varepsilon`. 24 | 25 | 26 | ## 0.2.2 27 | 28 | ### Text Mode 29 | 30 | - Add `\(\)` and `\[\]` support. 31 | - Add `\footnote` and `\cite`. 32 | 33 | ### Fixes 34 | 35 | - Remove duplicate keys for error when a dict has duplicate keys in Typst 0.11. 36 | - Fix bracket/paren group parsing. 37 | - Remove extra spacing for ceiling symbols. 38 | 39 | 40 | ## 0.2.1 41 | 42 | - Remove unnecessary zws. 43 | 44 | 45 | ## 0.2.0 46 | 47 | - Add more symbols for equations. 48 | - Basic macro support. 49 | - Basic text mode support. 50 | 51 | 52 | ## 0.1.0 53 | 54 | - LaTeX support for Typst, powered by Rust and WASM. We can now render LaTeX equations in real-time in Typst. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MiTeX 2 | 3 | ## Installing Dependencies 4 | 5 | You should install [Typst](https://github.com/typst/typst?tab=readme-ov-file#installation) and [Rust](https://www.rust-lang.org/tools/install) for running the build script. 6 | 7 | If you want to build the WASM plugin, you should also setup the wasm target by rustup: 8 | 9 | ```sh 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | ## Build 14 | 15 | For Linux: 16 | 17 | ```sh 18 | git clone https://github.com/mitex-rs/mitex.git 19 | scripts/build.sh 20 | ``` 21 | 22 | For Windows: 23 | 24 | ```sh 25 | git clone https://github.com/mitex-rs/mitex.git 26 | .\scripts\build.ps1 27 | ``` 28 | 29 | ## Fuzzing (Testing) 30 | 31 | The [afl.rs] only supports Linux. 32 | 33 | Installing [afl.rs] on Linux: 34 | 35 | ```bash 36 | cargo install cargo-afl 37 | ``` 38 | 39 | Building and fuzzing: 40 | 41 | ```bash 42 | cargo afl build --bin fuzz-target-mitex 43 | cargo afl fuzz -i local/seed -o local/fuzz-res ./target/debug/fuzz-target-mitex 44 | ``` 45 | 46 | To minimize test cases, using `afl-tmin` 47 | 48 | ```bash 49 | cargo afl tmin -i crash.tex -o minimized.tex ./target/debug/fuzz-target-mitex 50 | ``` 51 | 52 | ## Documents 53 | 54 | TODO. 55 | 56 | [afl.rs]: https://github.com/rust-fuzz/afl.rs 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | description = "Minimal TeX Equations Support." 3 | authors = [ 4 | "Myriad-Dreamin ", 5 | "OrangeX4 ", 6 | "mgt ", 7 | ] 8 | version = "0.2.5" 9 | edition = "2021" 10 | readme = "README.md" 11 | license = "Apache-2.0" 12 | homepage = "https://github.com/mitex-rs/mitex" 13 | repository = "https://github.com/mitex-rs/mitex" 14 | rust-version = "1.74" 15 | 16 | [workspace] 17 | resolver = "2" 18 | members = ["crates/*", "fuzz/*"] 19 | 20 | [workspace.dependencies] 21 | 22 | once_cell = "1" 23 | anyhow = "1" 24 | 25 | rustc-hash = "2" 26 | ecow = "0.2.2" 27 | ena = "0.14.3" 28 | 29 | logos = "0.14.0" 30 | rowan = "0.15.15" 31 | 32 | which = "6" 33 | 34 | mitex-spec = { version = "0.2.5", path = "crates/mitex-spec" } 35 | mitex-glob = { version = "0.2.5", path = "crates/mitex-glob" } 36 | mitex-lexer = { version = "0.2.5", path = "crates/mitex-lexer" } 37 | mitex-parser = { version = "0.2.5", path = "crates/mitex-parser" } 38 | mitex = { version = "0.2.5", path = "crates/mitex" } 39 | mitex-spec-gen = { version = "0.2.5", path = "crates/mitex-spec-gen" } 40 | 41 | clap = { version = "4.4", features = ["derive", "env", "unicode", "wrap_help"] } 42 | clap_builder = { version = "4", features = ["string"] } 43 | clap_complete = "4.4" 44 | clap_complete_fig = "4.4" 45 | clap_mangen = { version = "0.2.15" } 46 | vergen = { version = "8.2.5", features = [ 47 | "build", 48 | "cargo", 49 | "git", 50 | "gitcl", 51 | "rustc", 52 | ] } 53 | 54 | divan = "0.1.14" 55 | insta = "1.39" 56 | 57 | rkyv = "0.7.42" 58 | serde = "1.0.188" 59 | serde_json = "1.0.106" 60 | 61 | [profile.release] 62 | lto = true # Enable link-time optimization 63 | strip = true # Strip symbols from binary* 64 | opt-level = 3 # Optimize for speed 65 | codegen-units = 1 # Reduce number of codegen units to increase optimizations 66 | panic = 'abort' # Abort on panic 67 | 68 | [workspace.lints.rust] 69 | missing_docs = "warn" 70 | 71 | [workspace.lints.clippy] 72 | uninlined_format_args = "warn" 73 | missing_panics_doc = "warn" 74 | missing_safety_doc = "warn" 75 | undocumented_unsafe_blocks = "warn" 76 | -------------------------------------------------------------------------------- /crates/mitex-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex-cli" 3 | description = "CLI for MiTeX." 4 | categories = ["compilers", "command-line-utilities"] 5 | keywords = ["cli", "language", "compiler", "latex", "typst"] 6 | authors.workspace = true 7 | version.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | homepage.workspace = true 11 | repository.workspace = true 12 | 13 | [[bin]] 14 | name = "mitex" 15 | path = "src/main.rs" 16 | test = false 17 | doctest = false 18 | bench = false 19 | doc = false 20 | 21 | [dependencies] 22 | 23 | mitex-spec-gen.workspace = true 24 | mitex-spec.workspace = true 25 | mitex-parser.workspace = true 26 | mitex.workspace = true 27 | 28 | clap.workspace = true 29 | clap_builder.workspace = true 30 | clap_complete.workspace = true 31 | clap_complete_fig.workspace = true 32 | clap_mangen.workspace = true 33 | 34 | serde.workspace = true 35 | serde_json.workspace = true 36 | anyhow.workspace = true 37 | 38 | [build-dependencies] 39 | anyhow.workspace = true 40 | vergen.workspace = true 41 | 42 | [lints] 43 | workspace = true 44 | 45 | [features] 46 | prebuilt-spec = ["mitex-spec-gen/prebuilt"] 47 | generate-spec = ["mitex-spec-gen/generate"] 48 | -------------------------------------------------------------------------------- /crates/mitex-cli/build.rs: -------------------------------------------------------------------------------- 1 | //! Build script 2 | 3 | use anyhow::Result; 4 | use vergen::EmitBuilder; 5 | 6 | fn main() -> Result<()> { 7 | // Emit the instructions 8 | EmitBuilder::builder() 9 | .all_cargo() 10 | .build_timestamp() 11 | .git_sha(false) 12 | .git_describe(true, true, None) 13 | .all_rustc() 14 | .emit()?; 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /crates/mitex-cli/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions and types. 2 | 3 | /// A wrapper around `Box` that implements `std::error::Error`. 4 | pub struct Error(Box); 5 | 6 | impl std::fmt::Display for Error { 7 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 8 | f.write_str(&self.0) 9 | } 10 | } 11 | 12 | impl std::fmt::Debug for Error { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | f.write_str(&self.0) 15 | } 16 | } 17 | 18 | impl std::error::Error for Error {} 19 | 20 | impl From for Error { 21 | fn from(s: String) -> Self { 22 | Self(s.into_boxed_str()) 23 | } 24 | } 25 | 26 | impl From<&str> for Error { 27 | fn from(s: &str) -> Self { 28 | Self(s.to_owned().into_boxed_str()) 29 | } 30 | } 31 | 32 | impl From for Error { 33 | fn from(err: anyhow::Error) -> Self { 34 | Self(err.to_string().into_boxed_str()) 35 | } 36 | } 37 | 38 | impl From for Error { 39 | fn from(err: std::io::Error) -> Self { 40 | Self(err.to_string().into_boxed_str()) 41 | } 42 | } 43 | 44 | /// Exit with an error message. 45 | pub fn exit_with_error(err: E) -> ! { 46 | clap::Error::raw( 47 | clap::error::ErrorKind::ValueValidation, 48 | format!("mitex error: {err}"), 49 | ) 50 | .exit() 51 | } 52 | 53 | /// Exit with an error message. 54 | pub trait UnwrapOrExit { 55 | /// Unwrap the result or exit with an error message. 56 | fn unwrap_or_exit(self) -> T; 57 | } 58 | 59 | impl UnwrapOrExit for Result { 60 | fn unwrap_or_exit(self) -> T { 61 | self.map_err(exit_with_error).unwrap() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/mitex-cli/src/version.rs: -------------------------------------------------------------------------------- 1 | //! Version information 2 | 3 | use std::{fmt::Display, process::exit}; 4 | 5 | use crate::build_info::VERSION; 6 | use clap::ValueEnum; 7 | 8 | /// Available version formats for `$program -VV` 9 | #[derive(ValueEnum, Debug, Clone)] 10 | #[value(rename_all = "kebab-case")] 11 | pub enum VersionFormat { 12 | /// Don't show version information 13 | None, 14 | /// Shows short version information 15 | Short, 16 | /// Shows only features information 17 | Features, 18 | /// Shows full version information 19 | Full, 20 | /// Shows version information in JSON format 21 | Json, 22 | /// Shows version information in plain JSON format 23 | JsonPlain, 24 | } 25 | 26 | impl Display for VersionFormat { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | f.write_str(self.to_possible_value().unwrap().get_name()) 29 | } 30 | } 31 | 32 | /// Version information 33 | #[derive(serde::Serialize, serde::Deserialize)] 34 | struct VersionInfo { 35 | name: &'static str, 36 | version: &'static str, 37 | features: Vec<&'static str>, 38 | 39 | program_semver: &'static str, 40 | program_commit_hash: &'static str, 41 | program_target_triple: &'static str, 42 | program_opt_level: &'static str, 43 | program_build_timestamp: &'static str, 44 | 45 | rustc_semver: &'static str, 46 | rustc_commit_hash: &'static str, 47 | rustc_host_triple: &'static str, 48 | rustc_channel: &'static str, 49 | rustc_llvm_version: &'static str, 50 | } 51 | 52 | impl VersionInfo { 53 | fn new() -> Self { 54 | Self { 55 | name: "mitex-cli", 56 | version: VERSION, 57 | features: env!("VERGEN_CARGO_FEATURES").split(',').collect::>(), 58 | program_semver: match env!("VERGEN_GIT_DESCRIBE") { 59 | "VERGEN_IDEMPOTENT_OUTPUT" => "000000000000", 60 | s => s, 61 | }, 62 | program_commit_hash: match env!("VERGEN_GIT_SHA") { 63 | "VERGEN_IDEMPOTENT_OUTPUT" => "0000000000000000000000000000000000000000", 64 | s => s, 65 | }, 66 | program_target_triple: env!("VERGEN_CARGO_TARGET_TRIPLE"), 67 | program_opt_level: env!("VERGEN_CARGO_OPT_LEVEL"), 68 | program_build_timestamp: env!("VERGEN_BUILD_TIMESTAMP"), 69 | 70 | rustc_semver: env!("VERGEN_RUSTC_SEMVER"), 71 | rustc_commit_hash: env!("VERGEN_RUSTC_COMMIT_HASH"), 72 | rustc_host_triple: env!("VERGEN_RUSTC_HOST_TRIPLE"), 73 | rustc_channel: env!("VERGEN_RUSTC_CHANNEL"), 74 | rustc_llvm_version: env!("VERGEN_RUSTC_LLVM_VERSION"), 75 | } 76 | } 77 | 78 | fn program_build(&self) -> String { 79 | format!( 80 | "{} with opt_level({}) at {}", 81 | self.program_target_triple, self.program_opt_level, self.program_build_timestamp 82 | ) 83 | } 84 | 85 | fn rustc_build(&self) -> String { 86 | format!( 87 | "{}-{} with LLVM {}", 88 | self.rustc_host_triple, self.rustc_channel, self.rustc_llvm_version 89 | ) 90 | } 91 | } 92 | 93 | impl Default for VersionInfo { 94 | fn default() -> Self { 95 | Self::new() 96 | } 97 | } 98 | 99 | /// Print version information and exit if `-VV` is present 100 | /// 101 | /// # Panics 102 | /// panics if cannot print version information 103 | pub fn intercept_version(v: bool, f: VersionFormat) { 104 | let f = match f { 105 | VersionFormat::None if v => VersionFormat::Short, 106 | VersionFormat::None => return, 107 | _ => f, 108 | }; 109 | let version_info = VersionInfo::new(); 110 | match f { 111 | VersionFormat::Full => print_full_version(version_info), 112 | VersionFormat::Features => println!("{}", version_info.features.join(",")), 113 | VersionFormat::Json => { 114 | println!("{}", serde_json::to_string_pretty(&version_info).unwrap()) 115 | } 116 | VersionFormat::JsonPlain => println!("{}", serde_json::to_string(&version_info).unwrap()), 117 | _ => print_short_version(version_info), 118 | } 119 | exit(0); 120 | } 121 | 122 | fn print_full_version(vi: VersionInfo) { 123 | let program_semver = vi.program_semver; 124 | let program_commit_hash = vi.program_commit_hash; 125 | let program_build = vi.program_build(); 126 | 127 | let rustc_semver = vi.rustc_semver; 128 | let rustc_commit_hash = vi.rustc_commit_hash; 129 | let rustc_build = vi.rustc_build(); 130 | 131 | print_short_version(vi); 132 | println!( 133 | r##" 134 | program-ver: {program_semver} 135 | program-rev: {program_commit_hash} 136 | program-build: {program_build} 137 | 138 | rustc-ver: {rustc_semver} 139 | rustc-rev: {rustc_commit_hash} 140 | rustc-build: {rustc_build}"## 141 | ); 142 | } 143 | 144 | fn print_short_version(vi: VersionInfo) { 145 | let name = vi.name; 146 | let version = vi.version; 147 | let features = vi 148 | .features 149 | .iter() 150 | .copied() 151 | .filter(|&s| s != "default" && !s.ends_with("_exporter")) 152 | .collect::>() 153 | .join(" "); 154 | 155 | println!( 156 | r##"{name} version {version} 157 | features: {features}"## 158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /crates/mitex-glob/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex-glob" 3 | description = "Glob impl for MiTeX" 4 | authors.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | [features] 12 | glob-tests = [] 13 | -------------------------------------------------------------------------------- /crates/mitex-lexer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex-lexer" 3 | description = "Lexer for MiTeX" 4 | authors.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | 13 | mitex-spec.workspace = true 14 | 15 | logos.workspace = true 16 | ena.workspace = true 17 | ecow.workspace = true 18 | rustc-hash.workspace = true 19 | once_cell.workspace = true 20 | 21 | [dev-dependencies] 22 | mitex-spec-gen.workspace = true 23 | 24 | insta.workspace = true 25 | divan.workspace = true 26 | 27 | [lints] 28 | workspace = true 29 | -------------------------------------------------------------------------------- /crates/mitex-lexer/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Given source strings, MiTeX Lexer provides a sequence of tokens 2 | //! 3 | //! The core of the lexer is [`Lexer<'a, S>`] which receives a string `&'a str` 4 | //! and a [`TokenStream`] trait object `S`, then it provides public methods to 5 | //! peek and bump the token stream. 6 | //! 7 | //! It has two main lexer implementations: 8 | //! - [`Lexer<()>`]: provides plain tokens 9 | //! - See [`TokenStream`] for implementation 10 | //! - [`Lexer`]: provides tokens with macro expansion 11 | //! - See [`MacroEngine`] for implementation 12 | 13 | mod macro_engine; 14 | pub mod snapshot_map; 15 | mod stream; 16 | mod token; 17 | 18 | pub use macro_engine::MacroEngine; 19 | pub use token::{BraceKind, CommandName, IfCommandName, Token}; 20 | 21 | use logos::Logos; 22 | use mitex_spec::CommandSpec; 23 | 24 | use macro_engine::Macro; 25 | use stream::{LexCache, StreamContext}; 26 | 27 | /// MiTeX's token representation 28 | /// A token is a pair of a token kind and its text 29 | type Tok<'a> = (Token, &'a str); 30 | 31 | /// A trait for bumping the token stream 32 | /// Its bumping is less frequently called than token peeking 33 | pub trait TokenStream<'a>: MacroifyStream<'a> { 34 | /// Bump the token stream with at least one token if possible 35 | /// 36 | /// By default, it fills the peek cache with a page of tokens at the same 37 | /// time 38 | fn bump(&mut self, ctx: &mut StreamContext<'a>) { 39 | ctx.peek_outer.bump(std::iter::from_fn(|| { 40 | StreamContext::lex_one(&mut ctx.inner) 41 | })); 42 | } 43 | } 44 | 45 | /// Trait for querying macro state of a stream 46 | pub trait MacroifyStream<'a> { 47 | /// Get a macro by name (if meeted in the stream) 48 | fn get_macro(&self, _name: &str) -> Option> { 49 | None 50 | } 51 | } 52 | 53 | /// The default implementation of [`TokenStream`] 54 | /// 55 | /// See [`LexCache<'a>`] for implementation 56 | impl TokenStream<'_> for () {} 57 | 58 | /// The default implementation of [`MacroifyStream`] 59 | impl MacroifyStream<'_> for () {} 60 | 61 | /// Small memory-efficient lexer for TeX 62 | /// 63 | /// It gets improved performance on x86_64 but not wasm through 64 | #[derive(Debug, Clone)] 65 | pub struct Lexer<'a, S: TokenStream<'a> = ()> { 66 | /// A stream context shared with the bumper 67 | ctx: StreamContext<'a>, 68 | /// Implementations to bump the token stream into [`Self::ctx`] 69 | bumper: S, 70 | } 71 | 72 | impl<'a, S: TokenStream<'a>> Lexer<'a, S> { 73 | /// Create a new lexer on a main input source 74 | /// 75 | /// Note that since we have a bumper, the returning string is not always 76 | /// sliced from the input 77 | pub fn new(input: &'a str, spec: CommandSpec) -> Self 78 | where 79 | S: Default, 80 | { 81 | Self::new_with_bumper(input, spec, S::default()) 82 | } 83 | 84 | /// Create a new lexer on a main input source with a bumper 85 | /// 86 | /// Note that since we have a bumper, the returning string is not always 87 | /// sliced from the input 88 | pub fn new_with_bumper(input: &'a str, spec: CommandSpec, bumper: S) -> Self { 89 | let inner = Token::lexer_with_extras(input, (spec, 0..0)); 90 | let mut n = Self { 91 | ctx: StreamContext { 92 | inner, 93 | peek_outer: LexCache::default(), 94 | peek_inner: LexCache::default(), 95 | }, 96 | bumper, 97 | }; 98 | n.next(); 99 | 100 | n 101 | } 102 | 103 | /// Private method to advance the lexer by one token 104 | #[inline] 105 | fn next(&mut self) { 106 | if let Some(peeked) = self.ctx.peek_outer.buf.pop() { 107 | self.ctx.peek_outer.peeked = Some(peeked); 108 | return; 109 | } 110 | 111 | // it is not likely to be inlined 112 | self.bumper.bump(&mut self.ctx); 113 | } 114 | 115 | /// Peek the next token 116 | pub fn peek(&self) -> Option { 117 | self.ctx.peek_outer.peeked.map(|(kind, _)| kind) 118 | } 119 | 120 | /// Peek the next token's text 121 | pub fn peek_text(&self) -> Option<&'a str> { 122 | self.ctx.peek_outer.peeked.map(|(_, text)| text) 123 | } 124 | 125 | /// Peek the next token's first char 126 | pub fn peek_char(&self) -> Option { 127 | self.peek_text().map(str::chars).and_then(|mut e| e.next()) 128 | } 129 | 130 | /// Update the text part of the peeked token 131 | pub fn consume_utf8_bytes(&mut self, cnt: usize) { 132 | let Some(peek_mut) = &mut self.ctx.peek_outer.peeked else { 133 | return; 134 | }; 135 | if peek_mut.1.len() <= cnt { 136 | self.next(); 137 | } else { 138 | peek_mut.1 = &peek_mut.1[cnt..]; 139 | } 140 | } 141 | 142 | /// Update the peeked token and return the old one 143 | pub fn eat(&mut self) -> Option<(Token, &'a str)> { 144 | let peeked = self.ctx.peek_outer.peeked.take()?; 145 | self.next(); 146 | Some(peeked) 147 | } 148 | 149 | /// Find a **currently** defined macro by name 150 | pub fn get_macro(&mut self, name: &str) -> Option> { 151 | self.bumper.get_macro(name) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /crates/mitex-lexer/src/snapshot_map.rs: -------------------------------------------------------------------------------- 1 | //! Upstream [rustc_data_structures::snapshot_map]. 2 | //! Last checked commit: f4bb4500ddb4 3 | //! Last checked time: 2023-12-28 4 | //! 5 | //! [rustc_data_structures::snapshot_map]: https://github.com/rust-lang/rust/blob/master/compiler/rustc_data_structures/src/snapshot_map/mod.rs 6 | 7 | #![allow(missing_docs)] 8 | 9 | use ena::undo_log::{Rollback, Snapshots, UndoLogs, VecLog}; 10 | use std::borrow::{Borrow, BorrowMut}; 11 | use std::hash::Hash; 12 | use std::marker::PhantomData; 13 | use std::ops; 14 | 15 | pub use ena::undo_log::Snapshot; 16 | 17 | type FxHashMap = rustc_hash::FxHashMap; 18 | 19 | pub type SnapshotMapStorage = SnapshotMap, ()>; 20 | pub type SnapshotMapRef<'a, K, V, L> = SnapshotMap, &'a mut L>; 21 | 22 | #[derive(Clone)] 23 | pub struct SnapshotMap, L = VecLog>> { 24 | map: M, 25 | undo_log: L, 26 | _marker: PhantomData<(K, V)>, 27 | } 28 | 29 | // HACK(eddyb) manual impl avoids `Default` bounds on `K` and `V`. 30 | impl Default for SnapshotMap 31 | where 32 | M: Default, 33 | L: Default, 34 | { 35 | fn default() -> Self { 36 | SnapshotMap { 37 | map: Default::default(), 38 | undo_log: Default::default(), 39 | _marker: PhantomData, 40 | } 41 | } 42 | } 43 | 44 | #[derive(Clone)] 45 | pub enum UndoLog { 46 | Inserted(K), 47 | Overwrite(K, V), 48 | Purged, 49 | } 50 | 51 | impl SnapshotMap { 52 | #[inline] 53 | pub fn with_log(&mut self, undo_log: L2) -> SnapshotMap { 54 | SnapshotMap { 55 | map: &mut self.map, 56 | undo_log, 57 | _marker: PhantomData, 58 | } 59 | } 60 | } 61 | 62 | impl SnapshotMap 63 | where 64 | K: Hash + Clone + Eq, 65 | M: BorrowMut> + Borrow>, 66 | L: UndoLogs>, 67 | { 68 | pub fn clear(&mut self) { 69 | self.map.borrow_mut().clear(); 70 | self.undo_log.clear(); 71 | } 72 | 73 | pub fn insert(&mut self, key: K, value: V) -> bool { 74 | match self.map.borrow_mut().insert(key.clone(), value) { 75 | None => { 76 | self.undo_log.push(UndoLog::Inserted(key)); 77 | true 78 | } 79 | Some(old_value) => { 80 | self.undo_log.push(UndoLog::Overwrite(key, old_value)); 81 | false 82 | } 83 | } 84 | } 85 | 86 | pub fn remove(&mut self, key: K) -> bool { 87 | match self.map.borrow_mut().remove(&key) { 88 | Some(old_value) => { 89 | self.undo_log.push(UndoLog::Overwrite(key, old_value)); 90 | true 91 | } 92 | None => false, 93 | } 94 | } 95 | 96 | pub fn get(&self, k: &Q) -> Option<&V> 97 | where 98 | K: Borrow, 99 | Q: Hash + Eq + ?Sized, 100 | { 101 | self.map.borrow().get(k) 102 | } 103 | } 104 | 105 | impl SnapshotMap 106 | where 107 | K: Hash + Clone + Eq, 108 | { 109 | pub fn snapshot(&mut self) -> Snapshot { 110 | self.undo_log.start_snapshot() 111 | } 112 | 113 | pub fn commit(&mut self, snapshot: Snapshot) { 114 | self.undo_log.commit(snapshot) 115 | } 116 | 117 | pub fn rollback_to(&mut self, snapshot: Snapshot) { 118 | let map = &mut self.map; 119 | self.undo_log.rollback_to(|| map, snapshot) 120 | } 121 | } 122 | 123 | impl<'k, K, V, M, L> ops::Index<&'k K> for SnapshotMap 124 | where 125 | K: Hash + Clone + Eq, 126 | M: Borrow>, 127 | { 128 | type Output = V; 129 | fn index(&self, key: &'k K) -> &V { 130 | &self.map.borrow()[key] 131 | } 132 | } 133 | 134 | impl Rollback> for SnapshotMap 135 | where 136 | K: Eq + Hash, 137 | M: Rollback>, 138 | { 139 | fn reverse(&mut self, undo: UndoLog) { 140 | self.map.reverse(undo) 141 | } 142 | } 143 | 144 | impl Rollback> for FxHashMap 145 | where 146 | K: Eq + Hash, 147 | { 148 | fn reverse(&mut self, undo: UndoLog) { 149 | match undo { 150 | UndoLog::Inserted(key) => { 151 | self.remove(&key); 152 | } 153 | 154 | UndoLog::Overwrite(key, old_value) => { 155 | self.insert(key, old_value); 156 | } 157 | 158 | UndoLog::Purged => {} 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /crates/mitex-lexer/src/stream.rs: -------------------------------------------------------------------------------- 1 | use logos::Source; 2 | 3 | use crate::{BraceKind, CommandName, Tok, Token}; 4 | 5 | /// Lex Cache for bundling (bumping) lexing operations for CPU locality 6 | #[derive(Debug, Clone)] 7 | pub struct LexCache<'a> { 8 | /// The last peeked token 9 | pub peeked: Option>, 10 | /// A reversed sequence of peeked tokens 11 | pub buf: Vec>, 12 | } 13 | 14 | impl Default for LexCache<'_> { 15 | fn default() -> Self { 16 | Self { 17 | peeked: None, 18 | buf: Vec::with_capacity(8), 19 | } 20 | } 21 | } 22 | 23 | impl<'a> LexCache<'a> { 24 | /// Extend the peek cache with a sequence of tokens 25 | /// 26 | /// Note: the tokens in the cache are reversed 27 | fn extend(&mut self, peeked: impl Iterator>) { 28 | // Push the peeked token back to the peek cache 29 | let peeking = if let Some(peeked) = self.peeked { 30 | self.buf.push(peeked); 31 | true 32 | } else { 33 | false 34 | }; 35 | 36 | self.buf.extend(peeked); 37 | 38 | // Pop the first token again 39 | if peeking { 40 | self.peeked = self.buf.pop(); 41 | } 42 | } 43 | 44 | /// Fill the peek cache with a page of tokens at the same time 45 | pub fn bump(&mut self, peeked: impl Iterator>) { 46 | assert!( 47 | self.buf.is_empty(), 48 | "all of tokens in the peek cache should be consumed before bumping", 49 | ); 50 | 51 | /// The size of a page, in some architectures it is 16384B but that 52 | /// doesn't matter, we only need a sensible value 53 | const PAGE_SIZE: usize = 4096; 54 | /// The item size of the peek cache 55 | const PEEK_CACHE_SIZE: usize = (PAGE_SIZE - 16) / std::mem::size_of::>(); 56 | 57 | // Fill the peek cache with a page of tokens 58 | self.buf.extend(peeked.take(PEEK_CACHE_SIZE)); 59 | // Reverse the peek cache to make it a stack 60 | self.buf.reverse(); 61 | // Pop the first token again 62 | self.peeked = self.buf.pop(); 63 | } 64 | } 65 | 66 | /// A stream context for [`Lexer`] 67 | #[derive(Debug, Clone)] 68 | pub struct StreamContext<'a> { 69 | /// Input source 70 | /// The inner lexer 71 | pub inner: logos::Lexer<'a, Token>, 72 | 73 | /// Outer peek 74 | pub peek_outer: LexCache<'a>, 75 | /// Inner peek 76 | pub peek_inner: LexCache<'a>, 77 | } 78 | 79 | impl<'a> StreamContext<'a> { 80 | #[inline] 81 | pub fn lex_one(l: &mut logos::Lexer<'a, Token>) -> Option> { 82 | let tok = l.next()?.unwrap(); 83 | 84 | let source_text = match tok { 85 | Token::CommandName(CommandName::BeginEnvironment | CommandName::EndEnvironment) => { 86 | l.source().slice(l.extras.1.clone()).unwrap() 87 | } 88 | _ => l.slice(), 89 | }; 90 | 91 | Some((tok, source_text)) 92 | } 93 | 94 | // Inner bumping is not cached 95 | #[inline] 96 | pub fn next_token(&mut self) { 97 | let peeked = self 98 | .peek_inner 99 | .buf 100 | .pop() 101 | .or_else(|| Self::lex_one(&mut self.inner)); 102 | self.peek_inner.peeked = peeked; 103 | } 104 | 105 | #[inline] 106 | pub fn next_full(&mut self) -> Option> { 107 | self.next_token(); 108 | self.peek_inner.peeked 109 | } 110 | 111 | #[inline] 112 | pub fn peek_full(&mut self) -> Option> { 113 | self.peek_inner.peeked 114 | } 115 | 116 | pub fn peek(&mut self) -> Option { 117 | self.peek_inner.peeked.map(|(kind, _)| kind) 118 | } 119 | 120 | #[inline] 121 | pub fn next_stream(&mut self) -> impl Iterator> + '_ { 122 | std::iter::from_fn(|| self.next_full()) 123 | } 124 | 125 | #[inline] 126 | pub fn peek_stream(&mut self) -> impl Iterator> + '_ { 127 | self.peek_full().into_iter().chain(self.next_stream()) 128 | } 129 | 130 | pub fn next_not_trivia(&mut self) -> Option { 131 | self.next_stream().map(|e| e.0).find(|e| !e.is_trivia()) 132 | } 133 | 134 | pub fn peek_not_trivia(&mut self) -> Option { 135 | self.peek_stream().map(|e| e.0).find(|e| !e.is_trivia()) 136 | } 137 | 138 | pub fn eat_if(&mut self, tk: Token) { 139 | if self.peek_inner.peeked.map_or(false, |e| e.0 == tk) { 140 | self.next_token(); 141 | } 142 | } 143 | 144 | pub fn push_outer(&mut self, peeked: Tok<'a>) { 145 | self.peek_outer.buf.push(peeked); 146 | } 147 | 148 | pub fn extend_inner(&mut self, peeked: impl Iterator>) { 149 | self.peek_inner.extend(peeked); 150 | } 151 | 152 | pub fn peek_u8_opt(&mut self, bk: BraceKind) -> Option { 153 | let res = self 154 | .peek_full() 155 | .filter(|res| matches!(res.0, Token::Word)) 156 | .and_then(|(_, text)| text.parse().ok()); 157 | self.next_not_trivia()?; 158 | 159 | self.eat_if(Token::Right(bk)); 160 | 161 | res 162 | } 163 | 164 | pub fn peek_word_opt(&mut self, bk: BraceKind) -> Option> { 165 | let res = self.peek_full().filter(|res| matches!(res.0, Token::Word)); 166 | self.next_not_trivia()?; 167 | 168 | self.eat_if(Token::Right(bk)); 169 | 170 | res 171 | } 172 | 173 | pub fn peek_cmd_name_opt(&mut self, bk: BraceKind) -> Option> { 174 | let res = self 175 | .peek_full() 176 | .filter(|res| matches!(res.0, Token::CommandName(..))); 177 | 178 | self.next_not_trivia()?; 179 | self.eat_if(Token::Right(bk)); 180 | 181 | res 182 | } 183 | 184 | pub fn read_until_balanced(&mut self, bk: BraceKind) -> Vec> { 185 | let until_tok = Token::Right(bk); 186 | 187 | let mut curly_level = 0; 188 | let match_curly = &mut |e: Token| { 189 | if curly_level == 0 && e == until_tok { 190 | return false; 191 | } 192 | 193 | match e { 194 | Token::Left(BraceKind::Curly) => curly_level += 1, 195 | Token::Right(BraceKind::Curly) => curly_level -= 1, 196 | _ => {} 197 | } 198 | 199 | true 200 | }; 201 | 202 | let res = self 203 | .peek_stream() 204 | .take_while(|(e, _)| match_curly(*e)) 205 | .collect(); 206 | 207 | self.eat_if(until_tok); 208 | res 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /crates/mitex-lexer/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/mitex-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex-parser" 3 | description = "Parser for MiTeX" 4 | authors.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [[bench]] 14 | name = "simple" 15 | harness = false 16 | 17 | [dependencies] 18 | 19 | mitex-glob.workspace = true 20 | mitex-lexer.workspace = true 21 | mitex-spec.workspace = true 22 | 23 | rowan.workspace = true 24 | 25 | [dev-dependencies] 26 | 27 | mitex-spec-gen.workspace = true 28 | 29 | once_cell.workspace = true 30 | insta.workspace = true 31 | divan.workspace = true 32 | 33 | [lints] 34 | workspace = true 35 | -------------------------------------------------------------------------------- /crates/mitex-parser/src/arg_match.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use crate::{argument_kind::ARGUMENT_KIND_TERM, ArgPattern}; 4 | use mitex_glob::glob_match_prefix; 5 | use mitex_spec::GlobStr; 6 | 7 | /// A matcher for arguments of a TeX command 8 | /// It is created by `ArgMatcherBuilder` 9 | pub enum ArgMatcher { 10 | /// None of arguments is passed, i.e. it is processed as a 11 | /// variable in typst. 12 | /// Note: this is different from FixedLenTerm(0) 13 | /// Where, \alpha is None, but not FixedLenTerm(0) 14 | /// E.g. \alpha => $alpha$ 15 | None, 16 | /// Fixed or Range length pattern, equivalent to `/t{0,x}/g` 17 | /// E.g. \hat x y => $hat(x) y$, 18 | /// E.g. 1 \sum\limits => $1 limits(sum)$, 19 | AtMostTerm { max: u8, counter: u8 }, 20 | /// Receive terms as much as possible, 21 | /// equivalent to `/t*/g` 22 | /// E.g. \over, \displaystyle 23 | Greedy, 24 | /// Most powerful pattern, but slightly slow 25 | /// Note that the glob must accept all prefix of the input 26 | /// 27 | /// E.g. \sqrt has a glob pattern of `{,b}t` 28 | /// Description: 29 | /// - {,b}: first, it matches an bracket option, e.g. `\sqrt[3]` 30 | /// - t: it later matches a single term, e.g. `\sqrt[3]{a}` or `\sqrt{a}` 31 | /// 32 | /// Note: any prefix of the glob is valid in parse stage hence you need to 33 | /// check whether it is complete in later stage. 34 | Glob { re: GlobStr, prefix: String }, 35 | } 36 | 37 | impl ArgMatcher { 38 | /// Check if the matcher is greedy 39 | pub fn is_greedy(&self) -> bool { 40 | matches!(self, Self::Greedy) 41 | } 42 | 43 | /// Check if the matcher is ending match with that char 44 | /// 45 | /// Return true if modified as term 46 | pub fn match_as_term(&mut self, text: char) -> Option { 47 | match self { 48 | Self::None => None, 49 | Self::Greedy => Some(text != ARGUMENT_KIND_TERM), 50 | Self::AtMostTerm { .. } => self 51 | .try_match(ARGUMENT_KIND_TERM) 52 | .then_some(text != ARGUMENT_KIND_TERM), 53 | Self::Glob { .. } => self.try_match(text).then_some(false), 54 | } 55 | } 56 | 57 | /// Check if the matcher is ending match with that char 58 | pub fn try_match(&mut self, text: char) -> bool { 59 | match self { 60 | Self::None => false, 61 | Self::Greedy => true, 62 | Self::AtMostTerm { ref max, counter } => { 63 | // println!("try match {} {}, {}", text, self.counter, max); 64 | if text != ARGUMENT_KIND_TERM { 65 | return false; 66 | } 67 | let ct = *counter < *max; 68 | *counter += 1; 69 | ct 70 | } 71 | Self::Glob { ref re, prefix } => { 72 | prefix.push(text); 73 | glob_match_prefix(&re.0, prefix) 74 | } 75 | } 76 | } 77 | } 78 | 79 | #[derive(Default)] 80 | pub struct ArgMatcherBuilder {} 81 | 82 | impl fmt::Debug for ArgMatcherBuilder { 83 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 84 | f.debug_struct("ArgMatcherBuilder").finish() 85 | } 86 | } 87 | 88 | impl ArgMatcherBuilder { 89 | pub fn start_match(&mut self, pat_meta: &ArgPattern) -> ArgMatcher { 90 | match pat_meta { 91 | ArgPattern::None => ArgMatcher::None, 92 | ArgPattern::RangeLenTerm { max: mx, .. } | ArgPattern::FixedLenTerm { len: mx } => { 93 | if mx == &0 { 94 | ArgMatcher::None 95 | } else { 96 | ArgMatcher::AtMostTerm { 97 | max: *mx, 98 | counter: 0, 99 | } 100 | } 101 | } 102 | ArgPattern::Greedy => ArgMatcher::Greedy, 103 | ArgPattern::Glob { pattern: re } => ArgMatcher::Glob { 104 | re: re.clone(), 105 | prefix: String::new(), 106 | }, 107 | } 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use mitex_glob::glob_match_prefix; 114 | 115 | #[test] 116 | fn glob_prefix() { 117 | assert!(glob_match_prefix("abc", "")); 118 | assert!(glob_match_prefix("abc", "a")); 119 | assert!(glob_match_prefix("abc", "ab")); 120 | assert!(glob_match_prefix("abc", "abc")); 121 | assert!(!glob_match_prefix("abc", "b")); 122 | assert!(!glob_match_prefix("abc", "ac")); 123 | assert!(!glob_match_prefix("abc", "abd")); 124 | assert!(!glob_match_prefix("abc", "abcd")); 125 | assert!(!glob_match_prefix("abc", "abca")); 126 | } 127 | 128 | #[test] 129 | fn glob_negated_pattern() { 130 | assert!(!glob_match_prefix("!", "")); 131 | assert!(!glob_match_prefix("!abc", "")); 132 | assert!(!glob_match_prefix("!abc", "a")); 133 | assert!(!glob_match_prefix("!abc", "ab")); 134 | assert!(!glob_match_prefix("!abc", "abc")); 135 | assert!(glob_match_prefix("!abc", "b")); 136 | assert!(glob_match_prefix("!abc", "ac")); 137 | assert!(glob_match_prefix("!abc", "abd")); 138 | assert!(glob_match_prefix("!abc", "abcd")); 139 | assert!(glob_match_prefix("!abc", "abca")); 140 | } 141 | 142 | #[test] 143 | fn glob_sqrt() { 144 | assert!(glob_match_prefix("{,b}t", "t")); 145 | assert!(glob_match_prefix("{,b}t", "")); 146 | assert!(glob_match_prefix("{,b}t", "b")); 147 | assert!(glob_match_prefix("{,b}t", "bt")); 148 | assert!(!glob_match_prefix("{,b}t", "tt")); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /crates/mitex-parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Given source strings, MiTeX Parser provides an AST (abstract syntax tree). 2 | //! 3 | //! ## Option: Command Specification 4 | //! The parser retrieves a command specification which defines shape of 5 | //! commands. With the specification, the parser can parse commands correctly. 6 | //! Otherwise, all commands are parsed as barely names without arguments. 7 | //! 8 | //! ## Produce: AST 9 | //! It returns an untyped syntax node representing the AST defined by [`rowan`]. 10 | //! You can access the AST conveniently with interfaces provided by 11 | //! [`rowan::SyntaxNode`]. 12 | //! 13 | //! The untyped syntax node can convert to typed ones defined in 14 | //! [`crate::syntax`]. 15 | //! 16 | //! The untyped syntax node can also convert to [`rowan::cursor::SyntaxNode`] to 17 | //! modify the AST syntactically. 18 | 19 | mod arg_match; 20 | mod parser; 21 | pub mod syntax; 22 | 23 | pub use mitex_spec as spec; 24 | pub use spec::preludes::command as command_preludes; 25 | pub use spec::*; 26 | use syntax::SyntaxNode; 27 | 28 | use parser::Parser; 29 | 30 | /// Parse the input text with the given command specification 31 | /// and return the untyped syntax tree 32 | /// 33 | /// The error nodes are attached to the tree 34 | pub fn parse(input: &str, spec: CommandSpec) -> SyntaxNode { 35 | SyntaxNode::new_root(Parser::new_macro(input, spec).parse()) 36 | } 37 | 38 | /// It is only for internal testing 39 | pub fn parse_without_macro(input: &str, spec: CommandSpec) -> SyntaxNode { 40 | SyntaxNode::new_root(Parser::new(input, spec).parse()) 41 | } 42 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast.rs: -------------------------------------------------------------------------------- 1 | //! TODO: crate doc 2 | #[allow(missing_docs)] 3 | pub mod common; 4 | 5 | mod ast { 6 | mod prelude { 7 | pub use crate::common::parse_snap as parse; 8 | pub use insta::assert_debug_snapshot; 9 | } 10 | 11 | use prelude::*; 12 | 13 | #[cfg(test)] 14 | mod arg_parse; 15 | 16 | #[cfg(test)] 17 | mod arg_match; 18 | 19 | #[cfg(test)] 20 | mod attachment; 21 | 22 | #[cfg(test)] 23 | mod block_comment; 24 | 25 | #[cfg(test)] 26 | mod formula; 27 | 28 | #[cfg(test)] 29 | mod fuzzing; 30 | 31 | #[cfg(test)] 32 | mod command; 33 | 34 | #[cfg(test)] 35 | mod environment; 36 | 37 | #[cfg(test)] 38 | mod left_right; 39 | 40 | #[cfg(test)] 41 | mod trivia; 42 | 43 | #[cfg(test)] 44 | mod figure; 45 | 46 | #[cfg(test)] 47 | mod tabular; 48 | 49 | /// Convenient function to launch/debug a test case 50 | #[test] 51 | fn bug_playground() {} 52 | 53 | #[test] 54 | fn test_easy() { 55 | assert_debug_snapshot!(parse(r#"\frac{ a }{ b }"#), @r###" 56 | root 57 | |cmd 58 | ||cmd-name("\\frac") 59 | ||args 60 | |||curly 61 | ||||lbrace'("{") 62 | ||||space'(" ") 63 | ||||text(word'("a"),space'(" ")) 64 | ||||rbrace'("}") 65 | ||args 66 | |||curly 67 | ||||lbrace'("{") 68 | ||||space'(" ") 69 | ||||text(word'("b"),space'(" ")) 70 | ||||rbrace'("}") 71 | "###); 72 | } 73 | 74 | #[test] 75 | fn test_utf8_char() { 76 | // note that there is utf8 minus sign in the middle 77 | assert_debug_snapshot!(parse(r#"$u^−$"#), @r###" 78 | root 79 | |formula 80 | ||dollar'("$") 81 | ||attach-comp 82 | |||args 83 | ||||text(word'("u")) 84 | |||caret'("^") 85 | |||word'("−") 86 | ||dollar'("$") 87 | "### 88 | ); 89 | } 90 | 91 | #[test] 92 | fn test_beat_pandoc() { 93 | assert_debug_snapshot!(parse(r#"\frac 1 2 _3"#), @r###" 94 | root 95 | |attach-comp 96 | ||args 97 | |||cmd 98 | ||||cmd-name("\\frac") 99 | ||||args(word'("1")) 100 | ||||args(word'("2")) 101 | |||space'(" ") 102 | ||underscore'("_") 103 | ||word'("3") 104 | "###); 105 | } 106 | 107 | #[test] 108 | fn test_normal() { 109 | assert_debug_snapshot!(parse(r#"\int_1^2 x \mathrm{d} x"#), @r###" 110 | root 111 | |attach-comp 112 | ||args 113 | |||attach-comp 114 | ||||args 115 | |||||cmd(cmd-name("\\int")) 116 | ||||underscore'("_") 117 | ||||word'("1") 118 | ||caret'("^") 119 | ||word'("2") 120 | |space'(" ") 121 | |text(word'("x"),space'(" ")) 122 | |cmd 123 | ||cmd-name("\\mathrm") 124 | ||args 125 | |||curly 126 | ||||lbrace'("{") 127 | ||||text(word'("d")) 128 | ||||rbrace'("}") 129 | |space'(" ") 130 | |text(word'("x")) 131 | "###); 132 | } 133 | 134 | #[test] 135 | fn test_sticky() { 136 | assert_debug_snapshot!(parse(r#"\alpha_1"#), @r###" 137 | root 138 | |attach-comp 139 | ||args 140 | |||cmd(cmd-name("\\alpha")) 141 | ||underscore'("_") 142 | ||word'("1") 143 | "###); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/arg_parse.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// Argument will reset flag of being in a formula 4 | #[test] 5 | fn arg_scope() { 6 | assert_debug_snapshot!(parse(r#"$\text{${1}$}$"#), @r###" 7 | root 8 | |formula 9 | ||dollar'("$") 10 | ||cmd 11 | |||cmd-name("\\text") 12 | |||args 13 | ||||curly 14 | |||||lbrace'("{") 15 | |||||formula 16 | ||||||dollar'("$") 17 | ||||||curly 18 | |||||||lbrace'("{") 19 | |||||||text(word'("1")) 20 | |||||||rbrace'("}") 21 | ||||||dollar'("$") 22 | |||||rbrace'("}") 23 | ||dollar'("$") 24 | "###); 25 | // Note: This is a valid AST, but semantically incorrect (indicated by overleaf) 26 | assert_debug_snapshot!(parse(r#"$\frac{${1}$}{${2}$}$"#), @r###" 27 | root 28 | |formula 29 | ||dollar'("$") 30 | ||cmd 31 | |||cmd-name("\\frac") 32 | |||args 33 | ||||curly 34 | |||||lbrace'("{") 35 | |||||formula 36 | ||||||dollar'("$") 37 | ||||||curly 38 | |||||||lbrace'("{") 39 | |||||||text(word'("1")) 40 | |||||||rbrace'("}") 41 | ||||||dollar'("$") 42 | |||||rbrace'("}") 43 | |||args 44 | ||||curly 45 | |||||lbrace'("{") 46 | |||||formula 47 | ||||||dollar'("$") 48 | ||||||curly 49 | |||||||lbrace'("{") 50 | |||||||text(word'("2")) 51 | |||||||rbrace'("}") 52 | ||||||dollar'("$") 53 | |||||rbrace'("}") 54 | ||dollar'("$") 55 | "###); 56 | } 57 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/attachment.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn base() { 5 | // println!("{:#?}", parse(r#"{}_{1}^1"#)); 6 | assert_debug_snapshot!(parse(r#"_1^2"#), @r###" 7 | root 8 | |attach-comp(underscore'("_"),word'("1")) 9 | |attach-comp(caret'("^"),word'("2")) 10 | "###); 11 | assert_debug_snapshot!(parse(r#"{}_{1}^2"#), @r###" 12 | root 13 | |attach-comp 14 | ||args 15 | |||attach-comp 16 | ||||args 17 | |||||curly(lbrace'("{"),rbrace'("}")) 18 | ||||underscore'("_") 19 | ||||curly 20 | |||||lbrace'("{") 21 | |||||text(word'("1")) 22 | |||||rbrace'("}") 23 | ||caret'("^") 24 | ||word'("2") 25 | "###); 26 | assert_debug_snapshot!(parse(r#"\alpha_1"#), @r###" 27 | root 28 | |attach-comp 29 | ||args 30 | |||cmd(cmd-name("\\alpha")) 31 | ||underscore'("_") 32 | ||word'("1") 33 | "###); 34 | assert_debug_snapshot!(parse(r#"\alpha_[1]"#), @r###" 35 | root 36 | |attach-comp 37 | ||args 38 | |||cmd(cmd-name("\\alpha")) 39 | ||underscore'("_") 40 | ||lbracket'("[") 41 | |text(word'("1")) 42 | |rbracket'("]") 43 | "###); 44 | assert_debug_snapshot!(parse(r#"\alpha_(1)"#), @r###" 45 | root 46 | |attach-comp 47 | ||args 48 | |||cmd(cmd-name("\\alpha")) 49 | ||underscore'("_") 50 | ||lparen'("(") 51 | |text(word'("1")) 52 | |rparen'(")") 53 | "###); 54 | assert_debug_snapshot!(parse(r#"_1"#), @r###" 55 | root 56 | |attach-comp(underscore'("_"),word'("1")) 57 | "###); 58 | // Note: this is an invalid expression 59 | assert_debug_snapshot!(parse(r#"\over_1"#), @r###" 60 | root 61 | |cmd 62 | ||args() 63 | ||cmd-name("\\over") 64 | ||args 65 | |||attach-comp(underscore'("_"),word'("1")) 66 | "###); 67 | assert_debug_snapshot!(parse(r#"{}_1"#), @r###" 68 | root 69 | |attach-comp 70 | ||args 71 | |||curly(lbrace'("{"),rbrace'("}")) 72 | ||underscore'("_") 73 | ||word'("1") 74 | "###); 75 | assert_debug_snapshot!(parse(r#"{}_1_1"#), @r###" 76 | root 77 | |attach-comp 78 | ||args 79 | |||attach-comp 80 | ||||args 81 | |||||curly(lbrace'("{"),rbrace'("}")) 82 | ||||underscore'("_") 83 | ||||word'("1") 84 | ||underscore'("_") 85 | ||word'("1") 86 | "###); 87 | assert_debug_snapshot!(parse(r#"\frac{1}{2}_{3}"#), @r###" 88 | root 89 | |attach-comp 90 | ||args 91 | |||cmd 92 | ||||cmd-name("\\frac") 93 | ||||args 94 | |||||curly 95 | ||||||lbrace'("{") 96 | ||||||text(word'("1")) 97 | ||||||rbrace'("}") 98 | ||||args 99 | |||||curly 100 | ||||||lbrace'("{") 101 | ||||||text(word'("2")) 102 | ||||||rbrace'("}") 103 | ||underscore'("_") 104 | ||curly 105 | |||lbrace'("{") 106 | |||text(word'("3")) 107 | |||rbrace'("}") 108 | "###); 109 | assert_debug_snapshot!(parse(r#"\overbrace{a + b + c}^{\text{This is an overbrace}}"#), @r###" 110 | root 111 | |attach-comp 112 | ||args 113 | |||cmd 114 | ||||cmd-name("\\overbrace") 115 | ||||args 116 | |||||curly 117 | ||||||lbrace'("{") 118 | ||||||text(word'("a"),space'(" "),word'("+"),space'(" "),word'("b"),space'(" "),word'("+"),space'(" "),word'("c")) 119 | ||||||rbrace'("}") 120 | ||caret'("^") 121 | ||curly 122 | |||lbrace'("{") 123 | |||cmd 124 | ||||cmd-name("\\text") 125 | ||||args 126 | |||||curly 127 | ||||||lbrace'("{") 128 | ||||||text(word'("This"),space'(" "),word'("is"),space'(" "),word'("an"),space'(" "),word'("overbrace")) 129 | ||||||rbrace'("}") 130 | |||rbrace'("}") 131 | "###); 132 | assert_debug_snapshot!(parse(r#"\underbrace{x \times y}_{\text{This is an underbrace}}"#), @r###" 133 | root 134 | |attach-comp 135 | ||args 136 | |||cmd 137 | ||||cmd-name("\\underbrace") 138 | ||||args 139 | |||||curly 140 | ||||||lbrace'("{") 141 | ||||||text(word'("x"),space'(" ")) 142 | ||||||cmd(cmd-name("\\times")) 143 | ||||||space'(" ") 144 | ||||||text(word'("y")) 145 | ||||||rbrace'("}") 146 | ||underscore'("_") 147 | ||curly 148 | |||lbrace'("{") 149 | |||cmd 150 | ||||cmd-name("\\text") 151 | ||||args 152 | |||||curly 153 | ||||||lbrace'("{") 154 | ||||||text(word'("This"),space'(" "),word'("is"),space'(" "),word'("an"),space'(" "),word'("underbrace")) 155 | ||||||rbrace'("}") 156 | |||rbrace'("}") 157 | "###); 158 | assert_debug_snapshot!(parse(r#"x_1''^2"#), @r###" 159 | root 160 | |attach-comp 161 | ||args 162 | |||attach-comp 163 | ||||args 164 | |||||attach-comp 165 | ||||||args 166 | |||||||attach-comp 167 | ||||||||args 168 | |||||||||text(word'("x")) 169 | ||||||||underscore'("_") 170 | ||||||||word'("1") 171 | ||||||apostrophe'("'") 172 | ||||apostrophe'("'") 173 | ||caret'("^") 174 | ||word'("2") 175 | "###); 176 | assert_debug_snapshot!(parse(r#"x''_1"#), @r###" 177 | root 178 | |attach-comp 179 | ||args 180 | |||attach-comp 181 | ||||args 182 | |||||attach-comp 183 | ||||||args 184 | |||||||text(word'("x")) 185 | ||||||apostrophe'("'") 186 | ||||apostrophe'("'") 187 | ||underscore'("_") 188 | ||word'("1") 189 | "###); 190 | assert_debug_snapshot!(parse(r#"''"#), @r###" 191 | root(apostrophe'("'"),apostrophe'("'")) 192 | "###); 193 | assert_debug_snapshot!(parse(r#"\frac''"#), @r###" 194 | root 195 | |cmd 196 | ||cmd-name("\\frac") 197 | ||args(apostrophe'("'")) 198 | ||args(apostrophe'("'")) 199 | "###); 200 | } 201 | 202 | #[test] 203 | fn test_attachment_may_weird() { 204 | assert_debug_snapshot!(parse(r#"\frac ab_c"#), @r###" 205 | root 206 | |attach-comp 207 | ||args 208 | |||cmd 209 | ||||cmd-name("\\frac") 210 | ||||args(word'("a")) 211 | ||||args(word'("b")) 212 | ||underscore'("_") 213 | ||word'("c") 214 | "###); 215 | assert_debug_snapshot!(parse(r#"\frac a_c b"#), @r###" 216 | root 217 | |attach-comp 218 | ||args 219 | |||cmd 220 | ||||cmd-name("\\frac") 221 | ||||args(word'("a")) 222 | ||underscore'("_") 223 | ||word'("c") 224 | |space'(" ") 225 | |text(word'("b")) 226 | "###); 227 | assert_debug_snapshot!(parse(r#"\frac {a_c} b"#), @r###" 228 | root 229 | |cmd 230 | ||cmd-name("\\frac") 231 | ||args 232 | |||curly 233 | ||||lbrace'("{") 234 | ||||attach-comp 235 | |||||args 236 | ||||||text(word'("a")) 237 | |||||underscore'("_") 238 | |||||word'("c") 239 | ||||rbrace'("}") 240 | ||args(word'("b")) 241 | "###); 242 | } 243 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/block_comment.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn base() { 5 | assert_debug_snapshot!(parse(r#"\iffalse Test\fi"#), @r###" 6 | root 7 | |block-comment(space'(" "),word'("Test")) 8 | "###); 9 | assert_debug_snapshot!(parse(r#"\iffalse Test\else \LaTeX\fi"#), @r###" 10 | root 11 | |block-comment(space'(" "),word'("Test")) 12 | |space'(" ") 13 | |cmd(cmd-name("\\LaTeX")) 14 | "###); 15 | assert_debug_snapshot!(parse(r#"\iffalse Test\ifhbox Commented HBox\fi\fi"#), @r###" 16 | root 17 | |block-comment(space'(" "),word'("Test"),cmd-name("\\ifhbox"),space'(" "),word'("Commented"),space'(" "),word'("HBox"),cmd-name("\\fi")) 18 | "###); 19 | } 20 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/environment.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn easy() { 5 | assert_debug_snapshot!(parse(r#"\begin{equation}\end{equation}"#), @r###" 6 | root 7 | |env 8 | ||begin(sym'("equation")) 9 | ||end(sym'("equation")) 10 | "###); 11 | } 12 | 13 | #[test] 14 | fn matrix() { 15 | assert_debug_snapshot!(parse( 16 | r#"\begin{matrix} 17 | a & b \\ 18 | c & d 19 | \end{matrix}"#), @r###" 20 | root 21 | |env 22 | ||begin(sym'("matrix")) 23 | ||br'("\n") 24 | ||text(word'("a"),space'(" ")) 25 | ||ampersand'("&") 26 | ||space'(" ") 27 | ||text(word'("b"),space'(" ")) 28 | ||newline("\\\\") 29 | ||br'("\n") 30 | ||text(word'("c"),space'(" ")) 31 | ||ampersand'("&") 32 | ||space'(" ") 33 | ||text(word'("d"),br'("\n")) 34 | ||end(sym'("matrix")) 35 | "###); 36 | assert_debug_snapshot!(parse( 37 | r#"\begin{pmatrix}\\\end{pmatrix}"#), @r###" 38 | root 39 | |env 40 | ||begin(sym'("pmatrix")) 41 | ||newline("\\\\") 42 | ||end(sym'("pmatrix")) 43 | "###); 44 | assert_debug_snapshot!(parse( 45 | r#"\begin{pmatrix}x{\\}x\end{pmatrix}"#), @r###" 46 | root 47 | |env 48 | ||begin(sym'("pmatrix")) 49 | ||text(word'("x")) 50 | ||curly(lbrace'("{"),newline("\\\\"),rbrace'("}")) 51 | ||text(word'("x")) 52 | ||end(sym'("pmatrix")) 53 | "###); 54 | } 55 | 56 | #[test] 57 | fn arguments() { 58 | assert_debug_snapshot!(parse( 59 | r#"\begin{array}{lc} 60 | a & b \\ 61 | c & d 62 | \end{array}"#), @r###" 63 | root 64 | |env 65 | ||begin 66 | |||sym'("array") 67 | |||args 68 | ||||curly 69 | |||||lbrace'("{") 70 | |||||text(word'("lc")) 71 | |||||rbrace'("}") 72 | ||br'("\n") 73 | ||text(word'("a"),space'(" ")) 74 | ||ampersand'("&") 75 | ||space'(" ") 76 | ||text(word'("b"),space'(" ")) 77 | ||newline("\\\\") 78 | ||br'("\n") 79 | ||text(word'("c"),space'(" ")) 80 | ||ampersand'("&") 81 | ||space'(" ") 82 | ||text(word'("d"),br'("\n")) 83 | ||end(sym'("array")) 84 | "###); 85 | } 86 | 87 | #[test] 88 | fn space_around_and() { 89 | assert_debug_snapshot!(parse( 90 | r#"\begin{bmatrix}A&B\end{bmatrix}"#), @r###" 91 | root 92 | |env 93 | ||begin(sym'("bmatrix")) 94 | ||text(word'("A")) 95 | ||ampersand'("&") 96 | ||text(word'("B")) 97 | ||end(sym'("bmatrix")) 98 | "###); 99 | } 100 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/figure.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn figure() { 5 | assert_debug_snapshot!(parse(r###" 6 | \begin{figure}[ht] 7 | \centering 8 | \includegraphics[width=0.5\textwidth]{example-image} 9 | \caption{This is an example image.} 10 | \label{fig:example} 11 | \end{figure} 12 | "###), @r###" 13 | root 14 | |br'("\n") 15 | |space'(" ") 16 | |env 17 | ||begin 18 | |||sym'("figure") 19 | |||args 20 | ||||bracket 21 | |||||lbracket'("[") 22 | |||||text(word'("ht")) 23 | |||||rbracket'("]") 24 | ||br'("\n") 25 | ||space'(" ") 26 | ||cmd(cmd-name("\\centering")) 27 | ||br'("\n") 28 | ||space'(" ") 29 | ||cmd 30 | |||cmd-name("\\includegraphics") 31 | |||args 32 | ||||bracket 33 | |||||lbracket'("[") 34 | |||||text(word'("width=0.5")) 35 | |||||cmd(cmd-name("\\textwidth")) 36 | |||||rbracket'("]") 37 | |||args 38 | ||||curly 39 | |||||lbrace'("{") 40 | |||||text(word'("example-image")) 41 | |||||rbrace'("}") 42 | ||br'("\n") 43 | ||space'(" ") 44 | ||cmd 45 | |||cmd-name("\\caption") 46 | |||args 47 | ||||curly 48 | |||||lbrace'("{") 49 | |||||text(word'("This"),space'(" "),word'("is"),space'(" "),word'("an"),space'(" "),word'("example"),space'(" "),word'("image.")) 50 | |||||rbrace'("}") 51 | ||br'("\n") 52 | ||space'(" ") 53 | ||cmd 54 | |||cmd-name("\\label") 55 | |||args 56 | ||||curly 57 | |||||lbrace'("{") 58 | |||||text(word'("fig:example")) 59 | |||||rbrace'("}") 60 | ||br'("\n") 61 | ||space'(" ") 62 | ||end(sym'("figure")) 63 | |br'("\n") 64 | |space'(" ") 65 | "###); 66 | } 67 | 68 | #[test] 69 | fn table() { 70 | assert_debug_snapshot!(parse(r###" 71 | \begin{table}[ht] 72 | \centering 73 | \begin{tabular}{|c|c|} 74 | \hline 75 | \textbf{Name} & \textbf{Age} \\ 76 | \hline 77 | John & 25 \\ 78 | Jane & 22 \\ 79 | \hline 80 | \end{tabular} 81 | \caption{This is an example table.} 82 | \label{tab:example} 83 | \end{table} 84 | "###), @r###" 85 | root 86 | |br'("\n") 87 | |space'(" ") 88 | |env 89 | ||begin 90 | |||sym'("table") 91 | |||args 92 | ||||bracket 93 | |||||lbracket'("[") 94 | |||||text(word'("ht")) 95 | |||||rbracket'("]") 96 | ||br'("\n") 97 | ||space'(" ") 98 | ||cmd(cmd-name("\\centering")) 99 | ||br'("\n") 100 | ||space'(" ") 101 | ||env 102 | |||begin 103 | ||||sym'("tabular") 104 | ||||args 105 | |||||curly 106 | ||||||lbrace'("{") 107 | ||||||text(word'("|c|c|")) 108 | ||||||rbrace'("}") 109 | |||br'("\n") 110 | |||space'(" ") 111 | |||cmd(cmd-name("\\hline")) 112 | |||br'("\n") 113 | |||space'(" ") 114 | |||cmd 115 | ||||cmd-name("\\textbf") 116 | ||||args 117 | |||||curly 118 | ||||||lbrace'("{") 119 | ||||||text(word'("Name")) 120 | ||||||rbrace'("}") 121 | |||space'(" ") 122 | |||ampersand'("&") 123 | |||space'(" ") 124 | |||cmd 125 | ||||cmd-name("\\textbf") 126 | ||||args 127 | |||||curly 128 | ||||||lbrace'("{") 129 | ||||||text(word'("Age")) 130 | ||||||rbrace'("}") 131 | |||space'(" ") 132 | |||newline("\\\\") 133 | |||br'("\n") 134 | |||space'(" ") 135 | |||cmd(cmd-name("\\hline")) 136 | |||br'("\n") 137 | |||space'(" ") 138 | |||text(word'("John"),space'(" ")) 139 | |||ampersand'("&") 140 | |||space'(" ") 141 | |||text(word'("25"),space'(" ")) 142 | |||newline("\\\\") 143 | |||br'("\n") 144 | |||space'(" ") 145 | |||text(word'("Jane"),space'(" ")) 146 | |||ampersand'("&") 147 | |||space'(" ") 148 | |||text(word'("22"),space'(" ")) 149 | |||newline("\\\\") 150 | |||br'("\n") 151 | |||space'(" ") 152 | |||cmd(cmd-name("\\hline")) 153 | |||br'("\n") 154 | |||space'(" ") 155 | |||end(sym'("tabular")) 156 | ||br'("\n") 157 | ||space'(" ") 158 | ||cmd 159 | |||cmd-name("\\caption") 160 | |||args 161 | ||||curly 162 | |||||lbrace'("{") 163 | |||||text(word'("This"),space'(" "),word'("is"),space'(" "),word'("an"),space'(" "),word'("example"),space'(" "),word'("table.")) 164 | |||||rbrace'("}") 165 | ||br'("\n") 166 | ||space'(" ") 167 | ||cmd 168 | |||cmd-name("\\label") 169 | |||args 170 | ||||curly 171 | |||||lbrace'("{") 172 | |||||text(word'("tab:example")) 173 | |||||rbrace'("}") 174 | ||br'("\n") 175 | ||space'(" ") 176 | ||end(sym'("table")) 177 | |br'("\n") 178 | |space'(" ") 179 | "###); 180 | } 181 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/formula.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn cmd_formula() { 5 | assert_debug_snapshot!(parse(r#"\[\]"#), @r###" 6 | root 7 | |formula(begin-math'("\\["),end-math("\\]")) 8 | "###); 9 | // Note: this is a valid AST, but semantically incorrect 10 | assert_debug_snapshot!(parse(r#"\[\[\]\]"#), @r###" 11 | root 12 | |formula 13 | ||begin-math'("\\[") 14 | ||formula(begin-math'("\\["),end-math("\\]")) 15 | ||end-math("\\]") 16 | "###); 17 | // Note: this is a valid AST, but semantically incorrect 18 | assert_debug_snapshot!(parse(r#"\[\(\)\]"#), @r###" 19 | root 20 | |formula 21 | ||begin-math'("\\[") 22 | ||formula(begin-math'("\\("),end-math("\\)")) 23 | ||end-math("\\]") 24 | "###); 25 | // Note: this is a valid AST, but semantically incorrect 26 | // It looks strange, but we regard it as a valid AST for simplicity 27 | assert_debug_snapshot!(parse(r#"\[\)\(\]"#), @r###" 28 | root 29 | |formula(begin-math'("\\["),end-math("\\)")) 30 | |formula(begin-math'("\\("),end-math("\\]")) 31 | "###); 32 | } 33 | 34 | #[test] 35 | fn formula_scope() { 36 | assert_debug_snapshot!(parse(r#"$[)$ test"#), @r###" 37 | root 38 | |formula(dollar'("$"),lbracket'("["),rparen'(")"),dollar'("$")) 39 | |space'(" ") 40 | |text(word'("test")) 41 | "###); 42 | } 43 | 44 | #[test] 45 | fn curly_scope() { 46 | // Note: this is a broken AST 47 | assert_debug_snapshot!(parse(r#"${$}"#), @r###" 48 | root 49 | |formula 50 | ||dollar'("$") 51 | ||curly 52 | |||lbrace'("{") 53 | |||formula(dollar'("$")) 54 | |||rbrace'("}") 55 | "###); 56 | // Note: this is a valid but incompleted AST, converter should handle it 57 | // correctly 58 | assert_debug_snapshot!(parse(r#"{$}$"#), @r###" 59 | root 60 | |curly 61 | ||lbrace'("{") 62 | ||formula(dollar'("$")) 63 | ||rbrace'("}") 64 | |formula(dollar'("$")) 65 | "###); 66 | } 67 | 68 | #[test] 69 | fn env_scope() { 70 | // Note: this is a valid but incompleted AST, converter should handle it 71 | // correctly 72 | assert_debug_snapshot!(parse(r#"\begin{array}$\end{array}$"#), @r###" 73 | root 74 | |env 75 | ||begin 76 | |||sym'("array") 77 | |||args 78 | ||||formula(dollar'("$")) 79 | ||end(sym'("array")) 80 | |formula(dollar'("$")) 81 | "###); 82 | // Note: this is a valid but incompleted AST, converter should handle it 83 | // correctly 84 | assert_debug_snapshot!(parse(r#"$\begin{array}$\end{array}"#), @r###" 85 | root 86 | |formula 87 | ||dollar'("$") 88 | ||env 89 | |||begin(sym'("array")) 90 | |||formula(dollar'("$")) 91 | |||end(sym'("array")) 92 | "###); 93 | } 94 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/fuzzing.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn test_fuzzing() { 5 | assert_debug_snapshot!(parse(r#"\left\0"#), @r###" 6 | root 7 | |lr 8 | ||clause-lr(cmd-name("\\left"),sym'("\\0")) 9 | "###); 10 | assert_debug_snapshot!(parse(r#"\end{}"#), @r###" 11 | root 12 | |error'(sym'("")) 13 | "###); 14 | } 15 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/left_right.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn base() { 5 | assert_debug_snapshot!(parse(r#"\left.\right."#), @r###" 6 | root 7 | |lr 8 | ||clause-lr(cmd-name("\\left"),word'(".")) 9 | ||clause-lr(cmd-name("\\right"),word'(".")) 10 | "###); 11 | assert_debug_snapshot!(parse(r#"\left.a\right."#), @r###" 12 | root 13 | |lr 14 | ||clause-lr(cmd-name("\\left"),word'(".")) 15 | ||text(word'("a")) 16 | ||clause-lr(cmd-name("\\right"),word'(".")) 17 | "###); 18 | assert_debug_snapshot!(parse(r#"\left. \right] ,"#), @r###" 19 | root 20 | |lr 21 | ||clause-lr(cmd-name("\\left"),word'(".")) 22 | ||space'(" ") 23 | ||clause-lr(cmd-name("\\right"),rbracket'("]")) 24 | |space'(" ") 25 | |text(comma'(",")) 26 | "###); 27 | assert_debug_snapshot!(parse(r#"\left . a \right \|"#), @r###" 28 | root 29 | |lr 30 | ||clause-lr(cmd-name("\\left"),space'(" "),word'(".")) 31 | ||space'(" ") 32 | ||text(word'("a"),space'(" ")) 33 | ||clause-lr(cmd-name("\\right"),space'(" "),sym'("\\|")) 34 | "###); 35 | assert_debug_snapshot!(parse(r#"\left\langle a\right\|"#), @r###" 36 | root 37 | |lr 38 | ||clause-lr(cmd-name("\\left"),sym'("\\langle")) 39 | ||space'(" ") 40 | ||text(word'("a")) 41 | ||clause-lr(cmd-name("\\right"),sym'("\\|")) 42 | "###); 43 | // Note: this is an invalid expression 44 | // Error handling 45 | assert_debug_snapshot!(parse(r#"\left{.}a\right{.}"#), @r###" 46 | root 47 | |lr 48 | ||clause-lr(cmd-name("\\left"),lbrace'("{")) 49 | ||text(word'(".")) 50 | |error'(rbrace'("}")) 51 | |text(word'("a")) 52 | |cmd(cmd-name("\\right")) 53 | |curly 54 | ||lbrace'("{") 55 | ||text(word'(".")) 56 | ||rbrace'("}") 57 | "###); 58 | // Note: this is an invalid expression 59 | // Error handling 60 | assert_debug_snapshot!(parse(r#"\begin{equation}\left.\right\end{equation}"#), @r###" 61 | root 62 | |env 63 | ||begin(sym'("equation")) 64 | ||lr 65 | |||clause-lr(cmd-name("\\left"),word'(".")) 66 | |||clause-lr(cmd-name("\\right")) 67 | ||end(sym'("equation")) 68 | "###); 69 | // Note: this is an invalid expression 70 | // Error handling 71 | assert_debug_snapshot!(parse(r#"\begin{equation}\left\right\end{equation}"#), @r###" 72 | root 73 | |env 74 | ||begin(sym'("equation")) 75 | ||lr 76 | |||clause-lr(cmd-name("\\left")) 77 | |||clause-lr(cmd-name("\\right")) 78 | ||end(sym'("equation")) 79 | "###); 80 | } 81 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/tabular.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn tabular() { 5 | assert_debug_snapshot!(parse(r###" 6 | \begin{tabular}{|c|c|} 7 | \hline 8 | \textbf{Name} & \textbf{Age} \\ 9 | \hline 10 | John & 25 \\ 11 | Jane & 22 \\ 12 | \hline 13 | \end{tabular} 14 | "###), @r###" 15 | root 16 | |br'("\n") 17 | |space'(" ") 18 | |env 19 | ||begin 20 | |||sym'("tabular") 21 | |||args 22 | ||||curly 23 | |||||lbrace'("{") 24 | |||||text(word'("|c|c|")) 25 | |||||rbrace'("}") 26 | ||br'("\n") 27 | ||space'(" ") 28 | ||cmd(cmd-name("\\hline")) 29 | ||br'("\n") 30 | ||space'(" ") 31 | ||cmd 32 | |||cmd-name("\\textbf") 33 | |||args 34 | ||||curly 35 | |||||lbrace'("{") 36 | |||||text(word'("Name")) 37 | |||||rbrace'("}") 38 | ||space'(" ") 39 | ||ampersand'("&") 40 | ||space'(" ") 41 | ||cmd 42 | |||cmd-name("\\textbf") 43 | |||args 44 | ||||curly 45 | |||||lbrace'("{") 46 | |||||text(word'("Age")) 47 | |||||rbrace'("}") 48 | ||space'(" ") 49 | ||newline("\\\\") 50 | ||br'("\n") 51 | ||space'(" ") 52 | ||cmd(cmd-name("\\hline")) 53 | ||br'("\n") 54 | ||space'(" ") 55 | ||text(word'("John"),space'(" ")) 56 | ||ampersand'("&") 57 | ||space'(" ") 58 | ||text(word'("25"),space'(" ")) 59 | ||newline("\\\\") 60 | ||br'("\n") 61 | ||space'(" ") 62 | ||text(word'("Jane"),space'(" ")) 63 | ||ampersand'("&") 64 | ||space'(" ") 65 | ||text(word'("22"),space'(" ")) 66 | ||newline("\\\\") 67 | ||br'("\n") 68 | ||space'(" ") 69 | ||cmd(cmd-name("\\hline")) 70 | ||br'("\n") 71 | ||space'(" ") 72 | ||end(sym'("tabular")) 73 | |br'("\n") 74 | |space'(" ") 75 | "###); 76 | } 77 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/ast/trivia.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn curly_group() { 5 | assert_debug_snapshot!(parse(r#"a \mathbf{strong} text"#), @r###" 6 | root 7 | |text(word'("a"),space'(" ")) 8 | |cmd 9 | ||cmd-name("\\mathbf") 10 | ||args 11 | |||curly 12 | ||||lbrace'("{") 13 | ||||text(word'("strong")) 14 | ||||rbrace'("}") 15 | |space'(" ") 16 | |text(word'("text")) 17 | "###); 18 | } 19 | 20 | #[test] 21 | fn arguments() { 22 | assert_debug_snapshot!(parse(r#"\frac { 1 } { 2 }"#), @r###" 23 | root 24 | |cmd 25 | ||cmd-name("\\frac") 26 | ||args 27 | |||curly 28 | ||||lbrace'("{") 29 | ||||space'(" ") 30 | ||||text(word'("1"),space'(" ")) 31 | ||||rbrace'("}") 32 | ||args 33 | |||curly 34 | ||||lbrace'("{") 35 | ||||space'(" ") 36 | ||||text(word'("2"),space'(" ")) 37 | ||||rbrace'("}") 38 | "###); 39 | } 40 | 41 | #[test] 42 | fn greedy_trivia() { 43 | assert_debug_snapshot!(parse(r#"a {\displaystyle text } b"#), @r###" 44 | root 45 | |text(word'("a"),space'(" ")) 46 | |curly 47 | ||lbrace'("{") 48 | ||cmd 49 | |||cmd-name("\\displaystyle") 50 | |||args 51 | ||||space'(" ") 52 | ||||text(word'("text"),space'(" ")) 53 | ||rbrace'("}") 54 | |space'(" ") 55 | |text(word'("b")) 56 | "###); 57 | assert_debug_snapshot!(parse(r#"\displaystyle text "#), @r###" 58 | root 59 | |cmd 60 | ||cmd-name("\\displaystyle") 61 | ||args 62 | |||space'(" ") 63 | |||text(word'("text"),space'(" ")) 64 | "###); 65 | assert_debug_snapshot!(parse(r#"\displaystyle {text} "#), @r###" 66 | root 67 | |cmd 68 | ||cmd-name("\\displaystyle") 69 | ||args 70 | |||space'(" ") 71 | |||curly 72 | ||||lbrace'("{") 73 | ||||text(word'("text")) 74 | ||||rbrace'("}") 75 | |||space'(" ") 76 | "###); 77 | assert_debug_snapshot!(parse(r#"\displaystyle {\mathrm {text}} "#), @r###" 78 | root 79 | |cmd 80 | ||cmd-name("\\displaystyle") 81 | ||args 82 | |||space'(" ") 83 | |||curly 84 | ||||lbrace'("{") 85 | ||||cmd 86 | |||||cmd-name("\\mathrm") 87 | |||||args 88 | ||||||curly 89 | |||||||lbrace'("{") 90 | |||||||text(word'("text")) 91 | |||||||rbrace'("}") 92 | ||||rbrace'("}") 93 | |||space'(" ") 94 | "###); 95 | } 96 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(missing_docs)] 2 | pub mod parser { 3 | use mitex_parser::syntax::SyntaxNode; 4 | use mitex_spec_gen::DEFAULT_SPEC; 5 | 6 | use super::SnapNode; 7 | 8 | pub fn parse(input: &str) -> SyntaxNode { 9 | mitex_parser::parse(input, DEFAULT_SPEC.clone()) 10 | } 11 | 12 | pub fn parse_snap(input: &str) -> SnapNode { 13 | super::ast_snapshot::SnapNode(parse(input)) 14 | } 15 | } 16 | 17 | pub use ast_snapshot::{SnapNode, SnapToken}; 18 | pub use parser::*; 19 | 20 | #[allow(missing_docs)] 21 | pub mod ast_snapshot { 22 | use core::fmt; 23 | use std::fmt::Write; 24 | 25 | use mitex_parser::syntax::{SyntaxKind, SyntaxNode, SyntaxToken}; 26 | use rowan::NodeOrToken; 27 | 28 | pub struct SnapNode(pub SyntaxNode); 29 | 30 | impl fmt::Debug for SnapNode { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | let mut p = AstPrinter { level: 0 }; 33 | p.show_node(self.0.clone(), f) 34 | } 35 | } 36 | 37 | pub struct SnapToken(pub SyntaxToken); 38 | 39 | impl fmt::Debug for SnapToken { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | let mut p = AstPrinter { level: 0 }; 42 | p.show_token(self.0.clone(), f) 43 | } 44 | } 45 | 46 | impl fmt::Display for SnapToken { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | let mut p = AstPrinter { level: 0 }; 49 | p.show_token(self.0.clone(), f) 50 | } 51 | } 52 | 53 | struct AstPrinter { 54 | level: usize, 55 | } 56 | 57 | impl AstPrinter { 58 | fn pretty_syntax_kind(f: &mut fmt::Formatter<'_>, sk: SyntaxKind) -> fmt::Result { 59 | let w = match sk { 60 | SyntaxKind::TokenError => "error'", 61 | SyntaxKind::TokenLineBreak => "br'", 62 | SyntaxKind::TokenWhiteSpace => "space'", 63 | SyntaxKind::TokenComment => "comment'", 64 | SyntaxKind::TokenLBrace => "lbrace'", 65 | SyntaxKind::TokenRBrace => "rbrace'", 66 | SyntaxKind::TokenLBracket => "lbracket'", 67 | SyntaxKind::TokenRBracket => "rbracket'", 68 | SyntaxKind::TokenLParen => "lparen'", 69 | SyntaxKind::TokenRParen => "rparen'", 70 | SyntaxKind::TokenComma => "comma'", 71 | SyntaxKind::TokenTilde => "tilde'", 72 | SyntaxKind::TokenSlash => "slash'", 73 | SyntaxKind::TokenWord => "word'", 74 | SyntaxKind::TokenDollar => "dollar'", 75 | SyntaxKind::TokenBeginMath => "begin-math'", 76 | SyntaxKind::TokenEndMath => "end-math", 77 | SyntaxKind::TokenAmpersand => "ampersand'", 78 | SyntaxKind::TokenHash => "hash'", 79 | SyntaxKind::TokenAsterisk => "asterisk'", 80 | SyntaxKind::TokenAtSign => "at-sign'", 81 | SyntaxKind::TokenUnderscore => "underscore'", 82 | SyntaxKind::TokenCaret => "caret'", 83 | SyntaxKind::TokenApostrophe => "apostrophe'", 84 | SyntaxKind::TokenDitto => "ditto'", 85 | SyntaxKind::TokenSemicolon => "semicolon'", 86 | SyntaxKind::TokenCommandSym => "sym'", 87 | SyntaxKind::ClauseCommandName => "cmd-name", 88 | SyntaxKind::ClauseArgument => "args", 89 | SyntaxKind::ClauseLR => "clause-lr", 90 | SyntaxKind::ItemNewLine => "newline", 91 | SyntaxKind::ItemText => "text", 92 | SyntaxKind::ItemCurly => "curly", 93 | SyntaxKind::ItemBracket => "bracket", 94 | SyntaxKind::ItemParen => "paren", 95 | SyntaxKind::ItemCmd => "cmd", 96 | SyntaxKind::ItemEnv => "env", 97 | SyntaxKind::ItemLR => "lr", 98 | SyntaxKind::ItemBegin => "begin", 99 | SyntaxKind::ItemEnd => "end", 100 | SyntaxKind::ItemBlockComment => "block-comment", 101 | SyntaxKind::ItemTypstCode => "embedded-code", 102 | SyntaxKind::ItemAttachComponent => "attach-comp", 103 | SyntaxKind::ItemFormula => "formula", 104 | SyntaxKind::ScopeRoot => "root", 105 | }; 106 | 107 | f.write_str(w) 108 | } 109 | 110 | fn show_node(&mut self, node: SyntaxNode, f: &mut fmt::Formatter<'_>) -> fmt::Result { 111 | Self::pretty_syntax_kind(f, node.kind())?; 112 | 113 | if f.alternate() { 114 | if node.children().count() == 0 { 115 | let mut first = true; 116 | f.write_char('(')?; 117 | for tok in node.children_with_tokens() { 118 | if first { 119 | first = false; 120 | } else { 121 | f.write_char(',')? 122 | } 123 | match tok { 124 | NodeOrToken::Node(_) => unreachable!(), 125 | NodeOrToken::Token(token) => self.show_token(token, f)?, 126 | } 127 | } 128 | 129 | f.write_char(')')?; 130 | f.write_char('\n')?; 131 | return Ok(()); 132 | } 133 | 134 | f.write_char('\n')?; 135 | // Print with children 136 | self.level += 1; 137 | for element in node.children_with_tokens() { 138 | for _ in 0..self.level { 139 | f.write_char('|')?; 140 | } 141 | match element { 142 | NodeOrToken::Node(sub) => self.show_node(sub, f)?, 143 | NodeOrToken::Token(token) => { 144 | self.show_token(token, f)?; 145 | f.write_char('\n')? 146 | } 147 | } 148 | } 149 | self.level -= 1; 150 | } 151 | Ok(()) 152 | } 153 | 154 | fn show_token(&mut self, token: SyntaxToken, f: &mut fmt::Formatter<'_>) -> fmt::Result { 155 | Self::pretty_syntax_kind(f, token.kind())?; 156 | 157 | if token.text().len() < 25 { 158 | return write!(f, "({:?})", token.text()); 159 | } 160 | let text = token.text(); 161 | for idx in 21..25 { 162 | if text.is_char_boundary(idx) { 163 | return write!(f, "({:?}..)", &text[..idx]); 164 | } 165 | } 166 | unreachable!() 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /crates/mitex-parser/tests/properties.rs: -------------------------------------------------------------------------------- 1 | //! TODO: crate doc 2 | #[allow(missing_docs)] 3 | pub mod common; 4 | 5 | mod properties { 6 | use crate::common::*; 7 | use insta::{assert_debug_snapshot, assert_snapshot}; 8 | use mitex_parser::syntax::{CmdItem, EnvItem, FormulaItem, LRItem, SyntaxKind, SyntaxToken}; 9 | use rowan::ast::AstNode; 10 | 11 | #[test] 12 | fn test_env_name() { 13 | fn env_name(input: &str) -> Option { 14 | parse(input) 15 | .descendants() 16 | .find(|node| matches!(node.kind(), SyntaxKind::ItemEnv)) 17 | .and_then(EnvItem::cast) 18 | .and_then(|e| e.name_tok()) 19 | .map(SnapToken) 20 | } 21 | 22 | assert_debug_snapshot!(env_name(r#"\begin{equation}\end{equation}"#).unwrap(), @r###"sym'("equation")"###); 23 | } 24 | 25 | #[test] 26 | fn test_formula_display() { 27 | fn formula_item(input: &str) -> Option { 28 | parse(input) 29 | .descendants() 30 | .find(|node| matches!(node.kind(), SyntaxKind::ItemFormula)) 31 | .and_then(FormulaItem::cast) 32 | } 33 | 34 | assert!(formula_item(r#"$$a$$"#).unwrap().is_display()); 35 | assert!(!formula_item(r#"$$a$$"#).unwrap().is_inline()); 36 | assert!(!formula_item(r#"$a$"#).unwrap().is_display()); 37 | assert!(formula_item(r#"$a$"#).unwrap().is_inline()); 38 | } 39 | 40 | #[test] 41 | fn test_cmd_arguments() { 42 | fn cmd_args(input: &str) -> String { 43 | parse(input) 44 | .descendants() 45 | .filter(|node| matches!(node.kind(), SyntaxKind::ItemCmd)) 46 | .filter_map(CmdItem::cast) 47 | .map(|e| { 48 | let args = e 49 | .arguments() 50 | .map(SnapNode) 51 | .map(|e| format!("{e:#?}").trim().to_string()) 52 | .collect::>() 53 | .join("\n---\n"); 54 | 55 | format!( 56 | "name: {:#?}\n{}", 57 | e.name_tok().map(SnapToken).unwrap(), 58 | args 59 | ) 60 | }) 61 | .collect::>() 62 | .join("\n----\n") 63 | } 64 | 65 | assert_snapshot!(cmd_args(r#"\frac ab"#), @r###" 66 | name: cmd-name("\\frac") 67 | args(word'("a")) 68 | --- 69 | args(word'("b")) 70 | "###); 71 | assert_snapshot!(cmd_args(r#"\displaystyle abcdefg"#), @r###" 72 | name: cmd-name("\\displaystyle") 73 | args 74 | |space'(" ") 75 | |text(word'("abcdefg")) 76 | "###); 77 | assert_snapshot!(cmd_args(r#"\sum\limits"#), @r###" 78 | name: cmd-name("\\limits") 79 | args 80 | |cmd(cmd-name("\\sum")) 81 | ---- 82 | name: cmd-name("\\sum") 83 | "###); 84 | } 85 | 86 | #[test] 87 | fn test_lr_symbol() { 88 | fn lr_info(input: &str) -> Option { 89 | let e = parse(input) 90 | .descendants() 91 | .find(|node| matches!(node.kind(), SyntaxKind::ItemLR)) 92 | .and_then(LRItem::cast)?; 93 | 94 | fn pretty_sym(s: Option) -> String { 95 | s.map(SnapToken) 96 | .map(|t| t.to_string()) 97 | .unwrap_or_else(|| "None".to_string()) 98 | } 99 | 100 | Some( 101 | [ 102 | format!("{:?}", e.left().map(|l| l.is_left())), 103 | pretty_sym(e.left_sym()), 104 | format!("{:?}", e.right().map(|l| l.is_left())), 105 | pretty_sym(e.right_sym()), 106 | ] 107 | .join(", "), 108 | ) 109 | } 110 | 111 | assert_snapshot!(lr_info(r#"\left.\right."#).unwrap(), @r###"Some(true), word'("."), Some(false), word'(".")"###); 112 | assert_snapshot!(lr_info(r#"\left(\right)"#).unwrap(), @r###"Some(true), lparen'("("), Some(false), rparen'(")")"###); 113 | 114 | assert_snapshot!(lr_info(r#"\left"#).unwrap(), @"Some(true), None, Some(true), None"); 115 | // Note: this is an invalid expression, and produce an expected error 116 | assert_snapshot!(lr_info(r#"\left."#).unwrap(), @r###"Some(true), word'("."), Some(true), word'(".")"###); 117 | 118 | assert_snapshot!(lr_info(r#"\left\right"#).unwrap(), @"Some(true), None, Some(false), None"); 119 | assert_snapshot!(lr_info(r#"\left . a\right ."#).unwrap(), @r###"Some(true), word'("."), Some(false), word'(".")"###); 120 | assert_snapshot!(lr_info(r#"\left\langle a\right\|"#).unwrap(), @r###"Some(true), sym'("\\langle"), Some(false), sym'("\\|")"###); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /crates/mitex-spec-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex-spec-gen" 3 | description = "Guard to geneate specification files for dependent crates" 4 | authors.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | mitex-spec.workspace = true 13 | 14 | once_cell.workspace = true 15 | 16 | [build-dependencies] 17 | mitex-spec.workspace = true 18 | serde = { workspace = true, features = ["derive"] } 19 | serde_json.workspace = true 20 | anyhow.workspace = true 21 | which.workspace = true 22 | 23 | [lints] 24 | workspace = true 25 | 26 | [features] 27 | # use prebuilt spec data 28 | prebuilt = [] 29 | # generate spec data 30 | generate = [] 31 | -------------------------------------------------------------------------------- /crates/mitex-spec-gen/build.rs: -------------------------------------------------------------------------------- 1 | //! Common build script for crates depending on the compacted default 2 | //! specification. 3 | 4 | use std::path::{Path, PathBuf}; 5 | 6 | use anyhow::Context; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | fn main() { 10 | let project_root = get_project_root(); 11 | 12 | let spec_builder = if cfg!(feature = "prebuilt") { 13 | copy_prebuilt 14 | } else if cfg!(feature = "generate") || (which::which("typst").is_ok() && project_root.is_ok()) 15 | { 16 | generate 17 | } else { 18 | // fallback to prebuilt spec 19 | copy_prebuilt 20 | }; 21 | 22 | spec_builder() 23 | .with_context(|| "failed to build spec") 24 | .unwrap(); 25 | } 26 | 27 | fn get_project_root() -> anyhow::Result { 28 | let project_root = 29 | std::env::var("CARGO_MANIFEST_DIR").with_context(|| "failed to get manifest dir")?; 30 | let mut project_root = std::path::Path::new(&project_root); 31 | Ok(loop { 32 | let parent = project_root 33 | .parent() 34 | .with_context(|| "failed to get project root")?; 35 | if parent.join("Cargo.toml").exists() { 36 | break parent.to_owned(); 37 | } 38 | project_root = parent; 39 | }) 40 | } 41 | 42 | fn copy_prebuilt() -> anyhow::Result<()> { 43 | // println!("cargo:warning=copy_prebuilt"); 44 | let manifest_dir = 45 | std::env::var("CARGO_MANIFEST_DIR").with_context(|| "failed to get manifest dir")?; 46 | let manifest_dir = std::path::Path::new(&manifest_dir); 47 | let target_spec = 48 | Path::new(&std::env::var("OUT_DIR").unwrap()).join("mitex-artifacts/spec/default.rkyv"); 49 | 50 | // assets/artifacts/spec/default.rkyv 51 | std::fs::create_dir_all( 52 | target_spec 53 | .parent() 54 | .context("failed to get dirname of target spec")?, 55 | ) 56 | .with_context(|| "failed to create target_dir for store spec")?; 57 | 58 | let prebuilt_spec = manifest_dir.join(Path::new("assets/artifacts/spec/default.rkyv")); 59 | println!("cargo:warning=Use prebuilt spec binaries at {prebuilt_spec:?}"); 60 | 61 | std::fs::copy(prebuilt_spec, target_spec).with_context(|| { 62 | "failed to copy prebuilt spec, \ 63 | do you forget to run `git submodule update --init`?" 64 | })?; 65 | 66 | Ok(()) 67 | } 68 | 69 | fn generate() -> anyhow::Result<()> { 70 | // println!("cargo:warning=generate"); 71 | 72 | // typst query --root . ./packages/latex-spec/mod.typ "" 73 | let project_root = get_project_root()?; 74 | 75 | let spec_root = project_root.join("packages/mitex/specs/"); 76 | println!( 77 | "cargo:rerun-if-changed={spec_root}", 78 | spec_root = spec_root.display() 79 | ); 80 | 81 | let target_dir = Path::new(&std::env::var("OUT_DIR").unwrap()).join("mitex-artifacts"); 82 | 83 | let mut package_specs = std::process::Command::new("typst"); 84 | let package_specs = package_specs.args([ 85 | "query", 86 | "--root", 87 | project_root.to_str().unwrap(), 88 | project_root 89 | .join("packages/mitex/specs/mod.typ") 90 | .to_str() 91 | .unwrap(), 92 | "", 93 | ]); 94 | 95 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 96 | struct QueryItem { 97 | pub value: T, 98 | } 99 | 100 | type Json = Vec>; 101 | 102 | let mut json_spec: mitex_spec::JsonCommandSpec = Default::default(); 103 | let json_packages: Json = serde_json::from_slice( 104 | &package_specs 105 | .output() 106 | .with_context(|| "failed to query metadata")? 107 | .stdout, 108 | ) 109 | .context(format!( 110 | "failed to parse package specs cmd: {package_specs:?}" 111 | ))?; 112 | if json_packages.is_empty() { 113 | panic!("no package found"); 114 | } 115 | if json_packages.len() > 1 { 116 | panic!("multiple packages found"); 117 | } 118 | 119 | std::fs::create_dir_all(target_dir.join("spec")) 120 | .with_context(|| "failed to create target_dir for store spec")?; 121 | 122 | let json_packages = json_packages.into_iter().next().unwrap().value; 123 | std::fs::write( 124 | target_dir.join("spec/packages.json"), 125 | serde_json::to_string_pretty(&json_packages) 126 | .with_context(|| "failed to serialize json packages")?, 127 | ) 128 | .with_context(|| "failed to write json packages")?; 129 | 130 | for package in json_packages.0 { 131 | for (name, item) in package.spec.commands { 132 | json_spec.commands.insert(name, item); 133 | } 134 | } 135 | std::fs::write( 136 | target_dir.join("spec/default.json"), 137 | serde_json::to_string_pretty(&json_spec) 138 | .with_context(|| "failed to serialize json spec")?, 139 | ) 140 | .with_context(|| "failed to write json spec")?; 141 | 142 | let spec: mitex_spec::CommandSpec = json_spec.into(); 143 | 144 | std::fs::write(target_dir.join("spec/default.rkyv"), spec.to_bytes()) 145 | .with_context(|| "failed to write compacted spec")?; 146 | 147 | Ok(()) 148 | } 149 | -------------------------------------------------------------------------------- /crates/mitex-spec-gen/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Provides embedded command specifications for MiTeX. 2 | 3 | use mitex_spec::CommandSpec; 4 | 5 | /// The default command specification. 6 | /// 7 | /// See [Reproducing Default Command Specification][repro-default] for more 8 | /// information. 9 | /// 10 | /// [repro-default]: https://github.com/mitex-rs/artifacts/blob/main/README.md#default-command-specification-since-v011 11 | pub static DEFAULT_SPEC: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { 12 | CommandSpec::from_bytes(include_bytes!(concat!( 13 | env!("OUT_DIR"), 14 | "/mitex-artifacts/spec/default.rkyv" 15 | ))) 16 | }); 17 | -------------------------------------------------------------------------------- /crates/mitex-spec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex-spec" 3 | description = "Specification Library for MiTeX" 4 | authors.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | [[bench]] 12 | name = "spec_constructions" 13 | harness = false 14 | 15 | [dependencies] 16 | rkyv = { workspace = true, optional = true } 17 | serde = { workspace = true, features = ["derive"], optional = true } 18 | serde_json.workspace = true 19 | rustc-hash.workspace = true 20 | 21 | [features] 22 | 23 | rkyv = ["dep:rkyv", "rkyv/alloc", "rkyv/archive_le"] 24 | rkyv-validation = ["dep:rkyv", "rkyv/validation"] 25 | default = ["serde", "rkyv", "rkyv-validation"] 26 | 27 | [dev-dependencies] 28 | once_cell.workspace = true 29 | divan.workspace = true 30 | 31 | [lints] 32 | workspace = true 33 | -------------------------------------------------------------------------------- /crates/mitex-spec/src/preludes.rs: -------------------------------------------------------------------------------- 1 | // todo: remove me 2 | #![allow(missing_docs)] 3 | 4 | pub mod command { 5 | use crate::{ArgShape, CommandSpecItem, ContextFeature}; 6 | 7 | pub fn define_command(len: u8) -> CommandSpecItem { 8 | CommandSpecItem::Cmd(crate::CmdShape { 9 | args: crate::ArgShape::Right { 10 | pattern: crate::ArgPattern::FixedLenTerm { len }, 11 | }, 12 | alias: None, 13 | }) 14 | } 15 | 16 | pub fn define_glob_command(reg: &str, alias: &str) -> CommandSpecItem { 17 | CommandSpecItem::Cmd(crate::CmdShape { 18 | args: crate::ArgShape::Right { 19 | pattern: crate::ArgPattern::Glob { 20 | pattern: reg.into(), 21 | }, 22 | }, 23 | alias: Some(alias.to_owned()), 24 | }) 25 | } 26 | 27 | pub fn define_glob_env(reg: &str, alias: &str, ctx_feature: ContextFeature) -> CommandSpecItem { 28 | CommandSpecItem::Env(crate::EnvShape { 29 | args: crate::ArgPattern::Glob { 30 | pattern: reg.into(), 31 | }, 32 | ctx_feature, 33 | alias: Some(alias.to_owned()), 34 | }) 35 | } 36 | 37 | pub fn define_symbol(alias: &str) -> CommandSpecItem { 38 | CommandSpecItem::Cmd(crate::CmdShape { 39 | args: crate::ArgShape::Right { 40 | pattern: crate::ArgPattern::None, 41 | }, 42 | alias: Some(alias.to_owned()), 43 | }) 44 | } 45 | 46 | pub fn define_command_with_alias(len: u8, alias: &str) -> CommandSpecItem { 47 | CommandSpecItem::Cmd(crate::CmdShape { 48 | args: crate::ArgShape::Right { 49 | pattern: crate::ArgPattern::FixedLenTerm { len }, 50 | }, 51 | alias: Some(alias.to_owned()), 52 | }) 53 | } 54 | 55 | pub fn define_greedy_command(alias: &str) -> CommandSpecItem { 56 | CommandSpecItem::Cmd(crate::CmdShape { 57 | args: crate::ArgShape::Right { 58 | pattern: crate::ArgPattern::Greedy, 59 | }, 60 | alias: Some(alias.to_owned()), 61 | }) 62 | } 63 | 64 | pub fn define_matrix_env(num: Option, alias: &str) -> CommandSpecItem { 65 | CommandSpecItem::Env(crate::EnvShape { 66 | args: num 67 | .map(|len| crate::ArgPattern::FixedLenTerm { len }) 68 | .unwrap_or(crate::ArgPattern::None), 69 | ctx_feature: crate::ContextFeature::IsMatrix, 70 | alias: Some(alias.to_owned()), 71 | }) 72 | } 73 | 74 | pub fn define_normal_env(num: Option, alias: &str) -> CommandSpecItem { 75 | CommandSpecItem::Env(crate::EnvShape { 76 | args: num 77 | .map(|len| crate::ArgPattern::FixedLenTerm { len }) 78 | .unwrap_or(crate::ArgPattern::None), 79 | ctx_feature: crate::ContextFeature::None, 80 | alias: Some(alias.to_owned()), 81 | }) 82 | } 83 | pub const fn define_const_command(args: ArgShape) -> CommandSpecItem { 84 | CommandSpecItem::Cmd(crate::CmdShape { args, alias: None }) 85 | } 86 | 87 | pub const TEX_CMD0: CommandSpecItem = define_const_command(crate::ArgShape::Right { 88 | pattern: crate::ArgPattern::FixedLenTerm { len: 0 }, 89 | }); 90 | pub const TEX_CMD1: CommandSpecItem = define_const_command(crate::ArgShape::Right { 91 | pattern: crate::ArgPattern::FixedLenTerm { len: 1 }, 92 | }); 93 | pub const TEX_CMD2: CommandSpecItem = define_const_command(crate::ArgShape::Right { 94 | pattern: crate::ArgPattern::FixedLenTerm { len: 2 }, 95 | }); 96 | pub const TEX_SYMBOL: CommandSpecItem = define_const_command(crate::ArgShape::Right { 97 | pattern: crate::ArgPattern::None, 98 | }); 99 | pub const TEX_LEFT1_OPEARTOR: CommandSpecItem = define_const_command(crate::ArgShape::Left1); 100 | pub const TEX_GREEDY_OPERATOR: CommandSpecItem = define_const_command(crate::ArgShape::Right { 101 | pattern: crate::ArgPattern::Greedy, 102 | }); 103 | pub const TEX_INFIX_OPERATOR: CommandSpecItem = 104 | define_const_command(crate::ArgShape::InfixGreedy); 105 | pub const TEX_MATRIX_ENV: CommandSpecItem = CommandSpecItem::Env(crate::EnvShape { 106 | args: crate::ArgPattern::None, 107 | ctx_feature: crate::ContextFeature::IsMatrix, 108 | alias: None, 109 | }); 110 | pub const TEX_NORMAL_ENV: CommandSpecItem = CommandSpecItem::Env(crate::EnvShape { 111 | args: crate::ArgPattern::None, 112 | ctx_feature: crate::ContextFeature::None, 113 | alias: None, 114 | }); 115 | 116 | #[derive(Default)] 117 | pub struct SpecBuilder { 118 | commands: rustc_hash::FxHashMap, 119 | } 120 | 121 | impl SpecBuilder { 122 | pub fn add_command(&mut self, name: &str, item: CommandSpecItem) -> &mut Self { 123 | self.commands.insert(name.to_owned(), item); 124 | self 125 | } 126 | 127 | pub fn build(self) -> crate::CommandSpec { 128 | crate::CommandSpec::new(self.commands) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /crates/mitex-spec/src/query.rs: -------------------------------------------------------------------------------- 1 | //! The query module contains the data structures that are used by `typst query 2 | //! ` 3 | 4 | use std::{collections::HashMap, sync::Arc}; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{CmdShape, ContextFeature, EnvShape}; 9 | 10 | /// A package specification. 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | pub struct PackageSpec { 13 | /// The name of the package. 14 | pub name: String, 15 | /// The command specification of the package. 16 | pub spec: CommandSpecRepr, 17 | } 18 | 19 | /// A ordered list of package specifications. 20 | /// 21 | /// The latter package specification will override the former one. 22 | #[derive(Debug, Clone, Serialize, Deserialize)] 23 | pub struct PackagesVec(pub Vec); 24 | 25 | /// An item of command specification. 26 | /// This enum contains more sugar than the canonical representation. 27 | /// 28 | /// See [`crate::CommandSpecItem`] for more details. 29 | #[derive(Debug, Clone, Serialize, Deserialize)] 30 | #[serde(tag = "kind")] 31 | pub enum CommandSpecItem { 32 | /// A canonical command item. 33 | #[serde(rename = "cmd")] 34 | Cmd(CmdShape), 35 | /// A canonical environment item. 36 | #[serde(rename = "env")] 37 | Env(EnvShape), 38 | /// A command that takes no argument, and its handler is also a typst 39 | /// symbol. 40 | #[serde(rename = "sym")] 41 | Symbol, 42 | /// A command that takes zero argument, and its handler is a typst function. 43 | #[serde(rename = "cmd0")] 44 | Command0, 45 | /// A command that takes one argument. 46 | #[serde(rename = "cmd1")] 47 | Command1, 48 | /// A command that takes two arguments. 49 | #[serde(rename = "cmd2")] 50 | Command2, 51 | /// A command that takes one argument and is a left-associative operator. 52 | #[serde(rename = "left1-cmd")] 53 | CmdLeft1, 54 | /// A command that takes no argument and is a matrix environment. 55 | #[serde(rename = "matrix-env")] 56 | EnvMatrix, 57 | /// A command that takes no argument and is a normal environment. 58 | #[serde(rename = "normal-env")] 59 | EnvNormal, 60 | /// A command that has a glob argument pattern and is an environment. 61 | #[serde(rename = "glob-env")] 62 | EnvGlob { 63 | /// The glob pattern of the command. 64 | pattern: String, 65 | /// The aliasing typst handle of the command. 66 | alias: String, 67 | /// The context feature of the command. 68 | ctx_feature: ContextFeature, 69 | }, 70 | 71 | /// A command that is aliased to a Typst symbol. 72 | #[serde(rename = "alias-sym")] 73 | SymAlias { 74 | /// The aliasing typst handle of the symbol. 75 | alias: String, 76 | }, 77 | /// A command that is greedy and is aliased to a Typst handler. 78 | #[serde(rename = "greedy-cmd")] 79 | CmdGreedy { 80 | /// The aliasing typst handle of the command. 81 | alias: String, 82 | }, 83 | /// A command that is an infix operator and is aliased to a Typst handler. 84 | #[serde(rename = "infix-cmd")] 85 | CmdInfix { 86 | /// The aliasing typst handle of the command. 87 | alias: String, 88 | }, 89 | /// A command that has a glob argument pattern and is aliased to a Typst 90 | /// handler. 91 | #[serde(rename = "glob-cmd")] 92 | CmdGlob { 93 | /// The glob pattern of the command. 94 | pattern: String, 95 | /// The aliasing typst handle of the command. 96 | alias: String, 97 | }, 98 | } 99 | 100 | impl From for crate::CommandSpecItem { 101 | fn from(item: CommandSpecItem) -> Self { 102 | use crate::preludes::command::*; 103 | match item { 104 | CommandSpecItem::Cmd(shape) => Self::Cmd(shape), 105 | CommandSpecItem::Env(shape) => Self::Env(shape), 106 | CommandSpecItem::Symbol => TEX_SYMBOL, 107 | CommandSpecItem::Command0 => TEX_CMD0, 108 | CommandSpecItem::Command1 => TEX_CMD1, 109 | CommandSpecItem::Command2 => TEX_CMD2, 110 | CommandSpecItem::CmdLeft1 => TEX_LEFT1_OPEARTOR, 111 | CommandSpecItem::EnvMatrix => TEX_MATRIX_ENV, 112 | CommandSpecItem::EnvNormal => TEX_NORMAL_ENV, 113 | CommandSpecItem::EnvGlob { 114 | pattern, 115 | alias, 116 | ctx_feature, 117 | } => define_glob_env(&pattern, &alias, ctx_feature), 118 | CommandSpecItem::SymAlias { alias } => define_symbol(&alias), 119 | CommandSpecItem::CmdGreedy { alias } => define_greedy_command(&alias), 120 | CommandSpecItem::CmdInfix { alias } => crate::CommandSpecItem::Cmd(crate::CmdShape { 121 | args: crate::ArgShape::InfixGreedy, 122 | alias: Some(alias.to_owned()), 123 | }), 124 | CommandSpecItem::CmdGlob { pattern, alias } => define_glob_command(&pattern, &alias), 125 | } 126 | } 127 | } 128 | 129 | /// Command specification that contains a set of commands and environments. It 130 | /// is used for us to define the meta data of LaTeX packages in typst code and 131 | /// query by `typst query` then. See [`Spec`] for an example. 132 | /// 133 | /// Note: There are non-canonical format of items could be used for convenience. 134 | /// 135 | /// [`Spec`]: https://github.com/mitex-rs/mitex/tree/main/packages/mitex/specs 136 | #[derive(Default, Debug, Clone, Serialize, Deserialize)] 137 | pub struct CommandSpecRepr { 138 | /// The command specifications. 139 | pub commands: HashMap, 140 | } 141 | 142 | impl From for crate::CommandSpec { 143 | fn from(repr: CommandSpecRepr) -> Self { 144 | Self(Arc::new(repr.into())) 145 | } 146 | } 147 | 148 | impl From for crate::CommandSpecRepr { 149 | fn from(repr: CommandSpecRepr) -> Self { 150 | Self { 151 | commands: repr 152 | .commands 153 | .into_iter() 154 | .map(|(k, v)| (k, v.into())) 155 | .collect(), 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /crates/mitex-spec/src/stream.rs: -------------------------------------------------------------------------------- 1 | use super::{ArchivedCommandSpecRepr, CommandSpecRepr}; 2 | use rkyv::de::deserializers::SharedDeserializeMap; 3 | use rkyv::{AlignedVec, Deserialize}; 4 | 5 | enum RkyvStreamData<'a> { 6 | Aligned(&'a [u8]), 7 | Unaligned(AlignedVec), 8 | } 9 | 10 | impl AsRef<[u8]> for RkyvStreamData<'_> { 11 | #[inline] 12 | fn as_ref(&self) -> &[u8] { 13 | match self { 14 | Self::Aligned(v) => v, 15 | Self::Unaligned(v) => v.as_slice(), 16 | } 17 | } 18 | } 19 | 20 | pub struct BytesModuleStream<'a> { 21 | data: RkyvStreamData<'a>, 22 | } 23 | 24 | impl<'a> BytesModuleStream<'a> { 25 | pub fn from_slice(v: &'a [u8]) -> Self { 26 | let v = if (v.as_ptr() as usize) % AlignedVec::ALIGNMENT != 0 { 27 | let mut aligned = AlignedVec::with_capacity(v.len()); 28 | aligned.extend_from_slice(v); 29 | RkyvStreamData::Unaligned(aligned) 30 | } else { 31 | RkyvStreamData::Aligned(v) 32 | }; 33 | 34 | Self { data: v } 35 | } 36 | 37 | #[cfg(feature = "rkyv-validation")] 38 | pub fn checkout(&self) -> &ArchivedCommandSpecRepr { 39 | rkyv::check_archived_root::(self.data.as_ref()).unwrap() 40 | } 41 | 42 | /// # Safety 43 | /// The data source must be trusted and valid. 44 | pub unsafe fn checkout_unchecked(&self) -> &ArchivedCommandSpecRepr { 45 | rkyv::archived_root::(self.data.as_ref()) 46 | } 47 | 48 | #[cfg(feature = "rkyv-validation")] 49 | pub fn checkout_owned(&self) -> CommandSpecRepr { 50 | let v = self.checkout(); 51 | let mut dmap = SharedDeserializeMap::default(); 52 | v.deserialize(&mut dmap).unwrap() 53 | } 54 | 55 | /// # Safety 56 | /// The data source must be trusted and valid. 57 | pub unsafe fn checkout_owned_unchecked(&self) -> CommandSpecRepr { 58 | let v = self.checkout_unchecked(); 59 | let mut dmap = SharedDeserializeMap::default(); 60 | v.deserialize(&mut dmap).unwrap() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/mitex-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex-wasm" 3 | description = "Wasm module which uses mitex, running in browsers and Typst" 4 | authors.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | [lib] 12 | crate-type = ["cdylib"] 13 | 14 | [dependencies] 15 | 16 | mitex.workspace = true 17 | mitex-spec.workspace = true 18 | 19 | serde.workspace = true 20 | serde_json.workspace = true 21 | 22 | wasm-bindgen = { version = "0.2.92", optional = true } 23 | wasm-minimal-protocol = { git = "https://github.com/astrale-sharp/wasm-minimal-protocol", optional = true } 24 | 25 | [features] 26 | rkyv = ["mitex-spec/rkyv", "mitex-spec/rkyv-validation"] 27 | web = ["wasm-bindgen"] 28 | typst-plugin = ["wasm-minimal-protocol"] 29 | spec-api = [] 30 | 31 | default = ["rkyv"] 32 | 33 | [lints] 34 | workspace = true 35 | -------------------------------------------------------------------------------- /crates/mitex-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This is a WASM wrapper of the MiTeX library for Typst. 2 | //! 3 | //! # Usage 4 | //! 5 | //! For example, you can call [`convert_math`] function in Typst by loading the 6 | //! plugin: 7 | //! 8 | //! ```typ 9 | //! #let mitex-wasm = plugin("./mitex.wasm") 10 | //! 11 | //! #let mitex-convert(it: "", spec: bytes(())) = { 12 | //! str(mitex-wasm.convert_math(bytes(it), spec)) 13 | //! } 14 | //! ``` 15 | 16 | // todo: maybe a bug of wasm_minimal_protocol. 17 | // #[cfg_attr(target_arch = "wasm32", wasm_func)] 18 | // | ^^^^^^^^^ 19 | #![allow(missing_docs)] 20 | 21 | mod impls { 22 | #[cfg(feature = "web")] 23 | pub use wasm_bindgen::prelude::*; 24 | 25 | /// Converts a json command specification into a binary command 26 | /// specification 27 | /// 28 | /// # Errors 29 | /// Returns an error if the input is not a valid json string 30 | #[cfg(feature = "spec-api")] 31 | #[cfg_attr(feature = "web", wasm_bindgen)] 32 | pub fn compile_spec(input: &[u8]) -> Result, String> { 33 | let res: mitex_spec::JsonCommandSpec = 34 | serde_json::from_slice(input).map_err(|e| e.to_string())?; 35 | let res: mitex_spec::CommandSpec = res.into(); 36 | Result::Ok(res.to_bytes()) 37 | } 38 | 39 | /// Extracts the command specification from its binary (rkyv) 40 | /// representation. 41 | fn extract_spec(spec: &[u8]) -> Option { 42 | (!spec.is_empty()).then(|| mitex_spec::CommandSpec::from_bytes(spec)) 43 | } 44 | 45 | /// Converts a LaTeX math equation into a plain text. You can pass an binary 46 | /// (rkyv) command specification by `spec` at the same time to customize 47 | /// parsing. 48 | #[cfg_attr(feature = "web", wasm_bindgen)] 49 | pub fn convert_math(input: &str, spec: &[u8]) -> Result { 50 | mitex::convert_math(input, extract_spec(spec)) 51 | } 52 | 53 | /// Converts a LaTeX code into a plain text. You can pass an binary (rkyv) 54 | /// command specification by `spec` at the same time to customize parsing. 55 | #[cfg_attr(feature = "web", wasm_bindgen)] 56 | pub fn convert_text(input: &str, spec: &[u8]) -> Result { 57 | mitex::convert_text(input, extract_spec(spec)) 58 | } 59 | } 60 | 61 | /// Wrappers for Typst as the host 62 | #[cfg(feature = "typst-plugin")] 63 | mod wasm_host { 64 | pub use wasm_minimal_protocol::*; 65 | initiate_protocol!(); 66 | 67 | fn wasm_into_str(input: &[u8]) -> Result<&str, String> { 68 | std::str::from_utf8(input).map_err(|e| e.to_string()) 69 | } 70 | 71 | #[cfg(feature = "spec-api")] 72 | #[cfg_attr(feature = "typst-plugin", wasm_func)] 73 | pub fn compile_spec(input: &[u8]) -> Result, String> { 74 | super::impls::compile_spec(input) 75 | } 76 | 77 | /// See [`super::impls::convert_math`] 78 | /// 79 | /// # Errors 80 | /// Returns an error if the input is not a valid utf-8 string 81 | #[cfg_attr(feature = "typst-plugin", wasm_func)] 82 | pub fn convert_math(input: &[u8], spec: &[u8]) -> Result, String> { 83 | let input = wasm_into_str(input)?; 84 | let res = super::impls::convert_math(input, spec)?; 85 | Result::Ok(res.into_bytes()) 86 | } 87 | 88 | /// See [`super::impls::convert_text`] 89 | /// 90 | /// # Errors 91 | /// Returns an error if the input is not a valid utf-8 string 92 | #[cfg_attr(feature = "typst-plugin", wasm_func)] 93 | pub fn convert_text(input: &[u8], spec: &[u8]) -> Result, String> { 94 | let input = wasm_into_str(input)?; 95 | let res = super::impls::convert_text(input, spec)?; 96 | Result::Ok(res.into_bytes()) 97 | } 98 | } 99 | 100 | /// Wrappers for Browsers as the host 101 | #[cfg(not(feature = "typst-plugin"))] 102 | mod wasm_host { 103 | pub use super::impls::*; 104 | } 105 | 106 | pub use wasm_host::*; 107 | 108 | // test with b"abc" 109 | #[cfg(test)] 110 | #[cfg(feature = "typst-plugin")] 111 | mod tests { 112 | use super::*; 113 | 114 | #[test] 115 | fn test_convert_math() { 116 | assert_eq!(convert_math(b"$abc$", &[]).unwrap(), b"a b c "); 117 | } 118 | 119 | #[test] 120 | fn test_convert_text() { 121 | assert_eq!(convert_text(b"abc", &[]).unwrap(), b"abc"); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /crates/mitex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex" 3 | description = "MiTeX is a TeX2Typst converter." 4 | authors.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | [[bench]] 12 | name = "convert_large_projects" 13 | harness = false 14 | 15 | [dependencies] 16 | 17 | mitex-parser.workspace = true 18 | mitex-spec-gen.workspace = true 19 | rowan.workspace = true 20 | 21 | [dev-dependencies] 22 | insta.workspace = true 23 | divan.workspace = true 24 | serde.workspace = true 25 | serde_json.workspace = true 26 | 27 | # todo: add lints in when we resolves all the warnings 28 | # [lints] 29 | # workspace = true 30 | -------------------------------------------------------------------------------- /crates/mitex/benches/bench.ps1: -------------------------------------------------------------------------------- 1 | echo "/*" 2 | hyperfine.exe 'typst compile --root . crates\mitex\benches\empty.typ' --warmup 3 3 | hyperfine.exe 'typst compile --root . crates\mitex\benches\oiwiki.typ' --warmup 3 4 | hyperfine.exe 'typst compile --root . crates\mitex\benches\oiwiki-with-render.typ' --warmup 3 5 | cargo bench --manifest-path .\crates\mitex\Cargo.toml 6 | echo "*/" -------------------------------------------------------------------------------- /crates/mitex/benches/bencher.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "/packages/mitex/lib.typ": * 3 | 4 | #let integrate-conversion(it, data: (), convert-only: false) = { 5 | let passed = 0 6 | for d in data { 7 | 8 | passed += 1 9 | 10 | if convert-only { 11 | let _ = mitex-convert(d.text) 12 | } else /* render-math */ { 13 | if d.type == "inline" { 14 | mi(d.text) 15 | linebreak() 16 | } else { 17 | mitex(d.text) 18 | } 19 | } 20 | } 21 | 22 | it 23 | [#passed / #data.len() passed] 24 | } 25 | -------------------------------------------------------------------------------- /crates/mitex/benches/convert_large_projects.rs: -------------------------------------------------------------------------------- 1 | use divan::{AllocProfiler, Bencher}; 2 | 3 | #[global_allocator] 4 | static ALLOC: AllocProfiler = AllocProfiler::system(); 5 | 6 | fn main() { 7 | // Run registered benchmarks. 8 | divan::main(); 9 | } 10 | 11 | fn bench(bencher: Bencher, path: &str) { 12 | // typst query --root . .\packages\latex-spec\mod.typ "" 13 | let project_root = std::env::var("CARGO_MANIFEST_DIR").unwrap(); 14 | let project_root = std::path::Path::new(&project_root) 15 | .parent() 16 | .unwrap() 17 | .parent() 18 | .unwrap(); 19 | 20 | let Ok(v) = std::fs::read_to_string(project_root.join(path)) else { 21 | eprintln!("Cannot read file {}", path); 22 | return; 23 | }; 24 | 25 | #[derive(serde::Deserialize)] 26 | struct Fixture { 27 | text: String, 28 | } 29 | 30 | let data = serde_json::from_str::>(&v).unwrap(); 31 | 32 | let convert = if WITH_MACRO { 33 | mitex::convert_math 34 | } else { 35 | mitex::convert_math_no_macro 36 | }; 37 | 38 | // warm up 39 | convert("$ $", None).unwrap(); 40 | 41 | bencher.bench(|| { 42 | for fixture in &data { 43 | convert(&fixture.text, None).unwrap(); 44 | } 45 | }); 46 | } 47 | 48 | #[divan::bench] 49 | fn oiwiki_231222(bencher: Bencher) { 50 | bench::(bencher, "local/oiwiki-231222.json"); 51 | } 52 | 53 | #[divan::bench] 54 | fn oiwiki_231222_macro(bencher: Bencher) { 55 | bench::(bencher, "local/oiwiki-231222.json"); 56 | } 57 | 58 | /* 59 | 60 | last^1 (macro support, typst v0.10.0) 61 | Benchmark 1: typst compile --root . crates\mitex\benches\empty.typ 62 | Time (mean ± σ): 81.5 ms ± 2.8 ms [User: 7.8 ms, System: 7.7 ms] 63 | Range (min … max): 76.9 ms … 88.7 ms 34 runs 64 | 65 | Benchmark 1: typst compile --root . crates\mitex\benches\oiwiki.typ 66 | Time (mean ± σ): 1.472 s ± 0.074 s [User: 0.436 s, System: 0.027 s] 67 | Range (min … max): 1.416 s … 1.654 s 10 runs 68 | 69 | Benchmark 1: typst compile --root . crates\mitex\benches\oiwiki-with-render.typ 70 | Time (mean ± σ): 3.145 s ± 0.125 s [User: 0.937 s, System: 0.065 s] 71 | Range (min … max): 2.978 s … 3.394 s 10 runs 72 | 73 | convert_large_projects fastest │ slowest │ median │ mean │ samples │ iters 74 | ├─ oiwiki_231222 84.3 ms │ 115.5 ms │ 87.79 ms │ 88.91 ms │ 100 │ 100 75 | │ alloc: │ │ │ │ │ 76 | │ 1398801 │ 1398801 │ 1398801 │ 1398801 │ │ 77 | │ 85.32 MB │ 85.32 MB │ 85.32 MB │ 85.32 MB │ │ 78 | │ dealloc: │ │ │ │ │ 79 | │ 1398801 │ 1398801 │ 1398801 │ 1398801 │ │ 80 | │ 95.64 MB │ 95.64 MB │ 95.64 MB │ 95.64 MB │ │ 81 | │ grow: │ │ │ │ │ 82 | │ 71029 │ 71029 │ 71029 │ 71029 │ │ 83 | │ 10.31 MB │ 10.31 MB │ 10.31 MB │ 10.31 MB │ │ 84 | ╰─ oiwiki_231222_macro 81.19 ms │ 97.02 ms │ 85.2 ms │ 86.5 ms │ 100 │ 100 85 | alloc: │ │ │ │ │ 86 | 1398801 │ 1398801 │ 1398801 │ 1398801 │ │ 87 | 85.32 MB │ 85.32 MB │ 85.32 MB │ 85.32 MB │ │ 88 | dealloc: │ │ │ │ │ 89 | 1398801 │ 1398801 │ 1398801 │ 1398801 │ │ 90 | 95.64 MB │ 95.64 MB │ 95.64 MB │ 95.64 MB │ │ 91 | grow: │ │ │ │ │ 92 | 71029 │ 71029 │ 71029 │ 71029 │ │ 93 | 10.31 MB │ 10.31 MB │ 10.31 MB │ 10.31 MB │ │ 94 | 95 | baseline (typst v0.10.0) 96 | Benchmark 1: typst compile --root . crates\mitex\benches\empty.typ 97 | Time (mean ± σ): 379.0 ms ± 8.8 ms [User: 101.2 ms, System: 32.8 ms] 98 | Range (min … max): 369.9 ms … 396.6 ms 10 runs 99 | Benchmark 1: typst compile --root . crates\mitex\benches\oiwiki.typ 100 | Time (mean ± σ): 2.214 s ± 0.073 s [User: 0.469 s, System: 0.031 s] 101 | Range (min … max): 2.096 s … 2.316 s 10 runs 102 | Benchmark 1: typst compile --root . crates\mitex\benches\oiwiki-with-render.typ 103 | Time (mean ± σ): 3.772 s ± 0.088 s [User: 1.165 s, System: 0.102 s] 104 | Range (min … max): 3.591 s … 3.897 s 10 runs 105 | 106 | convert_large_projects fastest │ slowest │ median │ mean │ samples │ iters 107 | ├─ oiwiki_231222 80.34 ms │ 86.19 ms │ 82.85 ms │ 82.86 ms │ 100 │ 100 108 | │ alloc: │ │ │ │ │ 109 | │ 1388435 │ 1388435 │ 1388435 │ 1388435 │ │ 110 | │ 84.32 MB │ 84.32 MB │ 84.32 MB │ 84.32 MB │ │ 111 | │ dealloc: │ │ │ │ │ 112 | │ 1388435 │ 1388435 │ 1388435 │ 1388435 │ │ 113 | │ 94.21 MB │ 94.21 MB │ 94.21 MB │ 94.21 MB │ │ 114 | │ grow: │ │ │ │ │ 115 | │ 71604 │ 71604 │ 71604 │ 71604 │ │ 116 | │ 9.881 MB │ 9.881 MB │ 9.881 MB │ 9.881 MB │ │ 117 | ╰─ oiwiki_231222_macro 80.75 ms │ 88.61 ms │ 83.29 ms │ 83.37 ms │ 100 │ 100 118 | alloc: │ │ │ │ │ 119 | 1388435 │ 1388435 │ 1388435 │ 1388435 │ │ 120 | 84.32 MB │ 84.32 MB │ 84.32 MB │ 84.32 MB │ │ 121 | dealloc: │ │ │ │ │ 122 | 1388435 │ 1388435 │ 1388435 │ 1388435 │ │ 123 | 94.21 MB │ 94.21 MB │ 94.21 MB │ 94.21 MB │ │ 124 | grow: │ │ │ │ │ 125 | 71604 │ 71604 │ 71604 │ 71604 │ │ 126 | 9.881 MB │ 9.881 MB │ 9.881 MB │ 9.881 MB │ │ 127 | */ 128 | -------------------------------------------------------------------------------- /crates/mitex/benches/empty.typ: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitex-rs/mitex/5fc83b64ab5e0b91918528ef2987037866e24086/crates/mitex/benches/empty.typ -------------------------------------------------------------------------------- /crates/mitex/benches/oiwiki-with-render.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "bencher.typ": * 3 | 4 | #let data = json("/local/oiwiki-231222.json"); 5 | 6 | #show: integrate-conversion.with(data: data, convert-only: false) 7 | -------------------------------------------------------------------------------- /crates/mitex/benches/oiwiki.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "bencher.typ": * 3 | 4 | #let data = json("/local/oiwiki-231222.json"); 5 | 6 | #show: integrate-conversion.with(data: data, convert-only: true) 7 | -------------------------------------------------------------------------------- /crates/mitex/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod converter; 2 | 3 | pub use mitex_parser::command_preludes; 4 | use mitex_parser::parse; 5 | use mitex_parser::parse_without_macro; 6 | pub use mitex_parser::spec::*; 7 | 8 | use converter::convert_inner; 9 | use converter::LaTeXMode; 10 | 11 | pub fn convert_text(input: &str, spec: Option) -> Result { 12 | convert_inner(input, LaTeXMode::Text, spec, parse) 13 | } 14 | 15 | pub fn convert_math(input: &str, spec: Option) -> Result { 16 | convert_inner(input, LaTeXMode::Math, spec, parse) 17 | } 18 | 19 | /// For internal testing 20 | pub fn convert_math_no_macro(input: &str, spec: Option) -> Result { 21 | convert_inner(input, LaTeXMode::Math, spec, parse_without_macro) 22 | } 23 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt.rs: -------------------------------------------------------------------------------- 1 | mod cvt { 2 | mod prelude { 3 | pub use insta::assert_snapshot; 4 | pub use mitex::convert_math as mitex_convert_math; 5 | pub use mitex::convert_text as mitex_convert_text; 6 | pub use mitex_spec_gen::DEFAULT_SPEC; 7 | 8 | pub fn convert_text(input: &str) -> Result { 9 | mitex_convert_text(input, Some(DEFAULT_SPEC.clone())) 10 | } 11 | 12 | pub fn convert_math(input: &str) -> Result { 13 | mitex_convert_math(input, Some(DEFAULT_SPEC.clone())) 14 | } 15 | } 16 | 17 | use prelude::*; 18 | 19 | #[cfg(test)] 20 | mod basic_text_mode; 21 | 22 | #[cfg(test)] 23 | mod arg_parse; 24 | 25 | #[cfg(test)] 26 | mod arg_match; 27 | 28 | #[cfg(test)] 29 | mod attachment; 30 | 31 | #[cfg(test)] 32 | mod block_comment; 33 | 34 | #[cfg(test)] 35 | mod formula; 36 | 37 | #[cfg(test)] 38 | mod fuzzing; 39 | 40 | #[cfg(test)] 41 | mod command; 42 | 43 | #[cfg(test)] 44 | mod environment; 45 | 46 | #[cfg(test)] 47 | mod left_right; 48 | 49 | #[cfg(test)] 50 | mod simple_env; 51 | 52 | #[cfg(test)] 53 | mod trivia; 54 | 55 | #[cfg(test)] 56 | mod figure; 57 | 58 | #[cfg(test)] 59 | mod tabular; 60 | 61 | #[cfg(test)] 62 | mod misc; 63 | /// Convenient function to launch/debug a test case 64 | #[test] 65 | fn bug_playground() {} 66 | 67 | #[test] 68 | fn test_easy() { 69 | assert_snapshot!(convert_math(r#"\frac{ a }{ b }"#).unwrap(), @"frac( a , b )"); 70 | } 71 | 72 | #[test] 73 | fn test_utf8_char() { 74 | // note that there is utf8 minus sign in the middle 75 | assert_snapshot!(convert_math(r#"$u^−$"#).unwrap(), @"u ^(− )" 76 | ); 77 | } 78 | 79 | #[test] 80 | fn test_beat_pandoc() { 81 | assert_snapshot!(convert_math(r#"\frac 1 2 _3"#).unwrap(), @"frac(1 ,2 ) _(3 )"); 82 | } 83 | 84 | #[test] 85 | fn test_normal() { 86 | assert_snapshot!(convert_math(r#"\int_1^2 x \mathrm{d} x"#).unwrap(), @"integral _(1 )^(2 ) x upright(d ) x "); 87 | } 88 | 89 | #[test] 90 | fn test_sticky() { 91 | assert_snapshot!(convert_math(r#"\alpha_1"#).unwrap(), @"alpha _(1 )"); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/arg_match.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn curly_group() { 5 | assert_snapshot!(convert_math(r#"a \textbf{strong} text"#).unwrap(), @"a #textbf[strong]; t e x t "); 6 | assert_snapshot!(convert_math(r#"x \color {red} yz \frac{1}{2}"#).unwrap(), @"x mitexcolor( r e d , y z frac(1 ,2 ))"); 7 | } 8 | 9 | #[test] 10 | fn split_char() { 11 | assert_snapshot!(convert_math(r#"\frac abcd"#).unwrap(), @"frac(a ,b )c d "); 12 | assert_snapshot!(convert_math(r#"\frac ab"#).unwrap(), @"frac(a ,b )"); 13 | assert_snapshot!(convert_math(r#"\frac a"#).unwrap(), @"frac(a )"); 14 | } 15 | 16 | #[test] 17 | fn eat_regular_brace() { 18 | assert_snapshot!(convert_math(r#"\mathrm(x)"#).unwrap(), @r###"upright(\()x \)"###); 19 | assert_snapshot!(convert_math(r#"\mathrm[x]"#).unwrap(), @r###"upright(\[)x \]"###); 20 | assert_snapshot!(convert_math(r#"\mathrm\lbrace x \rbrace"#).unwrap(), @r###"upright(\{ ) x \} "###); 21 | } 22 | 23 | #[test] 24 | fn special_marks() { 25 | // & and newline' 26 | assert_snapshot!(convert_math(r#"\begin{matrix} 27 | \displaystyle 1 & 2 \\ 28 | 3 & 4 \\ 29 | \end{matrix}"#).unwrap(), @r###" 30 | 31 | matrix( 32 | mitexdisplay( 1 )zws , 2 zws ; 33 | 3 zws , 4 zws ; 34 | ) 35 | "###); 36 | assert_snapshot!(convert_math(r#"\begin{matrix} 37 | \displaystyle 1 \\ 38 | 3 \\ 39 | \end{matrix}"#).unwrap(), @r###" 40 | 41 | matrix( 42 | mitexdisplay( 1 )zws ; 43 | 3 zws ; 44 | ) 45 | "###); 46 | assert_snapshot!(convert_math(r#"\begin{matrix}\frac{1} & {2}\end{matrix}"#).unwrap(), @r###" 47 | 48 | matrix(frac(1 ) zws , 2 ) 49 | "###); 50 | assert_snapshot!(convert_math(r#"\begin{matrix}\frac{1} \\ {2}\end{matrix}"#).unwrap(), @r###" 51 | 52 | matrix(frac(1 ,zws ;) 2 ) 53 | "###); 54 | assert_snapshot!(convert_math(r#"1 \over 2 \\ 3 "#).unwrap(), @r###"frac(1 , 2 \ 3 )"###); 55 | } 56 | 57 | #[test] 58 | fn special_marks_in_env() { 59 | assert_snapshot!(convert_math(r#"\displaystyle \frac{1}{2} \\ \frac{1}{2}"#).unwrap(), @r###"mitexdisplay( frac(1 ,2 ) \ frac(1 ,2 ))"###); 60 | assert_snapshot!(convert_math(r#"\left. \displaystyle \frac{1}{2} \\ \frac{1}{2} \right."#).unwrap(), @r###" 61 | 62 | lr( mitexdisplay( frac(1 ,2 ) \ frac(1 ,2 ) ) ) 63 | "###); 64 | assert_snapshot!(convert_math(r#"\sqrt[\displaystyle \frac{1}{2} \\ \frac{1}{2} ]{}"#).unwrap(), @r###" 65 | 66 | mitexsqrt(\[mitexdisplay( frac(1 ,2 ) \ frac(1 ,2 ) )\],zws ) 67 | "###); 68 | assert_snapshot!(convert_math(r#"\begin{matrix}a \over b \\ c\end{matrix}"#).unwrap(), @r###" 69 | 70 | matrix(frac(a , b )zws ; c ) 71 | "###); 72 | } 73 | 74 | #[test] 75 | fn sqrt_pattern() { 76 | assert_snapshot!(convert_math(r#"\sqrt 12"#).unwrap(), @"mitexsqrt(1 )2 "); 77 | assert_snapshot!(convert_math(r#"\sqrt{1}2"#).unwrap(), @"mitexsqrt(1 )2 "); 78 | // Note: this is an invalid expression 79 | assert_snapshot!(convert_math(r#"\sqrt[1]"#).unwrap(), @r###"mitexsqrt(\[1 \])"###); 80 | assert_snapshot!(convert_math(r#"\sqrt[1]{2}"#).unwrap(), @r###"mitexsqrt(\[1 \],2 )"###); 81 | assert_snapshot!(convert_math(r#"\sqrt[1]{2}3"#).unwrap(), @r###"mitexsqrt(\[1 \],2 )3 "###); 82 | } 83 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/arg_parse.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// Argument will reset flag of being in a formula 4 | #[test] 5 | fn arg_scope() { 6 | assert_snapshot!(convert_math(r#"$\text{${1}$}$"#).unwrap(), @"#textmath[#math.equation(block: false, $1 $);];"); 7 | // Note: This is a valid AST, but semantically incorrect (indicated by overleaf) 8 | assert_snapshot!(convert_math(r#"$\frac{${1}$}{${2}$}$"#).unwrap(), @"frac(1 ,2 )"); 9 | } 10 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/attachment.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn base() { 5 | // println!("{:#?}", parse(r#"{}_{1}^1"#)); 6 | assert_snapshot!(convert_math(r#"_1^2"#).unwrap(), @"zws_(1 )zws^(2 )"); 7 | assert_snapshot!(convert_math(r#"{}_{1}^2"#).unwrap(), @"zws _(1 )^(2 )"); 8 | assert_snapshot!(convert_math(r#"\alpha_1"#).unwrap(), @"alpha _(1 )"); 9 | assert_snapshot!(convert_math(r#"\alpha_[1]"#).unwrap(), @r###"alpha _(\[)1 \]"###); 10 | assert_snapshot!(convert_math(r#"\alpha_(1)"#).unwrap(), @r###"alpha _(\()1 \)"###); 11 | assert_snapshot!(convert_math(r#"_1"#).unwrap(), @"zws_(1 )"); 12 | // Note: this is an invalid expression 13 | assert_snapshot!(convert_math(r#"\over_1"#).unwrap(), @"frac(,zws_(1 ))"); 14 | assert_snapshot!(convert_math(r#"{}_1"#).unwrap(), @"zws _(1 )"); 15 | assert_snapshot!(convert_math(r#"{}_1_1"#).unwrap(), @"zws _(1 )_(1 )"); 16 | assert_snapshot!(convert_math(r#"\frac{1}{2}_{3}"#).unwrap(), @"frac(1 ,2 )_(3 )"); 17 | assert_snapshot!(convert_math(r#"\overbrace{a + b + c}^{\text{This is an overbrace}}"#).unwrap(), @"mitexoverbrace(a + b + c )^(#textmath[This is an overbrace];)"); 18 | assert_snapshot!(convert_math(r#"\underbrace{x \times y}_{\text{This is an underbrace}}"#).unwrap(), @"mitexunderbrace(x times y )_(#textmath[This is an underbrace];)"); 19 | assert_snapshot!(convert_math(r#"x_1''^2"#).unwrap(), @"x _(1 )''^(2 )"); 20 | assert_snapshot!(convert_math(r#"x''_1"#).unwrap(), @"x ''_(1 )"); 21 | assert_snapshot!(convert_math(r#"''"#).unwrap(), @"''"); 22 | assert_snapshot!(convert_math(r#"\frac''"#).unwrap(), @"frac(',')"); 23 | } 24 | 25 | #[test] 26 | fn test_attachment_may_weird() { 27 | assert_snapshot!(convert_math(r#"\frac ab_c"#).unwrap(), @"frac(a ,b )_(c )"); 28 | assert_snapshot!(convert_math(r#"\frac a_c b"#).unwrap(), @"frac(a )_(c ) b "); 29 | assert_snapshot!(convert_math(r#"\frac {a_c} b"#).unwrap(), @"frac(a _(c ),b )"); 30 | } 31 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/basic_text_mode.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn test_convert_text_mode() { 5 | assert_snapshot!(convert_text(r#"abc"#).unwrap(), @"abc"); 6 | assert_snapshot!(convert_text(r#"\section{Title}"#).unwrap(), @"#heading(level: 1)[Title];"); 7 | assert_snapshot!(convert_text(r#"a \textbf{strong} text"#).unwrap(), @"a #strong[strong]; text"); 8 | assert_snapshot!(convert_text(r###"\section{Title} 9 | 10 | A \textbf{strong} text, a \emph{emph} text and inline equation $x + y$. 11 | 12 | Also block \eqref{eq:pythagoras}. 13 | 14 | \begin{equation} 15 | a^2 + b^2 = c^2 \label{eq:pythagoras} 16 | \end{equation}"###).unwrap(), @r###" 17 | 18 | #heading(level: 1)[Title]; 19 | 20 | A #strong[strong]; text\, a #emph[emph]; text and inline equation #math.equation(block: false, $x + y $);. 21 | 22 | Also block #mitexref[eq:pythagoras];. 23 | 24 | $ aligned( 25 | a ^(2 ) + b ^(2 ) = c ^(2 ) 26 | ) $ 27 | "###); 28 | } 29 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/block_comment.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn base() { 5 | assert_snapshot!(convert_text(r#"\iffalse Test\fi"#).unwrap(), @""); 6 | assert_snapshot!(convert_text(r#"\iffalse Test\else \LaTeX\fi"#).unwrap(), @" LaTeX "); 7 | assert_snapshot!(convert_text(r#"\iffalse Test\ifhbox Commented HBox\fi\fi"#).unwrap(), @""); 8 | } 9 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/command.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn test_starrd_command() { 5 | // Description: If the starred command is defined, it is treated as a starred 6 | assert_snapshot!(convert_math(r#"\operatorname*{a}"#).unwrap(), @"operatornamewithlimits(a )" 7 | ); 8 | // Description: If the starred command is not defined, it is treated as a normal 9 | // command 10 | assert_snapshot!(convert_math(r#"\varphi*1"#).unwrap(), @r###"phi \*1 "### 11 | ); 12 | } 13 | 14 | #[test] 15 | fn left_association() { 16 | assert_snapshot!(convert_math(r#"\sum"#).unwrap(), @"sum "); 17 | assert_snapshot!(convert_math(r#"\sum\limits"#).unwrap(), @"limits(sum )"); 18 | assert_snapshot!(convert_math(r#"\sum \limits"#).unwrap(), @"limits(sum )"); 19 | assert_snapshot!(convert_math(r#"\sum\limits\limits"#).unwrap(), @"limits(limits(sum ))"); 20 | assert_snapshot!(convert_math(r#"\sum\limits\sum"#).unwrap(), @"limits(sum )sum "); 21 | assert_snapshot!(convert_math(r#"\sum\limits\sum\limits"#).unwrap(), @"limits(sum )limits(sum )"); 22 | assert_snapshot!(convert_math(r#"\limits"#).unwrap(), @"limits()"); 23 | } 24 | 25 | #[test] 26 | fn greedy_assosiation() { 27 | // Description: infix greed and right greedy 28 | assert_snapshot!(convert_math(r#"1 \over \displaystyle 2"#).unwrap(), @"frac(1 , mitexdisplay( 2 ))"); 29 | // Description: right greed and right greedy 30 | assert_snapshot!(convert_math(r#"\displaystyle \displaystyle 1 \over 2"#).unwrap(), @"mitexdisplay( mitexdisplay(frac( 1 , 2 )))"); 31 | // Description: right greed and infix greedy 32 | assert_snapshot!(convert_math(r#"\displaystyle 1 \over 2"#).unwrap(), @"mitexdisplay(frac( 1 , 2 ))"); 33 | // Description: infix greed and infix greedy 34 | // Note: this is an invalid expression 35 | assert_snapshot!(convert_math(r#"a \over c \over b"#).unwrap(), @"frac(a ,frac( c , b ))"); 36 | } 37 | 38 | #[test] 39 | fn right_greedy() { 40 | // Description: produces an empty argument if the righ side is empty 41 | assert_snapshot!(convert_math(r#"\displaystyle"#).unwrap(), @"mitexdisplay()"); 42 | // Description: correctly works left association 43 | // left1 commands 44 | assert_snapshot!(convert_math(r#"\displaystyle\sum\limits"#).unwrap(), @"mitexdisplay(limits(sum ))"); 45 | // subscript 46 | assert_snapshot!(convert_math(r#"\displaystyle x_1"#).unwrap(), @"mitexdisplay( x _(1 ))"); 47 | // prime 48 | assert_snapshot!(convert_math(r#"\displaystyle x'"#).unwrap(), @"mitexdisplay( x ')"); 49 | // Description: doesn't panic on incorect left association 50 | // left1 commands 51 | assert_snapshot!(convert_math(r#"\displaystyle\limits"#).unwrap(), @"mitexdisplay(limits())"); 52 | // subscript 53 | assert_snapshot!(convert_math(r#"\displaystyle_1"#).unwrap(), @"mitexdisplay(zws_(1 ))"); 54 | // prime 55 | assert_snapshot!(convert_math(r#"\displaystyle'"#).unwrap(), @"mitexdisplay(')"); 56 | // Description: all right side content is collected to a single argument 57 | assert_snapshot!(convert_math(r#"\displaystyle a b c"#).unwrap(), @"mitexdisplay( a b c )"); 58 | assert_snapshot!(convert_math(r#"\displaystyle \sum T"#).unwrap(), @"mitexdisplay( sum T )"); 59 | // Curly braces doesn't start a new argument 60 | assert_snapshot!(convert_math(r#"\displaystyle{\sum T}"#).unwrap(), @"mitexdisplay(sum T )"); 61 | // Description: doesn't identify brackets as group 62 | assert_snapshot!(convert_math(r#"\displaystyle[\sum T]"#).unwrap(), @r###"mitexdisplay(\[sum T \])"###); 63 | // Description: scoped by curly braces 64 | assert_snapshot!(convert_math(r#"a + {\displaystyle a b} c"#).unwrap(), @"a + mitexdisplay( a b ) c "); 65 | // Description: doeesn't affect left side 66 | assert_snapshot!(convert_math(r#"T \displaystyle"#).unwrap(), @"T mitexdisplay()"); 67 | } 68 | 69 | #[test] 70 | fn infix() { 71 | assert_snapshot!(convert_math(r#"\over_1"#).unwrap(), @"frac(,zws_(1 ))"); 72 | assert_snapshot!(convert_math(r#"\over'"#).unwrap(), @"frac(,')"); 73 | assert_snapshot!(convert_math(r#"a \over b'_1"#).unwrap(), @"frac(a , b '_(1 ))"); 74 | assert_snapshot!(convert_math(r#"a \over b"#).unwrap(), @"frac(a , b )"); 75 | assert_snapshot!(convert_math(r#"1 + {2 \over 3}"#).unwrap(), @"1 + frac(2 , 3 )"); 76 | } 77 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/environment.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn easy() { 5 | assert_snapshot!(convert_text(r#"\begin{equation}\end{equation}"#).unwrap(), @"$ aligned() $"); 6 | assert_snapshot!(convert_math(r#"\begin{equation}\end{equation}"#).unwrap(), @"aligned()"); 7 | } 8 | 9 | #[test] 10 | fn matrix() { 11 | assert_snapshot!(convert_math( 12 | r#"\begin{matrix} 13 | a & b \\ 14 | c & d 15 | \end{matrix}"#).unwrap(), @r###" 16 | matrix( 17 | a zws , b zws ; 18 | c zws , d 19 | ) 20 | "###); 21 | assert_snapshot!(convert_math( 22 | r#"\begin{pmatrix}\\\end{pmatrix}"#).unwrap(), @"pmatrix(zws ;)"); 23 | assert_snapshot!(convert_math( 24 | r#"\begin{pmatrix}x{\\}x\end{pmatrix}"#).unwrap(), @"pmatrix(x x )"); 25 | } 26 | 27 | #[test] 28 | fn arguments() { 29 | assert_snapshot!(convert_math( 30 | r#"\begin{array}{lc} 31 | a & b \\ 32 | c & d 33 | \end{array}"#).unwrap(), @r###" 34 | mitexarray(arg0: l c , 35 | a zws , b zws ; 36 | c zws , d 37 | ) 38 | "###); 39 | } 40 | 41 | #[test] 42 | fn space_around_and() { 43 | assert_snapshot!(convert_math( 44 | r#"\begin{bmatrix}A&B\end{bmatrix}"#).unwrap(), @"bmatrix(A zws ,B )"); 45 | } 46 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/figure.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn figure() { 5 | assert_snapshot!(convert_text(r###"\begin{figure}[ht] 6 | \centering 7 | \includegraphics[width=0.5\textwidth, height=3cm, angle=45]{example-image.png} 8 | \caption{This is an example image.} 9 | \label{fig:example} 10 | \end{figure}"###).unwrap(), @r###" 11 | #figure(caption: [This is an example image.],)[ 12 | 13 | #image(width: 0.5 * 100%, height: 3cm, "example-image.png") 14 | 15 | 16 | ]; 17 | "###); 18 | } 19 | 20 | #[test] 21 | fn table() { 22 | assert_snapshot!(convert_text(r###"\begin{table}[ht] 23 | \centering 24 | \begin{tabular}{|c|c|} 25 | \hline 26 | \textbf{Name} & \textbf{Age} \\ 27 | \hline 28 | John & 25 \\ 29 | Jane & 22 \\ 30 | \hline 31 | \end{tabular} 32 | \caption{This is an example table.} 33 | \label{tab:example} 34 | \end{table}"###).unwrap(), @r###" 35 | #figure(caption: [This is an example table.],)[ 36 | 37 | #table(stroke: none, 38 | columns: 2, 39 | align: (center, center, ), 40 | table.vline(stroke: .5pt, x: 0), table.vline(stroke: .5pt, x: 1), table.vline(stroke: .5pt, x: 2), 41 | table.hline(stroke: .5pt), 42 | [#strong[Name]; ], [#strong[Age]; ], 43 | table.hline(stroke: .5pt), 44 | [John ], [25 ], 45 | [Jane ], [22 ], 46 | table.hline(stroke: .5pt), 47 | ); 48 | 49 | 50 | ]; 51 | "###); 52 | } 53 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/formula.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn cmd_formula() { 5 | assert_snapshot!(convert_text(r#"\[\]"#).unwrap(), @"$ $"); 6 | // Note: this is a valid AST, but semantically incorrect 7 | assert_snapshot!(convert_text(r#"\[\[\]\]"#).unwrap(), @"$ $"); 8 | // Note: this is a valid AST, but semantically incorrect 9 | assert_snapshot!(convert_text(r#"\[\(\)\]"#).unwrap(), @"$ $"); 10 | // Note: this is a valid AST, but semantically incorrect 11 | // It looks strange, but we regard it as a valid AST for simplicity 12 | assert_snapshot!(convert_text(r#"\[\)\(\]"#).unwrap_err(), @"error: formula is not valid"); 13 | } 14 | 15 | #[test] 16 | fn formula_scope() { 17 | assert_snapshot!(convert_text(r#"$[)$ test"#).unwrap(), @r###"#math.equation(block: false, $\[\)$); test"###); 18 | } 19 | 20 | #[test] 21 | fn curly_scope() { 22 | // Note: this is a broken AST 23 | assert_snapshot!(convert_text(r#"${$}"#).unwrap_err(), @"error: formula is not valid"); 24 | // Note: this is a valid but incompleted AST, converter should handle it 25 | // correctly 26 | assert_snapshot!(convert_text(r#"{$}$"#).unwrap(), @"#math.equation(block: false, $$);#math.equation(block: false, $$);"); 27 | } 28 | 29 | #[test] 30 | fn env_scope() { 31 | // Note: this is a valid but incompleted AST, converter should handle it 32 | // correctly 33 | assert_snapshot!(convert_text(r#"\begin{array}$\end{array}$"#).unwrap(), @"$ mitexarray(arg0: ,) $#math.equation(block: false, $$);"); 34 | // Note: this is a valid but incompleted AST, converter should handle it 35 | // correctly 36 | assert_snapshot!(convert_text(r#"$\begin{array}$\end{array}"#).unwrap_err(), @"error: formula is not valid"); 37 | } 38 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/fuzzing.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn test_fuzzing() { 5 | assert_snapshot!(convert_math(r#"\left\0"#).unwrap_err(), @r###"error: unknown command: \0"###); 6 | assert_snapshot!(convert_math(r#"\end{}"#).unwrap_err(), @r###"error: error unexpected: """###); 7 | } 8 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/left_right.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn base() { 5 | assert_snapshot!(convert_math(r#"\left.\right."#).unwrap(), @"lr( )"); 6 | assert_snapshot!(convert_math(r#"\left.a\right."#).unwrap(), @"lr( a )"); 7 | assert_snapshot!(convert_math(r#"\left. \right] ,"#).unwrap(), @r###"lr( \] ) \,"###); 8 | assert_snapshot!(convert_math(r#"\left . a \right \|"#).unwrap(), @"lr( a || )"); 9 | assert_snapshot!(convert_math(r#"\left\langle a\right\|"#).unwrap(), @"lr(angle.l a || )"); 10 | // Note: this is an invalid expression 11 | // Error handling 12 | assert_snapshot!(convert_math(r#"\left{.}a\right{.}"#).unwrap_err(), @r###"error: error unexpected: "}""###); 13 | // Note: this is an invalid expression 14 | // Error handling 15 | assert_snapshot!(convert_math(r#"\begin{equation}\left.\right\end{equation}"#).unwrap(), @"aligned(lr( ))"); 16 | // Note: this is an invalid expression 17 | // Error handling 18 | assert_snapshot!(convert_math(r#"\begin{equation}\left\right\end{equation}"#).unwrap(), @"aligned(lr())"); 19 | } 20 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/simple_env.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn quote() { 5 | assert_snapshot!(convert_text(r#"\begin{quote}\end{quote}"#).unwrap(), @"#quote(block: true)[];"); 6 | assert_snapshot!(convert_text(r#"\begin{quote}yes\end{quote}"#).unwrap(), @"#quote(block: true)[yes];"); 7 | } 8 | 9 | #[test] 10 | fn test_abstract() { 11 | assert_snapshot!(convert_text(r#"\begin{abstract}\end{abstract}"#).unwrap(), @"#quote(block: true)[];"); 12 | assert_snapshot!(convert_text(r#"\begin{abstract}yes\end{abstract}"#).unwrap(), @"#quote(block: true)[yes];"); 13 | } 14 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/tabular.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn tabular() { 5 | assert_snapshot!(convert_text(r###"\begin{tabular}{|c|c|} 6 | \hline 7 | \textbf{Name} & \textbf{Age} \\ 8 | \hline 9 | John & 25 \\ 10 | Jane & 22 \\ 11 | \hline 12 | \end{tabular}"###).unwrap(), @r###" 13 | #table(stroke: none, 14 | columns: 2, 15 | align: (center, center, ), 16 | table.vline(stroke: .5pt, x: 0), table.vline(stroke: .5pt, x: 1), table.vline(stroke: .5pt, x: 2), 17 | table.hline(stroke: .5pt), 18 | [#strong[Name]; ], [#strong[Age]; ], 19 | table.hline(stroke: .5pt), 20 | [John ], [25 ], 21 | [Jane ], [22 ], 22 | table.hline(stroke: .5pt), 23 | ); 24 | "###); 25 | } 26 | -------------------------------------------------------------------------------- /crates/mitex/tests/cvt/trivia.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | #[test] 4 | fn curly_group() { 5 | assert_snapshot!(convert_math(r#"a \mathbf{strong} text"#).unwrap(), @"a mitexmathbf(s t r o n g ) t e x t "); 6 | } 7 | 8 | #[test] 9 | fn arguments() { 10 | assert_snapshot!(convert_math(r#"\frac { 1 } { 2 }"#).unwrap(), @"frac( 1 , 2 )"); 11 | } 12 | 13 | #[test] 14 | fn greedy_trivia() { 15 | assert_snapshot!(convert_math(r#"a {\displaystyle text } b"#).unwrap(), @"a mitexdisplay( t e x t ) b "); 16 | assert_snapshot!(convert_math(r#"\displaystyle text "#).unwrap(), @"mitexdisplay( t e x t )"); 17 | assert_snapshot!(convert_math(r#"\displaystyle {text} "#).unwrap(), @"mitexdisplay( t e x t , )"); 18 | assert_snapshot!(convert_math(r#"\displaystyle {\mathrm {text}} "#).unwrap(), @"mitexdisplay( upright(t e x t ), )"); 19 | } 20 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | logo.svg 2 | logo.png -------------------------------------------------------------------------------- /docs/logo.typ: -------------------------------------------------------------------------------- 1 | 2 | #let wh = 2.5em 3 | #set page(width: wh, height: wh, margin: .08em) 4 | 5 | // Default font of Typst 6 | #set text(font: "Linux Libertine") 7 | 8 | #let TeX = { 9 | // Default font of LaTeX 10 | set text(font: "New Computer Modern", weight: "regular") 11 | box(width: 1.7em, { 12 | [T] 13 | place(top, dx: 0.56em, dy: 0.22em)[E] 14 | place(top, dx: 1.1em)[X] 15 | }) 16 | } 17 | 18 | #let func = text.with(fill: rgb("4b69c6")) 19 | #let punc = text.with(fill: rgb("d73a49")) 20 | #align(left + horizon, { 21 | v(-0.1em) 22 | box(scale(61.8%, func("mi") + punc("\u{005B}"))) 23 | linebreak() + v(-1.1em) 24 | h(0.35em) + TeX 25 | linebreak() + v(-1.2em) 26 | box(scale(61.8%, [~]+punc("\u{005D}"))) 27 | }) 28 | -------------------------------------------------------------------------------- /docs/pageless.typ: -------------------------------------------------------------------------------- 1 | // The project function defines how your document looks. 2 | // It takes your content and some metadata and formats it. 3 | // Go ahead and customize it to your liking! 4 | #let project(title: "", authors: (), body) = { 5 | // Set the document's basic properties. 6 | set document(author: authors, title: title) 7 | set page( 8 | height: auto, 9 | width: 210mm, 10 | // numbering: "1", number-align: center, 11 | ) 12 | set text(font: ("Linux Libertine", "Source Han Sans"), size: 14pt, lang: "en") 13 | // set page(height: 297mm) 14 | 15 | 16 | // Title row. 17 | align(center)[ 18 | #block(text(weight: 700, 1.75em, title)) 19 | ] 20 | 21 | // Main body. 22 | set par(justify: true) 23 | 24 | show raw.where(block: true): rect.with(width: 100%, radius: 2pt, fill: luma(240), stroke: 0pt) 25 | 26 | body 27 | } 28 | -------------------------------------------------------------------------------- /fixtures/underleaf/ieee/ieee.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "styling.typ": * 3 | 4 | // This function gets your whole document as its `body` and formats 5 | // it as an article in the style of the IEEE. 6 | #let ieee( 7 | // The paper's title. 8 | title: [Paper Title], 9 | 10 | // An array of authors. For each author you can specify a name, 11 | // department, organization, location, and email. Everything but 12 | // but the name is optional. 13 | authors: (), 14 | 15 | // The paper's abstract. Can be omitted if you don't have one. 16 | abstract: none, 17 | 18 | // A list of index terms to display after the abstract. 19 | index-terms: (), 20 | 21 | // The article's paper size. Also affects the margins. 22 | paper-size: "us-letter", 23 | 24 | // The path to a bibliography file if you want to cite some external 25 | // works. 26 | bibliography-file: none, 27 | 28 | // The paper's content. 29 | body 30 | ) = { 31 | // Set document metadata. 32 | set document(title: title, author: authors.map(author => author.name)) 33 | 34 | // Set the body font. 35 | set text(font: "STIX Two Text", size: 10pt) 36 | 37 | set text(fill: white) if prefer-theme == "dark" 38 | // set page(fill: black) if prefer-theme == "dark" 39 | 40 | // Configure the page. 41 | set page( 42 | paper: paper-size, 43 | // The margins depend on the paper size. 44 | margin: if paper-size == "a4" { 45 | (x: 41.5pt, top: 80.51pt, bottom: 89.51pt) 46 | } else { 47 | ( 48 | x: (50pt / 216mm) * 100%, 49 | top: (55pt / 279mm) * 100%, 50 | bottom: (64pt / 279mm) * 100%, 51 | ) 52 | } 53 | ) 54 | 55 | // Configure equation numbering and spacing. 56 | set math.equation(numbering: "(1)") 57 | show math.equation: set block(spacing: 0.65em) 58 | 59 | // Configure appearance of equation references 60 | show ref: it => { 61 | if it.element != none and it.element.func() == math.equation { 62 | // Override equation references. 63 | link(it.element.location(), numbering( 64 | it.element.numbering, 65 | ..counter(math.equation).at(it.element.location()) 66 | )) 67 | } else { 68 | // Other references as usual. 69 | it 70 | } 71 | } 72 | 73 | // Configure lists. 74 | set enum(indent: 10pt, body-indent: 9pt) 75 | set list(indent: 10pt, body-indent: 9pt) 76 | 77 | // Configure headings. 78 | set heading(numbering: "I.A.1.") 79 | show heading: it => locate(loc => { 80 | // Find out the final number of the heading counter. 81 | let levels = counter(heading).at(loc) 82 | let deepest = if levels != () { 83 | levels.last() 84 | } else { 85 | 1 86 | } 87 | 88 | set text(10pt, weight: 400) 89 | if it.level == 1 [ 90 | // First-level headings are centered smallcaps. 91 | // We don't want to number of the acknowledgment section. 92 | #let is-ack = it.body in ([Acknowledgment], [Acknowledgement]) 93 | #set align(center) 94 | #set text(if is-ack { 10pt } else { 12pt }) 95 | #show: smallcaps 96 | #v(20pt, weak: true) 97 | #if it.numbering != none and not is-ack { 98 | numbering("I.", deepest) 99 | h(7pt, weak: true) 100 | } 101 | #it.body 102 | #v(13.75pt, weak: true) 103 | ] else if it.level == 2 [ 104 | // Second-level headings are run-ins. 105 | #set par(first-line-indent: 0pt) 106 | #set text(style: "italic") 107 | #v(10pt, weak: true) 108 | #if it.numbering != none { 109 | numbering("A.", deepest) 110 | h(7pt, weak: true) 111 | } 112 | #it.body 113 | #v(10pt, weak: true) 114 | ] else [ 115 | // Third level headings are run-ins too, but different. 116 | #if it.level == 3 { 117 | numbering("1)", deepest) 118 | [ ] 119 | } 120 | _#(it.body):_ 121 | ] 122 | }) 123 | 124 | // Display the paper's title. 125 | v(3pt, weak: true) 126 | align(center, text(18pt, title)) 127 | v(8.35mm, weak: true) 128 | 129 | // Display the authors list. 130 | for i in range(calc.ceil(authors.len() / 3)) { 131 | let end = calc.min((i + 1) * 3, authors.len()) 132 | let is-last = authors.len() == end 133 | let slice = authors.slice(i * 3, end) 134 | grid( 135 | columns: slice.len() * (1fr,), 136 | gutter: 12pt, 137 | ..slice.map(author => align(center, { 138 | text(12pt, author.name) 139 | if "department" in author [ 140 | \ #emph(author.department) 141 | ] 142 | if "organization" in author [ 143 | \ #emph(author.organization) 144 | ] 145 | if "location" in author [ 146 | \ #author.location 147 | ] 148 | if "email" in author [ 149 | \ #link("mailto:" + author.email) 150 | ] 151 | })) 152 | ) 153 | 154 | if not is-last { 155 | v(16pt, weak: true) 156 | } 157 | } 158 | v(40pt, weak: true) 159 | 160 | // Start two column mode and configure paragraph properties. 161 | show: columns.with(2, gutter: 12pt) 162 | set par(justify: true, first-line-indent: 1em) 163 | show par: set block(spacing: 0.65em) 164 | 165 | // Display abstract and index terms. 166 | if abstract != none [ 167 | #set text(weight: 700) 168 | #h(1em) _Abstract_---#abstract 169 | 170 | #if index-terms != () [ 171 | #h(1em)_Index terms_---#index-terms.join(", ") 172 | ] 173 | #v(2pt) 174 | ] 175 | 176 | // Display the paper's contents. 177 | body 178 | 179 | // Display bibliography. 180 | if bibliography-file != none { 181 | show bibliography: set text(8pt) 182 | bibliography(bibliography-file, title: text(10pt)[References], style: "ieee") 183 | } 184 | } -------------------------------------------------------------------------------- /fixtures/underleaf/ieee/main.tex: -------------------------------------------------------------------------------- 1 | 2 | \iftypst 3 | #import "ieee.typ": * 4 | #show: ieee.with( 5 | title: [A typesetting system to untangle the scientific writing process], 6 | abstract: [ 7 | The process of scientific writing is often tangled up with the intricacies of typesetting, leading to frustration and wasted time for researchers. In this paper, we introduce Typst, a new typesetting system designed specifically for scientific writing. Typst untangles the typesetting process, allowing researchers to compose papers faster. In a series of experiments we demonstrate that Typst offers several advantages, including faster document creation, simplified syntax, and increased ease-of-use. 8 | ], 9 | authors: ( 10 | ( 11 | name: "Martin Haug", 12 | department: [Co-Founder], 13 | organization: [Typst GmbH], 14 | location: [Berlin, Germany], 15 | email: "haug@typst.app" 16 | ), 17 | ( 18 | name: "Laurenz Mädje", 19 | department: [Co-Founder], 20 | organization: [Typst GmbH], 21 | location: [Berlin, Germany], 22 | email: "maedje@typst.app" 23 | ), 24 | ), 25 | index-terms: ("Scientific writing", "Typesetting", "Document creation", "Syntax"), 26 | bibliography-file: "refs.bib", 27 | ) 28 | \fi 29 | 30 | \section{Introduction} 31 | Scientific writing is a crucial part of the research process, allowing researchers to share their findings with the wider scientific community. However, the process of typesetting scientific documents can often be a frustrating and time-consuming affair, particularly when using outdated tools such as LaTeX. Despite being over 30 years old, it remains a popular choice for scientific writing due to its power and flexibility. However, it also comes with a steep learning curve, complex syntax, and long compile times, leading to frustration and despair for many researchers. @netwok2020 32 | 33 | \subsubsection{Paper overview} 34 | In this paper we introduce Typst, a new typesetting system designed to streamline the scientific writing process and provide researchers with a fast, efficient, and easy-to-use alternative to existing systems. Our goal is to shake up the status quo and offer researchers a better way to approach scientific writing. 35 | 36 | By leveraging advanced algorithms and a user-friendly interface, Typst offers several advantages over existing typesetting systems, including faster document creation, simplified syntax, and increased ease-of-use. 37 | 38 | To demonstrate the potential of Typst, we conducted a series of experiments comparing it to other popular typesetting systems, including LaTeX. Our findings suggest that Typst offers several benefits for scientific writing, particularly for novice users who may struggle with the complexities of LaTeX. Additionally, we demonstrate that Typst offers advanced features for experienced users, allowing for greater customization and flexibility in document creation. 39 | 40 | Overall, we believe that Typst represents a significant step forward in the field of scientific writing and typesetting, providing researchers with a valuable tool to streamline their workflow and focus on what really matters: their research. In the following sections, we will introduce Typst in more detail and provide evidence for its superiority over other typesetting systems in a variety of scenarios. 41 | 42 | \section{Methods} 43 | \iftypst 44 | #lorem(90) 45 | \fi 46 | 47 | $$ a + b = \gamma $$ 48 | 49 | \iftypst 50 | #lorem(200) 51 | \fi -------------------------------------------------------------------------------- /fixtures/underleaf/ieee/main.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "@preview/mitex:0.2.5": * 3 | 4 | #let res = mitex-convert(mode: "text", read("main.tex")) 5 | #eval(res, mode: "markup", scope: mitex-scope) 6 | -------------------------------------------------------------------------------- /fixtures/underleaf/ieee/refs.bib: -------------------------------------------------------------------------------- 1 | @article{netwok2020, 2 | title={At-scale impact of the {Net Wok}: A culinarically holistic investigation of distributed dumplings}, 3 | author={Astley, Rick and Morris, Linda}, 4 | journal={Armenian Journal of Proceedings}, 5 | volume={61}, 6 | pages={192--219}, 7 | year=2020, 8 | publisher={Automatic Publishing Inc.} 9 | } -------------------------------------------------------------------------------- /fixtures/underleaf/ieee/styling.typ: -------------------------------------------------------------------------------- 1 | 2 | #let prefer-theme = "white"; 3 | -------------------------------------------------------------------------------- /fuzz/mitex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuzz-target-mitex" 3 | authors.workspace = true 4 | version.workspace = true 5 | license.workspace = true 6 | edition.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | 10 | 11 | [dependencies] 12 | afl = "0.15" 13 | mitex = { path = "../../crates/mitex" } 14 | rowan.workspace = true 15 | 16 | [dev-dependencies] 17 | insta.workspace = true 18 | divan.workspace = true 19 | serde.workspace = true 20 | serde_json.workspace = true 21 | 22 | -------------------------------------------------------------------------------- /fuzz/mitex/src/main.rs: -------------------------------------------------------------------------------- 1 | use afl::fuzz; 2 | fn main() { 3 | fuzz!(|data: &[u8]| { 4 | if let Ok(s) = std::str::from_utf8(data) { 5 | let _ = mitex::convert_math(s, None); 6 | } 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /packages/mitex-web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/mitex-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mitex-web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^5.2.2", 13 | "vite": "^5.0.8", 14 | "vite-plugin-top-level-await": "^1.4.1", 15 | "vite-plugin-wasm": "^3.3.0" 16 | }, 17 | "dependencies": { 18 | "@codemirror/legacy-modes": "^6.3.3", 19 | "@grammarly/editor-sdk": "^2.5.5", 20 | "@isomorphic-git/lightning-fs": "^4.6.0", 21 | "codemirror": "^6.0.1", 22 | "isomorphic-git": "^1.25.2", 23 | "lang-tex": "^0.0.3", 24 | "@myriaddreamin/typst-ts-renderer": "0.4.2-rc4", 25 | "@myriaddreamin/typst.ts": "0.4.2-rc4", 26 | "mitex-wasm": "file:../../crates/mitex-wasm/pkg", 27 | "typst-dom": "file:../typst-dom", 28 | "vanjs-core": "^1.2.7" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/mitex-web/src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $typst: any; 3 | $typst$script: any; 4 | $typst$createRenderer(): Promise; 5 | initTypstSvg: any; 6 | handleTypstLocation: any; 7 | } 8 | -------------------------------------------------------------------------------- /packages/mitex-web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mitex Online Math Converter 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/mitex-web/src/loader.mjs: -------------------------------------------------------------------------------- 1 | const e = `https://cdn.jsdelivr.net/npm`; 2 | const ns = `@myriaddreamin`; 3 | const tsConfig = { 4 | lib: `${e}/${ns}/typst.ts@v0.4.2-rc4/dist/esm/contrib/all-in-one-lite.bundle.js`, 5 | compilerModule: `${e}/${ns}/typst-ts-web-compiler@v0.4.2-rc4/pkg/typst_ts_web_compiler_bg.wasm`, 6 | rendererModule: `${e}/${ns}/typst-ts-renderer@v0.4.2-rc4/pkg/typst_ts_renderer_bg.wasm`, 7 | }; 8 | window.$typst$script = new Promise((resolve) => { 9 | const head = document.getElementsByTagName("head")[0]; 10 | const s = document.createElement("script"); 11 | s.type = "module"; 12 | s.onload = resolve; 13 | s.src = tsConfig.lib; 14 | s.id = "typst"; 15 | head.appendChild(s); 16 | }).then(() => { 17 | const $typst = window.$typst; 18 | $typst.setCompilerInitOptions({ getModule: () => tsConfig.compilerModule }); 19 | $typst.setRendererInitOptions({ getModule: () => tsConfig.rendererModule }); 20 | }); -------------------------------------------------------------------------------- /packages/mitex-web/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | import "./loader.mjs"; 3 | import van, { State } from "vanjs-core"; 4 | import { convert_math } from "mitex-wasm"; 5 | const { div, textarea, button } = van.tags; 6 | 7 | let $typst = window.$typst; 8 | 9 | const isDarkMode = () => 10 | window.matchMedia?.("(prefers-color-scheme: dark)").matches; 11 | 12 | const App = () => { 13 | /// Default source code 14 | const srcDefault = `\\newcommand{\\f}[2]{#1f(#2)} 15 | \\f\\relax{x} = \\int_{-\\infty}^\\infty 16 | \\f\\hat\\xi\\,e^{2 \\pi i \\xi x} 17 | \\,d\\xi`; 18 | 19 | /// Capture compiler load status 20 | let compilerLoaded = van.state(false); 21 | let fontLoaded = van.state(false); 22 | window.$typst$script.then(async () => { 23 | compilerLoaded.val = true; 24 | await $typst.svg({ mainContent: "" }); 25 | fontLoaded.val = true; 26 | }); 27 | 28 | /// The latex code input 29 | const input = van.state(srcDefault), 30 | /// The converted Typst code 31 | output = van.state(""), 32 | /// The source code state 33 | error = van.state(""), 34 | /// The dark mode style 35 | darkModeStyle = van.derive(() => { 36 | if (isDarkMode()) { 37 | return `#set text(fill: rgb("#fff"));`; 38 | } else { 39 | return `#set text(fill: rgb("#000"));`; 40 | } 41 | }); 42 | 43 | /// Drive src, output and error from input 44 | van.derive(() => { 45 | try { 46 | let convert_res = convert_math(input.val, new Uint8Array()); 47 | output.val = convert_res; 48 | error.val = ""; 49 | } catch (e) { 50 | output.val = ""; 51 | error.val = e as string; 52 | } 53 | }); 54 | 55 | /// The preview component 56 | const Preview = (output: State) => { 57 | const svgData = van.state(""); 58 | van.derive(async () => { 59 | if (fontLoaded.val) { 60 | svgData.val = await $typst.svg({ 61 | mainContent: `#import "@preview/mitex:0.2.5": * 62 | #set page(width: auto, height: auto, margin: 1em); 63 | #set text(size: 24pt); 64 | ${darkModeStyle.val} 65 | #math.equation(eval("$" + \`${output.val}\`.text + "$", mode: "markup", scope: mitex-scope), block: true) 66 | `, 67 | }); 68 | } else { 69 | svgData.val = ""; 70 | } 71 | }); 72 | 73 | return div( 74 | { class: "mitex-preview" }, 75 | div({ 76 | innerHTML: van.derive(() => { 77 | if (!compilerLoaded.val) { 78 | return "Loading compiler from CDN..."; 79 | } else if (!fontLoaded.val) { 80 | return "Loading fonts from CDN..."; 81 | } else { 82 | return svgData.val; 83 | } 84 | }), 85 | }) 86 | ); 87 | }; 88 | 89 | /// Copy a a derived string to clipboard 90 | const CopyButton = (title: string, content: State) => 91 | button({ 92 | onclick: () => navigator.clipboard.writeText(content.val), 93 | textContent: title, 94 | }); 95 | 96 | return div( 97 | { class: "mitex-main flex-column" }, 98 | div( 99 | { class: "mitex-edit-row flex-row" }, 100 | textarea({ 101 | class: "mitex-input", 102 | placeholder: "Type LaTeX math equations here", 103 | value: srcDefault, 104 | autofocus: true, 105 | rows: 10, 106 | oninput(event: Event) { 107 | input.val = (event.target! as HTMLInputElement).value; 108 | }, 109 | }), 110 | textarea({ 111 | class: "mitex-output", 112 | value: output, 113 | readOnly: true, 114 | placeholder: "Output", 115 | rows: 10, 116 | onfocus: (event: Event) => 117 | (event.target! as HTMLTextAreaElement).select(), 118 | }) 119 | ), 120 | /// Create DOM elements 121 | CopyButton( 122 | "Copy with template", 123 | van.derive( 124 | () => 125 | `#math.equation(eval("$" + \`${output.val}\`.text + "$", mode: "markup", scope: mitex-scope), block: true)` 126 | ) 127 | ), 128 | CopyButton( 129 | "Copy with template and imports", 130 | van.derive( 131 | () => `#import "@preview/mitex:0.2.5": *\n 132 | #math.equation(eval("$" + \`${output.val}\`.text + "$", mode: "markup", scope: mitex-scope), block: true)` 133 | ) 134 | ), 135 | Preview(output), 136 | div({ class: "error", textContent: error }) 137 | ); 138 | }; 139 | 140 | van.add(document.querySelector("#app")!, App()); 141 | -------------------------------------------------------------------------------- /packages/mitex-web/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | display: flex; 19 | place-items: center; 20 | min-width: 320px; 21 | min-height: 100vh; 22 | } 23 | 24 | #app { 25 | max-width: 1920px; 26 | margin: 0 auto; 27 | padding: 2rem; 28 | text-align: center; 29 | } 30 | 31 | .mitex-preview { 32 | border-radius: 8px; 33 | border: 1px solid #000; 34 | min-height: 1em; 35 | overflow: auto; 36 | max-width: 80vw; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | 55 | button:focus, 56 | button:focus-visible { 57 | outline: 4px auto -webkit-focus-ring-color; 58 | } 59 | 60 | @media (prefers-color-scheme: light) { 61 | :root { 62 | color: #213547; 63 | background-color: #ffffff; 64 | } 65 | 66 | a:hover { 67 | color: #747bff; 68 | } 69 | 70 | button { 71 | background-color: #f9f9f9; 72 | } 73 | } 74 | 75 | textarea { 76 | font-family: "Roboto Mono", Menlo, Consolas, monospace; 77 | font-size: 1em; 78 | font-weight: 400; 79 | line-height: 1.5; 80 | padding: 0.6em 1.2em; 81 | margin: 1em; 82 | border-radius: 4px; 83 | border: 1px solid #e0e0e0; 84 | transition: border-color 0.25s; 85 | } 86 | 87 | div.error { 88 | color: #ff0000; 89 | font-family: "Roboto Mono", Menlo, Consolas, monospace; 90 | } 91 | 92 | .flex-column { 93 | display: flex; 94 | flex-direction: column; 95 | } 96 | 97 | .flex-row { 98 | display: flex; 99 | flex-direction: row; 100 | justify-content: center; 101 | } 102 | 103 | .mitex-main { 104 | max-width: 1920px; 105 | display: flex; 106 | flex-direction: column; 107 | } 108 | -------------------------------------------------------------------------------- /packages/mitex-web/src/tools/typst.css: -------------------------------------------------------------------------------- 1 | 2 | .hidden { 3 | display: none; 4 | } 5 | 6 | .flex-row { 7 | display: flex; 8 | flex-direction: row; 9 | } 10 | 11 | .flex-row-rev { 12 | display: flex; 13 | flex-direction: row-reverse; 14 | } 15 | 16 | .flex-col { 17 | display: flex; 18 | flex-direction: column; 19 | } 20 | 21 | .disable-scrollbars::-webkit-scrollbar { 22 | background: transparent; /* Chrome/Safari/Webkit */ 23 | width: 0px; 24 | } 25 | 26 | .disable-scrollbars { 27 | scrollbar-width: none; /* Firefox */ 28 | -ms-overflow-style: none; /* IE 10+ */ 29 | } 30 | 31 | #typst-app { 32 | width: fit-content; 33 | margin: 0; 34 | transform-origin: 0 0; 35 | } 36 | 37 | .hide-scrollbar-x { 38 | overflow-x: hidden; 39 | } 40 | 41 | .hide-scrollbar-y { 42 | overflow-y: hidden; 43 | } 44 | 45 | .typst-text { 46 | pointer-events: bounding-box; 47 | } 48 | 49 | .tsel span, 50 | .tsel { 51 | left: 0; 52 | position: fixed; 53 | text-align: justify; 54 | white-space: nowrap; 55 | width: 100%; 56 | height: 100%; 57 | text-align-last: justify; 58 | color: transparent; 59 | } 60 | 61 | .tsel span::-moz-selection, 62 | .tsel::-moz-selection { 63 | color: transparent; 64 | background: #7db9dea0; 65 | } 66 | 67 | .tsel span::selection, 68 | .tsel::selection { 69 | color: transparent; 70 | background: #7db9dea0; 71 | } 72 | 73 | svg { 74 | --glyph_fill: black; 75 | } 76 | 77 | .pseudo-link { 78 | fill: transparent; 79 | cursor: pointer; 80 | pointer-events: all; 81 | } 82 | 83 | .image_glyph image, 84 | .outline_glyph path, path.outline_glyph { 85 | transform: matrix(1, 0, 0, 1, var(--o), 0); 86 | } 87 | 88 | .outline_glyph path, path.outline_glyph { 89 | fill: var(--glyph_fill); 90 | } 91 | 92 | .hover .typst-text { 93 | --glyph_fill: #66bab7; 94 | } 95 | 96 | .typst-jump-ripple, 97 | .typst-debug-react-ripple { 98 | width: 0; 99 | height: 0; 100 | background-color: transparent; 101 | position: absolute; 102 | border-radius: 50%; 103 | } 104 | 105 | .typst-jump-ripple { 106 | border: 2px solid #66bab7; 107 | } 108 | 109 | .typst-debug-react-ripple { 110 | border: 1px solid #cb1b45; 111 | } 112 | 113 | @keyframes typst-jump-ripple-effect { 114 | to { 115 | width: 10vw; 116 | height: 10vw; 117 | opacity: 0.01; 118 | margin: -5vw; 119 | } 120 | } 121 | 122 | @keyframes typst-debug-react-ripple-effect { 123 | to { 124 | width: 3vw; 125 | height: 3vw; 126 | opacity: 0.1; 127 | margin: -1.5vw; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /packages/mitex-web/src/tools/underleaf-editor.ts: -------------------------------------------------------------------------------- 1 | // https://codesandbox.io/p/sandbox/codemirror-6-grammarly-latex-opwol7?file=%2Findex.js%3A1%2C1-27%2C2 2 | import van, { State } from "vanjs-core"; 3 | const { div } = van.tags; 4 | 5 | import { 6 | ChangeSet, 7 | EditorState, 8 | Range as EditorRange, 9 | } from "@codemirror/state"; 10 | import { EditorView } from "@codemirror/view"; 11 | import { 12 | syntaxHighlighting, 13 | defaultHighlightStyle, 14 | StreamLanguage, 15 | } from "@codemirror/language"; 16 | import { stex } from "@codemirror/legacy-modes/mode/stex"; 17 | import { 18 | ViewPlugin, 19 | Decoration, 20 | DecorationSet, 21 | PluginValue, 22 | } from "@codemirror/view"; 23 | import { syntaxTree } from "@codemirror/language"; 24 | import { EditorSDK, init } from "@grammarly/editor-sdk"; 25 | import { FsItemState } from "./underleaf-fs"; 26 | 27 | interface NativeSpellPlugin extends PluginValue { 28 | decorations: DecorationSet; 29 | } 30 | 31 | const nativeSpelling = () => { 32 | return ViewPlugin.define( 33 | () => { 34 | const buildDecorations = (view: EditorView) => { 35 | const decorations: EditorRange[] = []; 36 | 37 | const tree = syntaxTree(view.state); 38 | 39 | for (const { from, to } of view.visibleRanges) { 40 | tree.iterate({ 41 | from, 42 | to, 43 | enter({ type, from, to }) { 44 | if (typesWithoutSpellcheck.includes(type.name)) { 45 | decorations.push(spellcheckDisabledMark.range(from, to)); 46 | } 47 | }, 48 | }); 49 | } 50 | 51 | return Decoration.set(decorations); 52 | }; 53 | 54 | const value: NativeSpellPlugin = { 55 | decorations: Decoration.none, 56 | update(update) { 57 | /// shouldRecalculate 58 | if (update.docChanged || update.viewportChanged) { 59 | value.decorations = buildDecorations(update.view); 60 | } 61 | }, 62 | }; 63 | return value; 64 | }, 65 | { 66 | decorations: (value) => value.decorations, 67 | } 68 | ); 69 | }; 70 | 71 | const spellcheckDisabledMark = Decoration.mark({ 72 | attributes: { spellcheck: "false" }, 73 | }); 74 | 75 | const typesWithoutSpellcheck = ["typeName", "atom"]; 76 | 77 | const extensions = [ 78 | EditorView.contentAttributes.of({ spellcheck: "true" }), 79 | EditorView.lineWrapping, 80 | StreamLanguage.define(stex), 81 | syntaxHighlighting(defaultHighlightStyle), 82 | nativeSpelling(), 83 | ]; 84 | 85 | const encoder = new TextEncoder(); 86 | const decoder = new TextDecoder("utf-8"); 87 | /// The editor component 88 | export const Editor = ( 89 | darkMode: State, 90 | changeFocusFile: State, 91 | focusFile: State 92 | ) => { 93 | let updateListenerExtension = EditorView.updateListener.of(async (update) => { 94 | const f = focusFile.val; 95 | if (update.docChanged && f) { 96 | const c = encoder.encode(update.state.doc.toString()); 97 | await window.$typst?.mapShadow(f.path, c); 98 | f.data.val = c; 99 | // console.log("update", f.path, decoder.decode(c)); 100 | } 101 | }); 102 | 103 | const state = EditorState.create({ 104 | extensions: [ 105 | ...extensions, 106 | EditorView.theme({ 107 | "&": { height: "100%" }, 108 | }), 109 | EditorView.darkTheme.of(darkMode.val), 110 | updateListenerExtension, 111 | ], 112 | }); 113 | const view = new EditorView({ state }); 114 | init("client_9m1fYK3MPQxwKsib5CxtpB").then((Grammarly: EditorSDK) => { 115 | Grammarly.addPlugin(view.contentDOM, { 116 | activation: "immediate", 117 | }); 118 | }); 119 | 120 | const vs = van.derive(() => { 121 | // console.log("focusFile.val", changeFocusFile.val); 122 | const path = changeFocusFile.val?.path; 123 | if (!changeFocusFile.val || !path) { 124 | return ""; 125 | } 126 | if (path.endsWith(".png")) { 127 | return `Binary file ${path} is not shown`; 128 | } 129 | const data = changeFocusFile.val.data.val; 130 | return data ? decoder.decode(data) : ""; 131 | }); 132 | van.derive(() => { 133 | view.dispatch({ 134 | changes: [ 135 | ChangeSet.empty(0), 136 | { 137 | from: 0, 138 | insert: vs.val, 139 | }, 140 | ], 141 | }); 142 | }); 143 | 144 | return div({ class: "mitex-input" }, view.dom); 145 | }; 146 | -------------------------------------------------------------------------------- /packages/mitex-web/src/tools/underleaf-preview.ts: -------------------------------------------------------------------------------- 1 | import van, { State } from "vanjs-core"; 2 | import { PreviewMode, TypstDocument } from "typst-dom/typst-doc.mjs"; 3 | import type { RenderSession } from "@myriaddreamin/typst.ts/dist/esm/renderer.mjs"; 4 | 5 | const { div } = van.tags; 6 | 7 | export interface PreviewViewState { 8 | darkMode: State; 9 | compilerLoaded: State; 10 | fontLoaded: State; 11 | typstDoc: State; 12 | } 13 | 14 | /// The preview component 15 | export const Preview = ({ 16 | darkMode, 17 | compilerLoaded, 18 | fontLoaded, 19 | typstDoc, 20 | }: PreviewViewState) => { 21 | const previewRef = van.state(undefined); 22 | const kModule = van.state(undefined); 23 | 24 | /// Creates a render session 25 | van.derive( 26 | async () => 27 | fontLoaded.val && 28 | (await window.$typst.getRenderer()).runWithSession( 29 | (m: RenderSession) /* module kernel from wasm */ => { 30 | return new Promise(async (kModuleDispose) => { 31 | kModule.val = m; 32 | /// simply let session leak 33 | void kModuleDispose; 34 | }); 35 | } 36 | ) 37 | ); 38 | 39 | /// Creates a TypstDocument 40 | van.derive(() => { 41 | if (!(kModule.val && previewRef.val)) { 42 | return; 43 | } 44 | 45 | if (typstDoc.val) { 46 | return; 47 | } 48 | 49 | const hookedElem = previewRef.val!; 50 | if (hookedElem.firstElementChild?.tagName !== "svg") { 51 | hookedElem.innerHTML = ""; 52 | } 53 | const resizeTarget = document.getElementById("mitex-preview")!; 54 | 55 | const doc = (typstDoc.val = new TypstDocument(hookedElem, kModule.val!, { 56 | previewMode: PreviewMode.Doc, 57 | isContentPreview: false, 58 | sourceMapping: false, 59 | // set rescale target to `body` 60 | retrieveDOMState() { 61 | return { 62 | // reserving 1px to hide width border 63 | width: resizeTarget.clientWidth + 1, 64 | // reserving 1px to hide width border 65 | height: resizeTarget.offsetHeight, 66 | boundingRect: resizeTarget.getBoundingClientRect(), 67 | }; 68 | }, 69 | })); 70 | doc.setPartialRendering(true); 71 | 72 | /// Responds to dark mode change 73 | van.derive(() => doc.setPageColor(darkMode.val ? "#242424" : "white")); 74 | }); 75 | 76 | return div({ id: "mitex-preview" }, (dom?: Element) => { 77 | dom ||= div(); 78 | if (!compilerLoaded.val) { 79 | dom.textContent = "Loading compiler from CDN..."; 80 | } else if (!fontLoaded.val) { 81 | dom.textContent = "Loading fonts from CDN..."; 82 | } else { 83 | dom.textContent = ""; 84 | /// Catches a new reference to dom 85 | previewRef.val = dom as HTMLDivElement; 86 | } 87 | return dom; 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /packages/mitex-web/src/tools/underleaf.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | min-width: 320px; 19 | height: 100vh; 20 | overflow: hidden; 21 | } 22 | 23 | #app { 24 | max-width: 1920px; 25 | margin: 0 auto; 26 | padding: 1vw; 27 | height: 100%; 28 | } 29 | 30 | .mitex-main { 31 | height: calc(100% - 2vw); 32 | max-width: 1920px; 33 | } 34 | 35 | .mitex-dir-view { 36 | flex: 8; 37 | min-width: 150px; 38 | border-radius: 4px; 39 | border: 1px solid #000; 40 | padding: 5px; 41 | } 42 | 43 | .mitex-dir-view-icon path { 44 | stroke: #fff; 45 | } 46 | 47 | .mitex-input { 48 | flex: 46; 49 | border-radius: 4px; 50 | border: 1px solid #000; 51 | } 52 | 53 | #mitex-preview { 54 | flex: 46; 55 | border-radius: 4px; 56 | border: 1px solid #000; 57 | min-height: 1em; 58 | overflow: auto; 59 | max-width: 80vw; 60 | text-align: center; 61 | } 62 | 63 | .mitex-dir-view { 64 | word-wrap: break-word; 65 | } 66 | 67 | .mitex-toolbar-row { 68 | justify-content: flex-end; 69 | height: 30px; 70 | } 71 | 72 | .mitex-edit-row { 73 | height: calc(100% - 30px); 74 | } 75 | 76 | button { 77 | border-radius: 8px; 78 | border: 1px solid transparent; 79 | padding: 0.3em 0.6em; 80 | font-size: 1em; 81 | font-weight: 500; 82 | font-family: inherit; 83 | background-color: #1a1a1a; 84 | cursor: pointer; 85 | transition: border-color 0.25s; 86 | } 87 | 88 | button:hover { 89 | border-color: #646cff; 90 | } 91 | 92 | button:focus, 93 | button:focus-visible { 94 | outline: 4px auto -webkit-focus-ring-color; 95 | } 96 | 97 | @media (prefers-color-scheme: light) { 98 | :root { 99 | color: #213547; 100 | background-color: #ffffff; 101 | } 102 | 103 | .mitex-dir-view-icon path { 104 | stroke: #000; 105 | } 106 | 107 | a:hover { 108 | color: #747bff; 109 | } 110 | 111 | button { 112 | background-color: #f9f9f9; 113 | } 114 | } 115 | 116 | textarea { 117 | font-family: "Roboto Mono", Menlo, Consolas, monospace; 118 | font-size: 1em; 119 | font-weight: 400; 120 | line-height: 1.5; 121 | padding: 0.6em 1.2em; 122 | margin: 1em; 123 | border-radius: 4px; 124 | border: 1px solid #e0e0e0; 125 | transition: border-color 0.25s; 126 | } 127 | 128 | div.error { 129 | color: #ff0000; 130 | font-size: 10px; 131 | font-family: "Roboto Mono", Menlo, Consolas, monospace; 132 | } 133 | 134 | .flex-column { 135 | display: flex; 136 | flex-direction: column; 137 | } 138 | 139 | .flex-row { 140 | display: flex; 141 | flex-direction: row; 142 | } 143 | 144 | .text-layer { 145 | position: relative; 146 | left: 0; 147 | top: 0; 148 | right: 0; 149 | bottom: 0; 150 | overflow: hidden; 151 | opacity: 0.2; 152 | line-height: 1; 153 | text-align: left; 154 | } 155 | 156 | .text-layer > div { 157 | /* color: transparent; */ 158 | /* position: absolute; */ 159 | white-space: pre; 160 | cursor: text; 161 | transform-origin: 0% 0%; 162 | } 163 | -------------------------------------------------------------------------------- /packages/mitex-web/src/tools/underleaf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mitex Online Text Converter 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/mitex-web/src/tools/underleaf.ts: -------------------------------------------------------------------------------- 1 | import "./underleaf.css"; 2 | import "./typst.css"; 3 | import "./typst.ts"; 4 | import "../loader.mjs"; 5 | import van from "vanjs-core"; 6 | const { div, button } = van.tags; 7 | 8 | import { Editor } from "./underleaf-editor"; 9 | import { DirectoryView, FsItemState } from "./underleaf-fs"; 10 | import { Preview } from "./underleaf-preview"; 11 | import { TypstDocument } from "typst-dom/typst-doc.mjs"; 12 | import { IncrementalServer } from "@myriaddreamin/typst.ts/dist/esm/compiler.mjs"; 13 | 14 | let $typst = window.$typst; 15 | 16 | /// Checks if the browser is in dark mode 17 | const isDarkMode = () => 18 | window.matchMedia?.("(prefers-color-scheme: dark)").matches; 19 | 20 | /// Exports the document 21 | const ExportButton = (title: string, onclick: () => void) => 22 | button({ 23 | onclick, 24 | textContent: title, 25 | }); 26 | 27 | const App = () => { 28 | /// External status 29 | const /// Captures compiler load status 30 | compilerLoaded = van.state(false), 31 | /// Captures font load status 32 | fontLoaded = van.state(false), 33 | /// Binds to filesystem reload event 34 | reloadBell = van.state(false); 35 | 36 | const mainFilePath = "/repo/fixtures/underleaf/ieee/main.typ"; 37 | 38 | /// Component status 39 | const /// The incremental server 40 | incrServer = van.state(undefined), 41 | /// The document in memory 42 | typstDoc = van.state(undefined), 43 | /// request to change focus file 44 | changeFocusFile = van.state(undefined), 45 | /// The current focus file 46 | focusFile = van.state(undefined); 47 | 48 | /// Styles and outputs 49 | const /// The source code state 50 | error = van.state(""), 51 | /// The dark mode style 52 | darkMode = van.state(isDarkMode()); 53 | 54 | /// Checks compiler status 55 | window.$typst$script.then(async () => { 56 | $typst = window.$typst; 57 | 58 | await $typst.getCompiler(); 59 | compilerLoaded.val = true; 60 | await $typst.svg({ mainContent: "" }); 61 | fontLoaded.val = true; 62 | 63 | (await $typst.getCompiler()).withIncrementalServer( 64 | (srv: IncrementalServer) => { 65 | return new Promise((disposeServer) => { 66 | incrServer.val = srv; 67 | 68 | /// Let it leak. 69 | void disposeServer; 70 | }); 71 | } 72 | ); 73 | }); 74 | 75 | /// Listens to dark mode change 76 | window 77 | .matchMedia?.("(prefers-color-scheme: dark)") 78 | .addEventListener("change", (event) => (darkMode.val = event.matches)); 79 | 80 | /// Triggers compilation when precondition is met or changed 81 | van.derive(async () => { 82 | try { 83 | if ( 84 | /// Compiler with fonts should be loaded 85 | fontLoaded.val && 86 | /// Incremental server should be loaded 87 | incrServer.val && 88 | /// Typst document should be loaded 89 | typstDoc.val && 90 | /// Filesystem should be loaded 91 | reloadBell.val && 92 | /// recompile If focus file changed 93 | focusFile.val && 94 | /// recompile If focus file content changed 95 | focusFile.val.data.val && 96 | /// recompile If dark mode changed 97 | darkMode 98 | ) { 99 | console.log("recompilation"); 100 | 101 | setTypstTheme(darkMode.val); 102 | 103 | const v = await ( 104 | await $typst.getCompiler() 105 | ).compile({ 106 | mainFilePath, 107 | incrementalServer: incrServer.val, 108 | }); 109 | 110 | // todo: incremental update 111 | typstDoc.val.addChangement(["diff-v1", v as any]); 112 | } 113 | 114 | error.val = ""; 115 | } catch (e) { 116 | error.val = e as string; 117 | } 118 | }); 119 | 120 | const exportAs = (data: string | Uint8Array, mime: string) => { 121 | var fileBlob = new Blob([data], { type: mime }); 122 | 123 | // Create element with tag 124 | const link = document.createElement("a"); 125 | 126 | // name 127 | link.download = 128 | mime === "application/pdf" 129 | ? "A typesetting system to untangle the scientific writing process.pdf" 130 | : "A typesetting system to untangle the scientific writing process.html"; 131 | 132 | // Add file content in the object URL 133 | link.href = URL.createObjectURL(fileBlob); 134 | 135 | // Add file name 136 | link.target = "_blank"; 137 | 138 | // Add click event to tag to save file. 139 | link.click(); 140 | URL.revokeObjectURL(link.href); 141 | }; 142 | 143 | const exportPdf = () => { 144 | setTypstTheme(false); 145 | const pdfData = $typst.pdf({ mainFilePath }); 146 | return pdfData.then((pdfData: string) => 147 | exportAs(pdfData, "application/pdf") 148 | ); 149 | }; 150 | 151 | const exportHtml = () => { 152 | setTypstTheme(false); 153 | const svgData = $typst.svg({ 154 | mainFilePath, 155 | data_selection: { body: true, defs: true, css: true }, 156 | }); 157 | return svgData.then((svgData: string) => 158 | exportAs( 159 | ` 160 | 161 | A typesetting system to untangle the scientific writing process 162 | ${svgData} 163 | 164 | `, 165 | "text/html" 166 | ) 167 | ); 168 | }; 169 | 170 | return div( 171 | { class: "mitex-main flex-column" }, 172 | div( 173 | { 174 | class: "flex-row", 175 | style: "justify-content: space-between; margin-bottom: 10px", 176 | }, 177 | div( 178 | { 179 | style: 180 | "display: flex; align-items: center; text-align: center; text-decoration: underline; padding-left: 10px", 181 | }, 182 | "A typesetting system to untangle the scientific writing process" 183 | ), 184 | div( 185 | { class: "mitex-toolbar-row flex-row" }, 186 | div({ class: "error", textContent: error }), 187 | ExportButton("Export to PDF", exportPdf), 188 | div({ style: "width: 5px" }), 189 | ExportButton("HTML", exportHtml) 190 | ) 191 | ), 192 | div( 193 | { class: "mitex-edit-row flex-row" }, 194 | DirectoryView({ compilerLoaded, changeFocusFile, focusFile, reloadBell }), 195 | Editor(darkMode, changeFocusFile, focusFile), 196 | Preview({ darkMode, compilerLoaded, fontLoaded, typstDoc }) 197 | ) 198 | ); 199 | 200 | async function setTypstTheme(darkMode: boolean) { 201 | let styling = darkMode 202 | ? `#let prefer-theme = "dark";` 203 | : `#let prefer-theme = "light";`; 204 | await $typst.addSource( 205 | "/repo/fixtures/underleaf/ieee/styling.typ", 206 | styling 207 | ); 208 | } 209 | }; 210 | 211 | van.add(document.querySelector("#app")!, App()); 212 | -------------------------------------------------------------------------------- /packages/mitex-web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/mitex-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "types": ["./src/global.d.ts"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/mitex-web/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import wasm from "vite-plugin-wasm"; 4 | import topLevelAwait from "vite-plugin-top-level-await"; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | wasm(), 9 | topLevelAwait(), 10 | ], 11 | root: "src", 12 | build: { 13 | outDir: '../dist', 14 | emptyOutDir: true, 15 | rollupOptions: { 16 | input: { 17 | main: resolve(__dirname, 'src/index.html'), 18 | underleaf: resolve(__dirname, 'src/tools/underleaf.html'), 19 | }, 20 | }, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /packages/mitex/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.2.5 4 | 5 | - Fix: assign correct class for lvert and rvert (https://github.com/mitex-rs/mitex/pull/172) 6 | - Fix: convert `bf` to `bold(upright(..))` (https://github.com/mitex-rs/mitex/pull/183) 7 | - Fix: remove xarrow and use stretch in typst v0.12.0 (https://github.com/mitex-rs/mitex/pull/184) 8 | - Fix: fix some symbols `\not`, `\gtreqqless` and `\gtrapprox` (https://github.com/mitex-rs/mitex/pull/185) 9 | 10 | 11 | ## 0.2.4 12 | 13 | - Fix `\boxed` command. 14 | - Support basic figure and tabular environments. 15 | - Add escape symbols for plus, minus and percent. 16 | - Fix `\left` and `\right`: size of lr use em. 17 | 18 | 19 | ## 0.2.3 20 | 21 | - Support nesting math equation like `\text{$x$}`. 22 | - Fix broken `\left` and `\right` commands. #143 23 | - Correct the symbol mapping for `\varphi`, `phi`, `\epsilon` and `\varepsilon`. 24 | 25 | 26 | ## 0.2.2 27 | 28 | ### Text Mode 29 | 30 | - Add `\(\)` and `\[\]` support. 31 | - Add `\footnote` and `\cite`. 32 | 33 | ### Fixes 34 | 35 | - Remove duplicate keys for error when a dict has duplicate keys in Typst 0.11. 36 | - Fix bracket/paren group parsing. 37 | - Remove extra spacing for ceiling symbols. 38 | 39 | 40 | ## 0.2.1 41 | 42 | - Remove unnecessary zws. 43 | 44 | 45 | ## 0.2.0 46 | 47 | - Add more symbols for equations. 48 | - Basic macro support. 49 | - Basic text mode support. 50 | 51 | 52 | ## 0.1.0 53 | 54 | - LaTeX support for Typst, powered by Rust and WASM. We can now render LaTeX equations in real-time in Typst. -------------------------------------------------------------------------------- /packages/mitex/examples/bench.typ: -------------------------------------------------------------------------------- 1 | #import "../lib.typ": * 2 | 3 | #set page(width: 500pt) 4 | 5 | #assert.eq(mitex-convert("\alpha x"), "alpha x ") 6 | 7 | Write inline equations like #mi("x") or #mi[y]. 8 | 9 | Also block equations: 10 | 11 | #mitex("\alpha x" * 8000) 12 | 13 | /* 14 | last^1 15 | 17000 16 | Benchmark 1: typst compile --root . packages\mitex\examples\bench.typ 17 | Time (mean ± σ): 323.1 ms ± 18.0 ms [User: 84.4 ms, System: 14.1 ms] 18 | Range (min … max): 302.1 ms … 353.9 ms 10 runs 19 | 8000 20 | Benchmark 1: typst compile --root . packages\mitex\examples\bench.typ 21 | Time (mean ± σ): 198.3 ms ± 6.5 ms [User: 50.2 ms, System: 24.6 ms] 22 | Range (min … max): 188.5 ms … 207.1 ms 14 runs 23 | 24 | last^2 25 | 17000 26 | Benchmark 1: typst compile --root . packages\mitex\examples\bench.typ 27 | Time (mean ± σ): 638.8 ms ± 10.4 ms [User: 143.8 ms, System: 32.8 ms] 28 | Range (min … max): 616.5 ms … 652.5 ms 10 runs 29 | 8000 30 | Benchmark 1: typst compile --root . packages\mitex\examples\bench.typ 31 | Time (mean ± σ): 503.2 ms ± 15.1 ms [User: 109.4 ms, System: 28.1 ms] 32 | Range (min … max): 485.8 ms … 535.5 ms 10 runs 33 | 34 | init 35 | 17000 36 | Benchmark 1: typst compile --root . typst-package\examples\bench.typ 37 | Time (mean ± σ): 972.4 ms ± 28.3 ms [User: 223.4 ms, System: 62.2 ms] 38 | Range (min … max): 938.4 ms … 1029.7 ms 10 runs 39 | 8000 40 | Benchmark 1: typst compile --root . typst-package\examples\bench.typ 41 | Time (mean ± σ): 687.6 ms ± 20.6 ms [User: 154.4 ms, System: 24.8 ms] 42 | Range (min … max): 668.2 ms … 731.7 ms 10 runs 43 | 44 | */ 45 | -------------------------------------------------------------------------------- /packages/mitex/examples/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitex-rs/mitex/5fc83b64ab5e0b91918528ef2987037866e24086/packages/mitex/examples/example.png -------------------------------------------------------------------------------- /packages/mitex/examples/example.typ: -------------------------------------------------------------------------------- 1 | #import "../lib.typ": * 2 | 3 | #set page(width: 500pt, height: auto, margin: 1em) 4 | 5 | Write inline equations like #mi(`x`). 6 | 7 | Also block equations (this case is from #text(blue.lighten(20%), link("https://katex.org/")[katex.org])): 8 | 9 | #mitex(```latex 10 | \newcommand{\f}[2]{#1f(#2)} 11 | \f\relax{x} = \int_{-\infty}^\infty 12 | \f\hat\xi\,e^{2 \pi i \xi x} 13 | \,d\xi 14 | ```) 15 | 16 | We also support text mode (in development): 17 | 18 | #mitext(```latex 19 | \iftypst 20 | #set math.equation(numbering: "(1)", supplement: "Equation") 21 | \fi 22 | 23 | \section{Title} 24 | 25 | A \textbf{strong} text, a \emph{emph} text and inline equation $x + y$. 26 | 27 | Also block \eqref{eq:pythagoras} and \ref{tab:example}. 28 | 29 | \begin{equation} 30 | a^2 + b^2 = c^2 \label{eq:pythagoras} 31 | \end{equation} 32 | 33 | \begin{table}[ht] 34 | \centering 35 | \begin{tabular}{|c|c|} 36 | \hline 37 | \textbf{Name} & \textbf{Age} \\ 38 | \hline 39 | John & 25 \\ 40 | Jane & 22 \\ 41 | \hline 42 | \end{tabular} 43 | \caption{This is an example table.} 44 | \label{tab:example} 45 | \end{table} 46 | ```) 47 | -------------------------------------------------------------------------------- /packages/mitex/lib.typ: -------------------------------------------------------------------------------- 1 | #import "mitex.typ": mitex-wasm, mitex-convert, mitex-scope, mitex, mitext, mimath, mi -------------------------------------------------------------------------------- /packages/mitex/mitex.typ: -------------------------------------------------------------------------------- 1 | #import "specs/mod.typ": mitex-scope 2 | #let mitex-wasm = plugin("./mitex.wasm") 3 | 4 | #let get-elem-text(it) = { 5 | { 6 | if type(it) == str { 7 | it 8 | } else if type(it) == content and it.has("text") { 9 | it.text 10 | } else { 11 | panic("Unsupported type: " + str(type(it))) 12 | } 13 | } 14 | } 15 | 16 | #let mitex-convert(it, mode: "math", spec: bytes(())) = { 17 | if mode == "math" { 18 | str(mitex-wasm.convert_math(bytes(get-elem-text(it)), spec)) 19 | } else { 20 | str(mitex-wasm.convert_text(bytes(get-elem-text(it)), spec)) 21 | } 22 | } 23 | 24 | // Math Mode 25 | #let mimath(it, block: true, ..args) = { 26 | let res = mitex-convert(mode: "math", it) 27 | let eval-res = eval("$" + res + "$", scope: mitex-scope) 28 | math.equation(block: block, eval-res, ..args) 29 | } 30 | 31 | // Text Mode 32 | #let mitext(it) = { 33 | let res = mitex-convert(mode: "text", it) 34 | eval(res, mode: "markup", scope: mitex-scope) 35 | } 36 | 37 | #let mitex(it, mode: "math", ..args) = { 38 | if mode == "math" { 39 | mimath(it, ..args) 40 | } else { 41 | mitext(it, ..args) 42 | } 43 | } 44 | 45 | #let mi = mimath.with(block: false) 46 | -------------------------------------------------------------------------------- /packages/mitex/specs/mod.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "prelude.typ": * 3 | #import "latex/standard.typ": package as latex-std 4 | 5 | // 1. import all the packages and form a mitex-scope for mitex to use 6 | #let packages = (latex-std,) 7 | #let mitex-scope = packages.map(pkg => pkg.scope).sum() 8 | 9 | // 2. export all packages with specs by metadata and label, 10 | // mitex-cli can fetch them by 11 | // `typst query --root . ./packages/mitex/specs/mod.typ ""` 12 | #metadata(packages) 13 | -------------------------------------------------------------------------------- /packages/mitex/typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitex" 3 | version = "0.2.5" 4 | entrypoint = "lib.typ" 5 | authors = ["Myriad-Dreamin", "OrangeX4", "Enter-tainer"] 6 | license = "Apache-2.0" 7 | description = "LaTeX support for Typst, powered by Rust and WASM." 8 | 9 | homepage = "https://github.com/mitex-rs/mitex" 10 | repository = "https://github.com/mitex-rs/mitex" 11 | categories = ["utility"] 12 | keywords = ["wasm", "rust", "LaTeX", "equation"] 13 | exclude = ["examples"] -------------------------------------------------------------------------------- /packages/typst-dom/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .yarn-metadata.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /packages/typst-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typst-dom", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build && tsc", 9 | "preview": "vite preview", 10 | "test": "vitest", 11 | "coverage": "vitest run --coverage", 12 | "link:local": "yarn link @myriaddreamin/typst.ts @myriaddreamin/typst-ts-renderer" 13 | }, 14 | "peerDependencies": { 15 | "@myriaddreamin/typst-ts-renderer": "0.4.2-rc4", 16 | "@myriaddreamin/typst.ts": "0.4.2-rc4" 17 | }, 18 | "devDependencies": { 19 | "@myriaddreamin/typst-ts-renderer": "0.4.2-rc4", 20 | "@myriaddreamin/typst.ts": "0.4.2-rc4", 21 | "typescript": "^5.0.2", 22 | "vite": "^4.3.9", 23 | "vite-plugin-singlefile": "^0.13.5", 24 | "vite-plugin-wasm": "^3.2.2", 25 | "vitest": "^0.32.2" 26 | }, 27 | "exports": { 28 | "./*": "./dist/esm/*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/typst-dom/src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | initTypstSvg(docRoot: SVGElement): void; 3 | handleTypstLocation(elem: Element, page: number, x: number, y: number); 4 | typstWebsocket: WebSocket; 5 | } 6 | const acquireVsCodeApi: any; 7 | -------------------------------------------------------------------------------- /packages/typst-dom/src/index.mts: -------------------------------------------------------------------------------- 1 | export * from './typst-doc.mjs'; -------------------------------------------------------------------------------- /packages/typst-dom/src/traits/base.mts: -------------------------------------------------------------------------------- 1 | import type { RenderSession } from "@myriaddreamin/typst.ts/dist/esm/renderer.mjs"; 2 | import { ContainerDOMState } from "../typst-doc.mjs"; 3 | 4 | 5 | export type GConstructor = new (...args: any[]) => T; 6 | 7 | interface TypstDocumentFacade { 8 | rescale(): void; 9 | } 10 | 11 | class TypstDocumentContext { 12 | public hookedElem: HTMLElement; 13 | public kModule: RenderSession; 14 | public opts: any; 15 | public modes: [string, TypstDocumentFacade][] = []; 16 | 17 | 18 | /// Cache fields 19 | 20 | /// cached state of container, default to retrieve state from `this.hookedElem` 21 | cachedDOMState: ContainerDOMState = { 22 | width: 0, 23 | height: 0, 24 | boundingRect: { 25 | left: 0, 26 | top: 0, 27 | }, 28 | }; 29 | 30 | constructor(opts: { hookedElem: HTMLElement, kModule: RenderSession }) { 31 | this.hookedElem = opts.hookedElem; 32 | this.kModule = opts.kModule; 33 | this.opts = opts; 34 | } 35 | 36 | static derive(ctx: any, mode: string) { 37 | return ['rescale'].reduce((acc: any, x: string) => { acc[x] = ctx[`${x}$${mode}`]; return acc; }, {} as TypstDocumentFacade); 38 | } 39 | 40 | registerMode(mode: any) { 41 | this.modes.push([mode, TypstDocumentContext.derive(this, mode)]); 42 | } 43 | } 44 | 45 | interface TypstCanvasDocument { 46 | renderCanvas(): any; 47 | } 48 | 49 | function provideCanvas>(Base: TBase) 50 | : TBase & GConstructor { 51 | return class extends Base { 52 | 53 | constructor(...args: any[]) { 54 | super(...args); 55 | this.registerMode("canvas"); 56 | } 57 | 58 | renderCanvas() { 59 | } 60 | 61 | rescale$canvas() { 62 | // get dom state from cache, so we are free from layout reflowing 63 | // Note: one should retrieve dom state before rescale 64 | // const { width: cwRaw, height: ch } = this.cachedDOMState; 65 | // const cw = (this.isContentPreview ? (cwRaw - 10) : cwRaw); 66 | 67 | // // get dom state from cache, so we are free from layout reflowing 68 | // const docDiv = this.hookedElem.firstElementChild! as HTMLDivElement; 69 | // if (!docDiv) { 70 | // return; 71 | // } 72 | 73 | // let isFirst = true; 74 | 75 | // const rescale = (canvasContainer: HTMLElement) => { 76 | // // console.log(ch); 77 | // // if (isFirst) { 78 | // // isFirst = false; 79 | // // canvasContainer.style.marginTop = `0px`; 80 | // // } else { 81 | // // canvasContainer.style.marginTop = `${this.isContentPreview ? 6 : 5}px`; 82 | // // } 83 | // let elem = canvasContainer.firstElementChild as HTMLDivElement; 84 | 85 | // const canvasWidth = Number.parseFloat(elem.getAttribute("data-page-width")!); 86 | // const canvasHeight = Number.parseFloat(elem.getAttribute("data-page-height")!); 87 | 88 | // this.currentRealScale = 89 | // this.previewMode === PreviewMode.Slide ? 90 | // Math.min(cw / canvasWidth, ch / canvasHeight) : 91 | // cw / canvasWidth; 92 | // const scale = this.currentRealScale * this.currentScaleRatio; 93 | 94 | // // apply scale 95 | // const appliedScale = (scale / this.pixelPerPt).toString(); 96 | 97 | 98 | // // set data applied width and height to memoize change 99 | // if (elem.getAttribute("data-applied-scale") !== appliedScale) { 100 | // elem.setAttribute("data-applied-scale", appliedScale); 101 | // // apply translate 102 | // const scaledWidth = Math.ceil(canvasWidth * scale); 103 | // const scaledHeight = Math.ceil(canvasHeight * scale); 104 | 105 | // elem.style.width = `${scaledWidth}px`; 106 | // elem.style.height = `${scaledHeight}px`; 107 | // elem.style.transform = `scale(${appliedScale})`; 108 | 109 | // if (this.previewMode === PreviewMode.Slide) { 110 | 111 | // const widthAdjust = Math.max((cw - scaledWidth) / 2, 0); 112 | // const heightAdjust = Math.max((ch - scaledHeight) / 2, 0); 113 | // docDiv.style.transform = `translate(${widthAdjust}px, ${heightAdjust}px)`; 114 | // } 115 | // } 116 | // } 117 | 118 | // if (this.isContentPreview) { 119 | // isFirst = false; 120 | // const rescaleChildren = (elem: HTMLElement) => { 121 | // for (const ch of elem.children) { 122 | // let canvasContainer = ch as HTMLElement; 123 | // if (canvasContainer.classList.contains('typst-page')) { 124 | // rescale(canvasContainer); 125 | // } 126 | // if (canvasContainer.classList.contains('typst-outline')) { 127 | // rescaleChildren(canvasContainer); 128 | // } 129 | // } 130 | // } 131 | 132 | // rescaleChildren(docDiv); 133 | // } else { 134 | // for (const ch of docDiv.children) { 135 | // let canvasContainer = ch as HTMLDivElement; 136 | // if (!canvasContainer.classList.contains('typst-page')) { 137 | // continue; 138 | // } 139 | // rescale(canvasContainer); 140 | // } 141 | // } 142 | } 143 | 144 | } 145 | } 146 | 147 | export const traits = { 148 | TypstDocumentContext, 149 | canvas: provideCanvas, 150 | } 151 | 152 | -------------------------------------------------------------------------------- /packages/typst-dom/src/traits/base.test.mts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { GConstructor } from "./base.mjs"; 3 | 4 | class TypstDocument { 5 | doc = 1; 6 | } 7 | 8 | interface TypstSvgDocument { 9 | svgProp(): number; 10 | renderSvg(): number; 11 | } 12 | 13 | interface TypstCanvasDocument { 14 | renderCanvas(): number; 15 | } 16 | 17 | function provideCanvas>(Base: TBase) 18 | : TBase & GConstructor { 19 | return class extends Base { 20 | canvasFeat = 10; 21 | renderCanvas() { 22 | return this.doc + this.canvasFeat * this.svgProp(); 23 | } 24 | } 25 | } 26 | 27 | function provideSvg>(Base: TBase) 28 | : TBase & GConstructor { 29 | return class extends Base { 30 | feat = 100; 31 | svgProp() { 32 | return 5; 33 | } 34 | renderSvg() { 35 | return this.doc + this.feat * this.renderCanvas(); 36 | } 37 | } 38 | } 39 | 40 | describe("mixinClass", () => { 41 | it("doMixin", () => { 42 | const T = provideSvg(provideCanvas(TypstDocument as GConstructor)); 43 | const t = new T(); 44 | expect(t.renderCanvas()).toBe(51); 45 | expect(t.renderSvg()).toBe(5101); 46 | }); 47 | }); -------------------------------------------------------------------------------- /packages/typst-dom/src/typst-animation.mts: -------------------------------------------------------------------------------- 1 | export function triggerRipple( 2 | docRoot: Element, 3 | left: number, 4 | top: number, 5 | className: string, 6 | animation: string, 7 | color?: string 8 | ) { 9 | const ripple = document.createElement("div"); 10 | 11 | ripple.className = className; 12 | ripple.style.left = left.toString() + "px"; 13 | ripple.style.top = top.toString() + "px"; 14 | 15 | if (color) { 16 | ripple.style.border = `1px solid ${color}`; 17 | } 18 | 19 | docRoot.appendChild(ripple); 20 | 21 | ripple.style.animation = animation; 22 | ripple.onanimationend = () => { 23 | docRoot.removeChild(ripple); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/typst-dom/src/typst-debug-info.mts: -------------------------------------------------------------------------------- 1 | import { triggerRipple } from "./typst-animation.mjs"; 2 | 3 | const enum SourceMappingType { 4 | Text = 0, 5 | Group = 1, 6 | Image = 2, 7 | Shape = 3, 8 | Page = 4, 9 | } 10 | 11 | // one-of following classes must be present: 12 | // - typst-page 13 | // - typst-group 14 | // - typst-text 15 | // - typst-image 16 | // - typst-shape 17 | const CssClassToType = [ 18 | ["typst-text", SourceMappingType.Text], 19 | ["typst-group", SourceMappingType.Group], 20 | ["typst-image", SourceMappingType.Image], 21 | ["typst-shape", SourceMappingType.Shape], 22 | ["typst-page", SourceMappingType.Page], 23 | ] as const; 24 | 25 | function castToSourceMappingElement( 26 | elem: Element 27 | ): [SourceMappingType, Element] | undefined { 28 | if (elem.classList.length === 0) { 29 | return undefined; 30 | } 31 | for (const [cls, ty] of CssClassToType) { 32 | if (elem.classList.contains(cls)) { 33 | return [ty, elem]; 34 | } 35 | } 36 | return undefined; 37 | } 38 | 39 | function castToNestSourceMappingElement( 40 | elem: Element 41 | ): [SourceMappingType, Element] | undefined { 42 | while (elem) { 43 | const result = castToSourceMappingElement(elem); 44 | if (result) { 45 | return result; 46 | } 47 | let chs = elem.children; 48 | if (chs.length !== 1) { 49 | return undefined; 50 | } 51 | elem = chs[0]; 52 | } 53 | 54 | return undefined; 55 | } 56 | 57 | function castChildrenToSourceMappingElement( 58 | elem: Element 59 | ): [SourceMappingType, Element][] { 60 | return Array.from(elem.children) 61 | .map(castToNestSourceMappingElement) 62 | .filter((x) => x) as [SourceMappingType, Element][]; 63 | } 64 | 65 | export function removeSourceMappingHandler(docRoot: HTMLElement) { 66 | const prevSourceMappingHandler = (docRoot as any).sourceMappingHandler; 67 | if (prevSourceMappingHandler) { 68 | docRoot.removeEventListener("click", prevSourceMappingHandler); 69 | delete (docRoot as any).sourceMappingHandler; 70 | // console.log("remove removeSourceMappingHandler"); 71 | } 72 | } 73 | 74 | function findIndexOfChild(elem: Element, child: Element) { 75 | const children = castChildrenToSourceMappingElement(elem); 76 | console.log(elem, "::", children, "=>", child); 77 | return children.findIndex((x) => x[1] === child); 78 | } 79 | 80 | export function installEditorJumpToHandler(svgDoc: any, docRoot: HTMLElement) { 81 | void castChildrenToSourceMappingElement; 82 | 83 | const findSourceLocation = (elem: Element) => { 84 | const visitChain: [SourceMappingType, Element][] = []; 85 | while (elem) { 86 | let srcElem = castToSourceMappingElement(elem); 87 | if (srcElem) { 88 | visitChain.push(srcElem); 89 | } 90 | if (elem === docRoot) { 91 | break; 92 | } 93 | elem = elem.parentElement!; 94 | } 95 | 96 | if (visitChain.length === 0) { 97 | return undefined; 98 | } 99 | 100 | for (let idx = 1; idx < visitChain.length; idx++) { 101 | const childIdx = findIndexOfChild( 102 | visitChain[idx][1], 103 | visitChain[idx - 1][1] 104 | ); 105 | if (childIdx < 0) { 106 | return undefined; 107 | } 108 | (visitChain[idx - 1][1] as any) = childIdx; 109 | } 110 | 111 | visitChain.reverse(); 112 | 113 | const pg = visitChain[0]; 114 | if (pg[0] !== SourceMappingType.Page) { 115 | return undefined; 116 | } 117 | const childIdx = findIndexOfChild(pg[1].parentElement!, visitChain[0][1]); 118 | if (childIdx < 0) { 119 | return undefined; 120 | } 121 | (visitChain[0][1] as any) = childIdx; 122 | 123 | const sourceNodePath = visitChain.flat(); 124 | 125 | // The page always shadowed by group, so we remove it. 126 | // todo: where should I remove group under page? Putting here is a bit magical. 127 | sourceNodePath.splice(2, 2); 128 | console.log(sourceNodePath); 129 | 130 | return svgDoc.get_source_loc(sourceNodePath); 131 | }; 132 | 133 | removeSourceMappingHandler(docRoot); 134 | const sourceMappingHandler = ((docRoot as any).sourceMappingHandler = ( 135 | event: MouseEvent 136 | ) => { 137 | let elem = event.target! as Element; 138 | 139 | const sourceLoc = findSourceLocation(elem); 140 | if (!sourceLoc) { 141 | return; 142 | } 143 | console.log("source location", sourceLoc); 144 | 145 | const triggerWindow = document.body || document.firstElementChild; 146 | const basePos = triggerWindow.getBoundingClientRect(); 147 | 148 | // const vw = window.innerWidth || 0; 149 | const left = event.clientX - basePos.left; 150 | const top = event.clientY - basePos.top; 151 | 152 | triggerRipple( 153 | triggerWindow, 154 | left, 155 | top, 156 | "typst-debug-react-ripple", 157 | "typst-debug-react-ripple-effect .4s linear" 158 | ); 159 | 160 | window.typstWebsocket.send(`srclocation ${sourceLoc}`); 161 | return; 162 | }); 163 | 164 | docRoot.addEventListener("click", sourceMappingHandler); 165 | } 166 | -------------------------------------------------------------------------------- /packages/typst-dom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "outDir": "dist/esm", 8 | "declaration": true, 9 | "lib": ["es2022", "DOM", "DOM.Iterable"], 10 | "skipLibCheck": true, 11 | "types": ["./src/global.d.ts"], 12 | 13 | /* Bundler mode */ 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/typst-dom/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { viteSingleFile } from "vite-plugin-singlefile"; 3 | 4 | export default defineConfig({ 5 | plugins: [viteSingleFile()], 6 | build: { minify: true, lib: { entry: "src/index.mts", name: "typst-dom" } }, 7 | }); 8 | -------------------------------------------------------------------------------- /scripts/bench.ps1: -------------------------------------------------------------------------------- 1 | hyperfine.exe 'typst compile --root . packages\mitex\examples\bench.typ' --warmup 10 -------------------------------------------------------------------------------- /scripts/build.ps1: -------------------------------------------------------------------------------- 1 | cargo build --release --target wasm32-unknown-unknown --manifest-path ./crates/mitex-wasm/Cargo.toml --features typst-plugin 2 | $InstallPath = "packages/mitex/mitex.wasm" 3 | if (Test-Path $InstallPath) { 4 | Remove-Item $InstallPath 5 | } 6 | Move-Item target/wasm32-unknown-unknown/release/mitex_wasm.wasm $InstallPath 7 | 8 | pwsh -Command { Set-Location crates/mitex-wasm; wasm-pack build --release --features web } 9 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | cargo build --release --target wasm32-unknown-unknown --manifest-path ./crates/mitex-wasm/Cargo.toml --features typst-plugin 4 | cp target/wasm32-unknown-unknown/release/mitex_wasm.wasm packages/mitex/mitex.wasm 5 | 6 | pushd crates/mitex-wasm 7 | wasm-pack build --release --features web 8 | popd 9 | -------------------------------------------------------------------------------- /scripts/publish.ps1: -------------------------------------------------------------------------------- 1 | cargo publish -p mitex-glob 2 | cargo publish -p mitex-spec 3 | cargo publish -p mitex-spec-gen 4 | cargo publish -p mitex-lexer 5 | cargo publish -p mitex-parser 6 | cargo publish -p mitex 7 | # cargo publish -p mitex-wasm 8 | cargo publish -p mitex-cli 9 | --------------------------------------------------------------------------------