Repository: lzanini/mdbook-katex Branch: master Commit: fc5bab43e77f Files: 22 Total size: 74.8 KB Directory structure: gitextract_9swmyqjt/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── deploy.yml │ ├── release-plz.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md └── src/ ├── cfg.rs ├── escape.rs ├── lib.rs ├── main.rs ├── preprocess.rs ├── render/ │ ├── cfg.rs │ └── preprocess.rs ├── render.rs ├── scan.rs └── tests/ ├── escape.rs ├── mod.rs ├── render/ │ └── not_duktape.rs └── render.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/deploy.yml ================================================ name: deploy on: push: tags: - "v*.*.*" release: types: [published] workflow_dispatch: # Manual trigger. jobs: msvc-windows-binary: runs-on: windows-latest env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - uses: actions/checkout@v3 - name: Install stable uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable target: x86_64-pc-windows-msvc override: true - uses: Swatinem/rust-cache@v2 - name: Build mdbook-katex run: | cargo build --no-default-features --features duktape --release - name: Get the version shell: bash id: tagName run: | VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) echo "::set-output name=version::$VERSION" - name: Create zip run: | $ZIP_PREFIX = "mdbook-katex-v${{ steps.tagName.outputs.version }}" 7z a "$ZIP_PREFIX-x86_64-pc-windows-msvc.zip" ` "./target/release/mdbook-katex.exe" - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: x86_64-pc-windows path: mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-pc-windows-msvc.zip gnu-windows-binary: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Install x86_64-pc-windows-gnu uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable target: x86_64-pc-windows-gnu override: true - uses: Swatinem/rust-cache@v2 - uses: actions-rs/cargo@v1 with: use-cross: true command: build args: | --release --target x86_64-pc-windows-gnu - name: Get the version id: tagName run: | VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) echo "::set-output name=version::$VERSION" - name: Create tar run: | mv target/x86_64-pc-windows-gnu/release/mdbook-katex.exe mdbook-katex.exe TAR_FILE=mdbook-katex-v${{ steps.tagName.outputs.version }} zip $TAR_FILE-x86_64-pc-windows-gnu.zip \ mdbook-katex.exe - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: x86_64-pc-windows-gnu path: | mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-pc-windows-gnu.zip gnu-linux-binary: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Install x86_64-unknown-linux-gnu target uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable target: x86_64-unknown-linux-gnu override: true - uses: Swatinem/rust-cache@v2 - name: Build mbdook-katex run: | cargo build --release - name: Get the version id: tagName run: | VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) echo "::set-output name=version::$VERSION" - name: Create tar run: | mv target/release/mdbook-katex mdbook-katex TAR_FILE=mdbook-katex-v${{ steps.tagName.outputs.version }} tar -czvf $TAR_FILE-x86_64-unknown-linux-gnu.tar.gz \ mdbook-katex - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: x86_64-unknown-linux-gnu path: | Cargo.lock mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-unknown-linux-gnu.tar.gz musl-linux-binary: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Install x86_64-unknown-linux-musl uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable target: x86_64-unknown-linux-musl override: true - uses: Swatinem/rust-cache@v2 - uses: actions-rs/cargo@v1 with: use-cross: true command: build args: | --release --target x86_64-unknown-linux-musl - name: Get the version id: tagName run: | VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) echo "::set-output name=version::$VERSION" - name: Create tar run: | mv target/x86_64-unknown-linux-musl/release/mdbook-katex mdbook-katex TAR_FILE=mdbook-katex-v${{ steps.tagName.outputs.version }} tar -czvf $TAR_FILE-x86_64-unknown-linux-musl.tar.gz \ mdbook-katex - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: x86_64-unknown-linux-musl path: | mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-unknown-linux-musl.tar.gz x86_64-macos-binary: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Install stable-x86_64-apple-darwin uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable target: x86_64-apple-darwin override: true - uses: Swatinem/rust-cache@v2 - name: Build mdbook-katex for x86_64-apple-darwin run: | cargo build --release - name: Get the version id: tagName run: | VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) echo "::set-output name=version::$VERSION" - name: Create tar for x86_64-apple-darwin run: | mv target/release/mdbook-katex mdbook-katex TAR_PREFIX=mdbook-katex-v${{ steps.tagName.outputs.version }} tar -czvf $TAR_PREFIX-x86_64-apple-darwin.tar.gz \ mdbook-katex - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: x86_64-apple-darwin path: | mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-apple-darwin.tar.gz aarch64-macos-binary: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Install stable-aarch64-apple-darwin uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable target: aarch64-apple-darwin override: true - uses: Swatinem/rust-cache@v2 - name: Cross build mdbook-katex for aarch64-apple-darwin run: | cargo build --release --target=aarch64-apple-darwin - name: Get the version id: tagName run: | VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) echo "::set-output name=version::$VERSION" - name: Create tar for aarch64-apple-darwin run: | mv target/aarch64-apple-darwin/release/mdbook-katex mdbook-katex TAR_PREFIX=mdbook-katex-v${{ steps.tagName.outputs.version }} tar -czvf $TAR_PREFIX-aarch64-apple-darwin.tar.gz \ mdbook-katex - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: aarch64-apple-darwin path: | mdbook-katex-v${{ steps.tagName.outputs.version }}-aarch64-apple-darwin.tar.gz deploy: needs: [ msvc-windows-binary, gnu-windows-binary, gnu-linux-binary, musl-linux-binary, x86_64-macos-binary, aarch64-macos-binary, ] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Download artifacts uses: actions/download-artifact@v4 with: pattern: "*" merge-multiple: true - name: Install Rust stable uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Get the version id: tagName run: | VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) echo "::set-output name=version::$VERSION" - name: Create a release uses: softprops/action-gh-release@v1 with: name: v${{ steps.tagName.outputs.version }}-binaries files: | Cargo.lock mdbook-katex-v${{ steps.tagName.outputs.version }}-aarch64-apple-darwin.tar.gz mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-apple-darwin.tar.gz mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-pc-windows-msvc.zip mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-pc-windows-gnu.zip mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-unknown-linux-gnu.tar.gz mdbook-katex-v${{ steps.tagName.outputs.version }}-x86_64-unknown-linux-musl.tar.gz tag_name: ${{ steps.tagName.outputs.version }}-binaries env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release-plz.yml ================================================ name: Release-plz permissions: pull-requests: write contents: write on: push: branches: - master paths: - '.github/workflows/release-plz.yml' - '**.rs' - '**Cargo.toml' - '**CHANGELOG.md' jobs: release-plz: name: Release-plz runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Run release-plz uses: MarcoIeni/release-plz-action@v0.5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_CREDENTIALS }} with: project_manifest: Cargo.toml ================================================ FILE: .github/workflows/test.yml ================================================ name: test-ci on: push: pull_request: jobs: test-musl: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: toolchain: stable target: x86_64-unknown-linux-musl override: true - uses: Swatinem/rust-cache@v2 - uses: actions-rs/cargo@v1 with: use-cross: true command: test args: | --target x86_64-unknown-linux-musl test-ubuntu: runs-on: ubuntu-22.04 strategy: matrix: rust: - stable - beta - nightly steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true - uses: Swatinem/rust-cache@v2 - run: cargo test test-macos: runs-on: macos-latest steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: Swatinem/rust-cache@v2 - run: cargo test test-windows: runs-on: windows-latest steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable target: x86_64-pc-windows-msvc override: true - uses: Swatinem/rust-cache@v2 - run: cargo test --no-default-features --features duktape test-windows-from-ubuntu: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: toolchain: stable target: x86_64-pc-windows-gnu override: true - uses: Swatinem/rust-cache@v2 - uses: actions-rs/cargo@v1 with: use-cross: true command: test args: | --target x86_64-pc-windows-gnu --no-run # Cannot run because not on Windows. fmt-clippy: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 - name: check formatting run: cargo fmt -- --check - name: clippy run: RUSTFLAGS="-Dwarnings" cargo clippy - name: check Cargo.lock run: cargo update --locked ================================================ FILE: .gitignore ================================================ target/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.10.0](https://github.com/lzanini/mdbook-katex/compare/v0.9.4...v0.10.0) - 2025-11-28 ### Other - update dependencies: toml to v0.9, rayon to 1.11 - use mdbook-preprocessor v0.5.1 instead of mdbook_fork4ls (a fork of mdbook v0.4, [#131](https://github.com/lzanini/mdbook-katex/pull/131)) - allow use of different js renderers of katex through features `quick-js` and `duktape` - changed default features to `quick-js` ## [0.9.4](https://github.com/lzanini/mdbook-katex/compare/v0.9.3...v0.9.4) - 2025-05-01 ### Other - make clippy happy;update dependencies - use mdbook_fork4ls v0.4.48 ## [0.9.3](https://github.com/lzanini/mdbook-katex/compare/v0.9.2...v0.9.3) - 2025-02-18 ### Other - use mdbook_fork4ls v0.4.45 & its builtin parallelization ## [0.9.2](https://github.com/lzanini/mdbook-katex/compare/v0.9.1...v0.9.2) - 2024-12-07 ### Other - make new clippy happy - update dependency mdbook to v0.4.43;update other dependencies - attempt to fix upload-artifact not allowing reused name - bump download-artifact as well - bump upload-artifact to v4 so deployment runs ## [0.9.1](https://github.com/lzanini/mdbook-katex/compare/v0.9.0...v0.9.1) - 2024-11-07 ### Fixed - fix deploy CI artifact names ### Other - make clippy happy - switch to mdbook_fork4ls v0.4.41; update dependencies - deploy CI explicit release tag - different tag for deploy than release - deploy CI does not publish on crates.io - allow manually trigger deploy CI ## [0.9.0](https://github.com/lzanini/mdbook-katex/compare/v0.8.1...v0.9.0) - 2024-05-23 ### Fixed - fix&enhance tracing subscriber output ### Other - update dependencies - use tracing - print render error&restore delimiter - build release binary on release-plz ================================================ FILE: Cargo.toml ================================================ [package] name = "mdbook-katex" version = "0.10.0-alpha" authors = [ "Lucas Zanini ", "Steven Hé (Sīchàng) ", ] edition = "2021" description = "mdBook preprocessor rendering LaTeX equations to HTML." license = "MIT" readme = "README.md" repository = "https://github.com/lzanini/mdbook-katex" [dependencies] clap = { version = "4.5.53", features = ["cargo"] } mdbook-preprocessor = {version = "0.5.1"} serde = "1.0.228" serde_derive = "1.0" serde_json = "1.0.145" toml = "0.9.8" tracing = { version = "0.1.41", default-features = false, features = [ "attributes", ] } tracing-subscriber = { version = "0.3.20", default-features = false, features = [ "ansi", "env-filter", "fmt", ] } rayon = "1.11" katex = { version = "0.4.6", default-features = false, optional = true } [features] default = ["quick-js"] pre-render = ["dep:katex"] quick-js = ["pre-render", "katex/quick-js"] duktape = ["pre-render", "katex/duktape"] [profile.release] opt-level = "z" lto = true ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Lucas Zanini Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # mdBook-KaTeX [![Crates.io version](https://img.shields.io/crates/v/mdbook-katex)](https://crates.io/crates/mdbook-katex) ![Crates.io downloads](https://img.shields.io/crates/d/mdbook-katex) mdBook-KaTeX is a preprocessor for [mdBook](https://github.com/rust-lang/mdBook), using KaTeX to render LaTeX math expressions. There are two working modes: - [Pre-render Mode](#pre-render-mode-default) (default): pre-renders math expressions at build time using KaTeX, - no client-side JavaScript required, - very fast page load, - customizable macros and separators. - [Escape mode](#escape-mode-experimental) (experimental): escapes math expressions to be rendered using either katex.js or MathJax in the browser. May be useful if having problems building mdBook-KaTeX with quickjs. Pre-rendering uses [the katex crate](https://github.com/xu-cheng/katex-rs). [List of LaTeX functions supported by KaTeX](https://katex.org/docs/supported.html).

## Getting Started First, install mdBook-KaTeX ### **Non-Windows** users ```shell cargo install mdbook-katex ``` ### Windows users The recommended way is to download the latest `x86_64-pc-windows-gnu.zip` from [Releases](https://github.com/lzanini/mdbook-katex/releases) for the full functionality. Otherwise, building with the default feature may fail unless you have GCC, and you may only be able to [build with the `duktape` feature](#build-options-features) with limited features. Another way is [Escape mode](#escape-mode-experimental). ### Basic setup Then, add the following line to your `book.toml` file ```toml [preprocessor.katex] after = ["links"] ``` You can now use `$` and `$$` delimiters for inline and display math expressions within your `.md` files. If you need a regular dollar symbol, you need to escape delimiters with a backslash `\$`. ```markdown # Chapter 1 Here is an inline example, $ \pi(\theta) $, an equation, $$ \nabla f(x) \in \mathbb{R}^n, $$ and a regular \$ symbol. ``` Math expressions will be rendered as HTML when running `mdbook build` or `mdbook serve` as usual. ## Pre-render mode (default) Pre-rendering uses [the katex crate](https://github.com/xu-cheng/katex-rs). [List of LaTeX functions supported by KaTeX](https://katex.org/docs/supported.html). ### KaTeX options Most [KaTeX options](https://katex.org/docs/options.html) are supported via the `katex` crate. Specify these options under `[preprocessor.katex]` in your `book.toml`: | Argument | Type | | :-------------------------------------------------------------------------------------------------- | :----------------------------------------- | | [`output`](https://katex.org/docs/options.html#:~:text=default-,output,-string) | `"html"`, `"mathml"`, or `"htmlAndMathml"` | | [`leqno`](https://katex.org/docs/options.html#:~:text=default-,leqno,-boolean) | `boolean` | | [`fleqn`](https://katex.org/docs/options.html#:~:text=LaTeX-,fleqn,-boolean) | `boolean` | | [`throw-on-error`](https://katex.org/docs/options.html#:~:text=package-,throwonerror,-boolean) | `boolean` | | [`error-color`](https://katex.org/docs/options.html#:~:text=errorColor-,errorcolor,-string) | `string` | | [`min-rule-thickness`](https://katex.org/docs/options.html#:~:text=state-,minrulethickness,-number) | `number` | | [`max-size`](https://katex.org/docs/options.html#:~:text=true-,maxsize,-number) | `number` | | [`max-expand`](https://katex.org/docs/options.html#:~:text=maxexpand) | `number` | | [`trust`](https://katex.org/docs/options.html#:~:text=LaTeX-,trust,-boolean) | `boolean` | There are also extra options to configure the behaviour of the preprocessor: | Option | Description | | :----------------- | :-------------------------------------------------------------------------------------------------------- | | `no-css` | Do not inject KaTeX stylesheet link (See [Self-host KaTeX CSS and fonts](#self-host-katex-css-and-fonts)) | | `macros` | Path to macros file (see [Custom macros](#custom-macros)) | | `include-src` | Include math expressions source code (See [Including math Source](#including-math-source)) | | `block-delimiter` | See [Custom delimiter](#custom-delimiter) | | `inline-delimiter` | See [Custom delimiter](#custom-delimiter) | | `pre-render` | See [Escape mode](#escape-mode-experimental) | For example, the default configuration: ```toml [preprocessor.katex] after = ["links"] # KaTeX options. output = "html" leqno = false fleqn = false throw-on-error = true error-color = "#cc0000" min-rule-thickness = -1.0 max-size = "Infinity" max-expand = 1000 trust = false # Extra options. no-css = false include-src = false block-delimiter = { left = "$$", right = "$$" } inline-delimiter = { left = "$", right = "$" } pre-render = true ``` ### Self-host KaTeX CSS and fonts KaTeX requires a stylesheet and fonts to render correctly. By default, mdBook-KaTeX injects a KaTeX stylesheet link pointing to a CDN. If you want to self-host the CSS and fonts instead, you should specify in `book.toml`: ```toml [preprocessor.katex] no-css = true ``` and manually add the CSS and fonts to your mdBook project before building it. See [mdBook-KaTeX Static CSS Example](https://github.com/SichangHe/mdbook_katex_static_css) for an automated example. ### Custom macros Custom LaTeX macros must be defined in a `.txt` file, according to the following pattern ```txt \grad:{\nabla} \R:{\mathbb{R}^{#1 \times #2}} ``` You need to specify the path of this file in your `book.toml` as follows ```toml [preprocessor.katex] macros = "path/to/macros.txt" ``` These macros can then be used in your `.md` files ```markdown # Chapter 1 $$ \grad f(x) \in \R{n}{p} $$ ``` ### Including math source This option is added so users can have a convenient way to copy the source code of math expressions when they view the book. When `include-src` is set to `true`, each math block is wrapped within a `` tag with `class="katex-src"` with the included math source code being its `value` attribute. For example, before being fed into mdBook, ```markdown Define $f(x)$: $$ f(x)=x^2\\ x\in\R $$ ``` is preprocessed into (the content of the `katex` `span`s are omitted and represented as `…`) ```markdown Define : ``` The math source code is included in a minimal fashion, and it is up to the users to write custom CSS and JavaScript to make use of it. For more information about adding custom CSS and JavaScript in mdBook, see [additional-css and additional-js](https://rust-lang.github.io/mdBook/format/configuration/renderers.html#html-renderer-options). If you need more information about this feature, please check the issues or file a new issue. ### Custom delimiter To change the delimiters for math expressions, set the `block-delimiter` and `inline-delimiter` under `[preprocessor.katex]`. For example, to use `\(`and `\)` for inline math and `\[` and `\]` for math block, set ```toml [preprocessor.katex] block-delimiter = { left = "\\[", right = "\\]" } inline-delimiter = { left = "\\(", right = "\\)" } ``` Note that the double backslash above are just used to escape `\` in the TOML format. ### Caveats `$\backslash$` does not work, but you can use `$\setminus$` instead. Only the x86_64 Linux, Windows GNU, and macOS builds have full functionality (matrix, ...) , all other builds have compromised capabilities. See [#39](https://github.com/lzanini/mdbook-katex/issues/39) for the reasons. ### Build options (features) Katex supports multiple js backends: `quick-js` (default), `duktape`, and `wasm-js`. It is possible to build mdbook-katex with either `quick-js` (default) and `duktape`. ```shell cargo install mdbook-katex --no-default-features --features duktape ``` Note that, for `duketape`, things such as matrices will not work. See [#67](https://github.com/lzanini/mdbook-katex/issues/67) for the reasons. ## Escape mode (experimental) Escapes the string needed for a formula in advance so that it remains the original formula after the markdown processor. Disable pre-render to use "Escape mode", and provide your client-side rendering library of choice. An example with `katex.js` included in `head.hbs` (see [index.hbs](https://rust-lang.github.io/mdBook/format/theme/index-hbs.html)) is provided below. ```toml [preprocessor.katex] after = ["links"] pre-render = false no-css = true [output.html] theme = "theme" # use theme/head.hbs ``` Note that the [KaTeX Options](#katex-options) are ignored in escape mode. An example `head.hbs`: ```html ``` ================================================ FILE: src/cfg.rs ================================================ //! Configurations for preprocessing KaTeX. use super::*; /// Configuration for KaTeX preprocessor, /// including options for `katex-rs` and feature options. #[derive(Debug, Deserialize, Serialize)] #[serde(default, rename_all = "kebab-case")] pub struct KatexConfig { // options for the katex-rust crate /// KaTeX output type. pub output: String, /// Whether to have `\tags` rendered on the left instead of the right. pub leqno: bool, /// Whether to make display math flush left. pub fleqn: bool, /// Whether to let KaTeX throw a ParseError for invalid LaTeX. pub throw_on_error: bool, /// Color used for invalid LaTeX. pub error_color: String, /// Specifies a minimum thickness, in ems. pub min_rule_thickness: f64, /// Max size for user-specified sizes. pub max_size: f64, /// Limit the number of macro expansions to the specified number. pub max_expand: i32, /// Whether to trust users' input. pub trust: bool, // other options /// Do not inject KaTeX CSS headers. pub no_css: bool, /// Include math source in rendered HTML. pub include_src: bool, /// Path to macro file. pub macros: Option, /// Delimiter for math display block. pub block_delimiter: Delimiter, /// Delimiter for math inline block. pub inline_delimiter: Delimiter, /// Use katex.rs to pre-render math equations. pub pre_render: bool, } impl Default for KatexConfig { fn default() -> KatexConfig { KatexConfig { // default options for the katex-rust crate // uses defaults specified in: https://katex.org/docs/options.html output: "html".into(), leqno: false, fleqn: false, throw_on_error: true, error_color: String::from("#cc0000"), min_rule_thickness: -1.0, max_size: f64::INFINITY, max_expand: 1000, trust: false, // other options no_css: false, include_src: false, macros: None, block_delimiter: Delimiter::same("$$".into()), inline_delimiter: Delimiter::same("$".into()), pre_render: true, } } } impl KatexConfig { /// Generate extra options for the preprocessor. pub fn build_extra_opts(&self) -> ExtraOpts { ExtraOpts { include_src: self.include_src, block_delimiter: self.block_delimiter.clone(), inline_delimiter: self.inline_delimiter.clone(), } } } /// Extract configuration for katex preprocessor from `book_cfg`. pub fn get_config( book_cfg: &mdbook_preprocessor::config::Config, ) -> Result { let cfg = match book_cfg .get::("preprocessor.katex") .unwrap_or_default() { Some(raw) => raw.clone().try_into(), None => Ok(KatexConfig::default()), }; cfg.or_else(|_| Ok(KatexConfig::default())) } ================================================ FILE: src/escape.rs ================================================ //! Escaping math blocks to fix KaTeX rendering. use super::*; /// Escape a math block `item` into a delimited string. /// Delimiter also need to be escaped, e.g. `\(,\)` and `\[,\]`. pub fn escape_math_with_delimiter(item: &str, delimiter: &Delimiter) -> String { let mut result = String::new(); escape_math(&delimiter.left, &mut result); escape_math(item, &mut result); escape_math(&delimiter.right, &mut result); result } /// This is a amazing but useful little trick. /// Mdbook's markdown engine will parse a part of KaTeX formula into HTML, e.g. `$[x^n](f + g)$`. /// So if we escape the math formula in advance so that it passes through the markdown /// engine as the original formula, it will be rendered correctly by katex.js. pub fn escape_math(item: &str, result: &mut String) { for c in item.chars() { match c { '_' => { result.push_str("\\_"); } '*' => { result.push_str("\\*"); } '\\' => { result.push_str("\\\\"); } _ => { result.push(c); } } } } ================================================ FILE: src/lib.rs ================================================ #![deny(missing_docs)] //! Preprocess math blocks using KaTeX for mdBook. use std::{ borrow::Cow, collections::HashMap, collections::VecDeque, fs::File, io::{stderr, Read}, path::{Path, PathBuf}, }; use mdbook_preprocessor::{book::Book, errors::Result, Preprocessor, PreprocessorContext}; use rayon::iter::*; use serde_derive::{Deserialize, Serialize}; use tracing::*; use tracing_subscriber::EnvFilter; use { cfg::*, escape::*, preprocess::*, scan::{Event, *}, }; pub mod cfg; pub mod escape; pub mod preprocess; pub mod scan; #[cfg(feature = "pre-render")] pub mod render; #[cfg(feature = "pre-render")] pub use render::*; #[cfg(test)] mod tests; #[doc(hidden)] pub fn init_tracing() { _ = tracing_subscriber::fmt() .with_writer(stderr) .with_ansi(true) .with_env_filter( EnvFilter::builder() .with_default_directive(Level::INFO.into()) .from_env_lossy(), ) .try_init(); } ================================================ FILE: src/main.rs ================================================ use clap::{crate_version, Arg, ArgMatches, Command}; use mdbook_katex::{init_tracing, preprocess::KatexProcessor}; use mdbook_preprocessor::errors::{Error, Result}; use mdbook_preprocessor::{parse_input, Preprocessor}; use std::io; use tracing::*; /// Parse CLI options. pub fn make_app() -> Command { Command::new("mdbook-katex") .version(crate_version!()) .about("A preprocessor that renders KaTex equations to HTML.") .subcommand( Command::new("supports") .arg(Arg::new("renderer").required(true)) .about("Check whether a renderer is supported by this preprocessor"), ) } /// Produce a warning on mdBook version mismatch. fn check_mdbook_version(version: &str) { if version != mdbook_preprocessor::MDBOOK_VERSION { warn!( "This mdbook-katex was built against mdbook v{}, \ but we are being called from mdbook v{version}. \ If you have any issue, this might be a reason.", mdbook_preprocessor::MDBOOK_VERSION, ) } } /// Tell mdBook if we support what it asks for. fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> Result<()> { let renderer = sub_args .get_one::("renderer") .expect("Required argument"); let supported = pre.supports_renderer(renderer).unwrap_or(false); if supported { Ok(()) } else { Err(Error::msg(format!( "The katex preprocessor does not support the '{renderer}' renderer", ))) } } /// Preprocess `book` using `pre` and print it out. fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<()> { let (ctx, book) = parse_input(io::stdin())?; check_mdbook_version(&ctx.mdbook_version); let processed_book = pre.run(&ctx, book)?; serde_json::to_writer(io::stdout(), &processed_book)?; Ok(()) } fn main() -> Result<()> { init_tracing(); // set up app let matches = make_app().get_matches(); let pre = KatexProcessor; // determine what behaviour has been requested if let Some(sub_args) = matches.subcommand_matches("supports") { // handle cmdline supports handle_supports(&pre, sub_args) } else { // handle preprocessing handle_preprocessing(&pre) } } ================================================ FILE: src/preprocess.rs ================================================ //! Preprocessing and escaping with KaTeX. use super::*; /// When `pre-render` is called but not enabled. #[cfg(not(feature = "pre-render"))] pub fn process_all_chapters_prerender( _: &mut Book, _: &KatexConfig, _: &str, _: &PreprocessorContext, ) -> Vec { panic!("Pre-render is unavailable because this `mdbook-katex` program does not have the `pre-render` feature enabled, only escaping mode is available, and you can set `pre-render = false` to enable it. If you do need `pre-render` mode, you need to add the `pre-render` feature and recompile. See the README at .") } /// Header that points to CDN for the KaTeX stylesheet. pub const KATEX_HEADER: &str = r#" "#; /// Extra options for the KaTeX preprocessor. #[derive(Clone, Debug)] pub struct ExtraOpts { /// Path to macro file. pub include_src: bool, /// Delimiter for math display block. pub block_delimiter: Delimiter, /// Delimiter for math inline block. pub inline_delimiter: Delimiter, } /// KaTeX `mdbook::preprocess::Proprecessor` for mdBook. pub struct KatexProcessor; // preprocessor to inject rendered katex blocks and stylesheet impl Preprocessor for KatexProcessor { fn name(&self) -> &str { "katex" } fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result { // parse TOML config let cfg = get_config(&ctx.config)?; let header = if cfg.no_css { "" } else { KATEX_HEADER }.to_owned(); if cfg.pre_render { process_all_chapters_prerender(&mut book, &cfg, &header, ctx); } else { process_all_chapters_escape(&mut book, &cfg, &header, ctx); } Ok(book) } } /// Escape all Katex equations. pub fn process_all_chapters_escape( book: &mut Book, cfg: &KatexConfig, stylesheet_header: &str, _: &PreprocessorContext, ) { let extra_opts = cfg.build_extra_opts(); book.for_each_chapter_mut(|chapter| { chapter.content = process_chapter_escape(&chapter.content, &extra_opts, stylesheet_header); }); } /// Escape Katex equations. pub fn process_chapter_escape( raw_content: &str, extra_opts: &ExtraOpts, stylesheet_header: &str, ) -> String { get_render_tasks(raw_content, stylesheet_header, extra_opts) .into_par_iter() .map(|rend| match rend { Render::Text(t) => t.into(), Render::InlineTask(item) => { escape_math_with_delimiter(item, &extra_opts.inline_delimiter).into() } Render::DisplayTask(item) => { escape_math_with_delimiter(item, &extra_opts.block_delimiter).into() } }) .collect::>>() .join("") } /// A render job for chapter processing. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Render<'a> { /// No need to render. Text(&'a str), /// A render task for a math inline block. InlineTask(&'a str), /// A render task for a math display block. DisplayTask(&'a str), } /// Find all the `Render` tasks in `raw_content`. pub fn get_render_tasks<'a>( raw_content: &'a str, stylesheet_header: &'a str, extra_opts: &ExtraOpts, ) -> Vec> { let scan = Scan::new( raw_content, &extra_opts.block_delimiter, &extra_opts.inline_delimiter, ); let mut rendering = Vec::new(); rendering.push(Render::Text(stylesheet_header)); let mut checkpoint = 0; for event in scan { match event { Event::Begin(begin) => checkpoint = begin, Event::TextEnd(end) => rendering.push(Render::Text(&raw_content[checkpoint..end])), Event::InlineEnd(end) => { rendering.push(Render::InlineTask(&raw_content[checkpoint..end])); checkpoint = end; } Event::BlockEnd(end) => { rendering.push(Render::DisplayTask(&raw_content[checkpoint..end])); checkpoint = end; } } } if raw_content.len() > checkpoint { rendering.push(Render::Text(&raw_content[checkpoint..raw_content.len()])); } rendering } ================================================ FILE: src/render/cfg.rs ================================================ //! Extra configurations for pre-rendering KaTeX. use super::*; impl KatexConfig { /// Configured output type. /// Defaults to `Html`, can also be `Mathml` or `HtmlAndMathml`. pub fn output_type(&self) -> katex::OutputType { match self.output.as_str() { "html" => katex::OutputType::Html, "mathml" => katex::OutputType::Mathml, "htmlAndMathml" => katex::OutputType::HtmlAndMathml, other => { error!( "[preprocessor.katex]: `{other}` is not a valid choice for `output`! Please check your `book.toml`. Defaulting to `html`. Other valid choices for output are `mathml` and `htmlAndMathml`." ); katex::OutputType::Html } } } /// From `root`, load macros and generate configuration options /// `(inline_opts, display_opts)`. pub fn build_opts

(&self, root: P) -> (katex::Opts, katex::Opts) where P: AsRef, { // load macros as a HashMap let macros = load_macros(root, &self.macros); self.build_opts_from_macros(macros) } /// Given `macros`, generate `(inline_opts, display_opts)`. pub fn build_opts_from_macros( &self, macros: HashMap, ) -> (katex::Opts, katex::Opts) { let mut configure_katex_opts = katex::Opts::builder(); configure_katex_opts .output_type(self.output_type()) .leqno(self.leqno) .fleqn(self.fleqn) .throw_on_error(self.throw_on_error) .error_color(self.error_color.clone()) .macros(macros) .min_rule_thickness(self.min_rule_thickness) .max_size(self.max_size) .max_expand(self.max_expand) .trust(self.trust); // inline rendering options let inline_opts = configure_katex_opts .clone() .display_mode(false) .build() .unwrap(); // display rendering options let display_opts = configure_katex_opts.display_mode(true).build().unwrap(); (inline_opts, display_opts) } } /// Load macros from `root`/`macros_path` into a `HashMap`. fn load_macros

(root: P, macros_path: &Option) -> HashMap where P: AsRef, { // load macros as a HashMap let mut map = HashMap::new(); if let Some(path) = get_macro_path(root, macros_path) { let macro_str = load_as_string(&path); for couple in macro_str.split('\n') { // only consider lines starting with a backslash if let Some('\\') = couple.chars().next() { let couple: Vec<&str> = couple.splitn(2, ':').collect(); map.insert(String::from(couple[0]), String::from(couple[1])); } } } map } /// Absolute path of the macro file. pub fn get_macro_path

(root: P, macros_path: &Option) -> Option where P: AsRef, { macros_path .as_ref() .map(|path| root.as_ref().join(PathBuf::from(path))) } /// Read file at `path`. pub fn load_as_string(path: &Path) -> String { let display = path.display(); let mut file = match File::open(path) { Err(why) => panic!("couldn't open {display}: {why}"), Ok(file) => file, }; let mut string = String::new(); if let Err(why) = file.read_to_string(&mut string) { panic!("couldn't read {display}: {why}") }; string } ================================================ FILE: src/render/preprocess.rs ================================================ //! Preprocessing and pre-rendering with KaTeX. use katex::Opts; use super::*; /// Render all Katex equations. pub fn process_all_chapters_prerender( book: &mut Book, cfg: &KatexConfig, stylesheet_header: &str, ctx: &PreprocessorContext, ) { let extra_opts = cfg.build_extra_opts(); let (inline_opts, display_opts) = cfg.build_opts(&ctx.root); book.for_each_chapter_mut(|chapter| { chapter.content = process_chapter_prerender( &chapter.content, inline_opts.clone(), display_opts.clone(), stylesheet_header, &extra_opts, ); }); } /// Render Katex equations in a `Chapter` as HTML, and add the Katex CSS. pub fn process_chapter_prerender( raw_content: &str, inline_opts: Opts, display_opts: Opts, stylesheet_header: &str, extra_opts: &ExtraOpts, ) -> String { get_render_tasks(raw_content, stylesheet_header, extra_opts) .into_par_iter() .map(|rend| match rend { Render::Text(t) => t.into(), Render::InlineTask(item) => { render(item, inline_opts.clone(), extra_opts.clone(), false).into() } Render::DisplayTask(item) => { render(item, display_opts.clone(), extra_opts.clone(), true).into() } }) .collect::>>() .join("") } ================================================ FILE: src/render.rs ================================================ //! Render KaTeX math block to HTML use katex::{Error, Opts}; use super::*; pub use {cfg::*, preprocess::*}; mod cfg; mod preprocess; /// Render a math block `item` into HTML following `opts`. /// Wrap result in `` tag if `extra_opts.include_src`. #[instrument(skip(opts, extra_opts, display))] pub fn render(item: &str, opts: Opts, extra_opts: ExtraOpts, display: bool) -> String { let mut rendered_content = String::new(); // try to render equation match katex::render_with_opts(item, opts) { Ok(rendered) => { let rendered = rendered.replace('\n', " "); if extra_opts.include_src { // Wrap around with `data.katex-src` tag. rendered_content.push_str(r#""#); rendered_content.push_str(&rendered); rendered_content.push_str(r""); } else { rendered_content.push_str(&rendered); } } // if rendering fails, keep the unrendered equation Err(why) => { match why { Error::JsExecError(why) => { warn!("Rendering failed, keeping the original content: {why}") } _ => error!( ?why, "Unexpected rendering failure, keeping the original content." ), } let delimiter = match display { true => &extra_opts.block_delimiter, false => &extra_opts.inline_delimiter, }; rendered_content.push_str(&delimiter.left); rendered_content.push_str(item); rendered_content.push_str(&delimiter.right); } } rendered_content } ================================================ FILE: src/scan.rs ================================================ //! Scan Markdown text and identify math block events. use super::*; /// A pair of strings are delimiters. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Delimiter { /// Left delimiter. pub left: String, /// Right delimiter. pub right: String, } impl Delimiter { /// Same left and right `delimiter`. pub fn same(delimiter: String) -> Self { Self { left: delimiter.clone(), right: delimiter, } } /// The first byte of the left delimiter. pub fn first(&self) -> u8 { self.left.as_bytes()[0] } /// Whether `to_match` matches the left delimiter. pub fn match_left(&self, to_match: &[u8]) -> bool { if self.left.len() > to_match.len() { return false; } for (we, they) in self.left.as_bytes().iter().zip(to_match) { if we != they { return false; } } true } } /// An event for parsing in a Markdown file. #[derive(Debug)] pub enum Event { /// A beginning of text or math block. Begin(usize), /// An end of a text block. TextEnd(usize), /// An end of an inline math block. InlineEnd(usize), /// An end of a display math block. BlockEnd(usize), } /// Scanner for text to identify block and inline math `Event`s. #[derive(Debug)] pub struct Scan<'a> { string: &'a str, bytes: &'a [u8], index: usize, /// Buffer for block and inline math `Event`s. pub events: VecDeque, block_delimiter: &'a Delimiter, inline_delimiter: &'a Delimiter, } impl Iterator for Scan<'_> { type Item = Event; fn next(&mut self) -> Option { loop { match self.events.pop_front() { Some(item) => return Some(item), None => self.process_byte().ok()?, } } } } impl<'a> Scan<'a> { /// Set up a `Scan` for `string` with given delimiters. pub fn new( string: &'a str, block_delimiter: &'a Delimiter, inline_delimiter: &'a Delimiter, ) -> Self { Self { string, bytes: string.as_bytes(), index: 0, events: VecDeque::new(), block_delimiter, inline_delimiter, } } /// Scan, identify and store all `Event`s in `self.events`. pub fn run(&mut self) { while let Ok(()) = self.process_byte() {} } /// Get byte currently pointed to. Returns `Err(())` if out of bound. fn get_byte(&self) -> Result { self.bytes.get(self.index).map(|b| b.to_owned()).ok_or(()) } /// Increment index. fn inc(&mut self) { self.index += 1; } /// Scan one byte, proceed process based on the byte. /// - Start of delimiter => call `process_delimit`. /// - `\` => skip one byte. /// - `` ` `` => call `process_backtick`. /// Return `Err(())` if no more bytes to process. fn process_byte(&mut self) -> Result<(), ()> { let byte = self.get_byte()?; self.inc(); match byte { b if b == self.block_delimiter.first() && self .block_delimiter .match_left(&self.bytes[(self.index - 1)..]) => { self.index -= 1; self.process_delimit(false)?; } b if b == self.inline_delimiter.first() && self .inline_delimiter .match_left(&self.bytes[(self.index - 1)..]) => { self.index -= 1; self.process_delimit(true)?; } b'\\' => { self.inc(); } b'`' => self.process_backtick()?, _ => (), } Ok(()) } /// Fully skip a backtick-delimited code block. /// Guaranteed to match the number of backticks in delimiters. /// Return `Err(())` if no more bytes to process. fn process_backtick(&mut self) -> Result<(), ()> { let mut n_back_ticks = 1; loop { let byte = self.get_byte()?; if byte == b'`' { self.inc(); n_back_ticks += 1; } else { break; } } loop { self.index += self.string[self.index..] .find(&"`".repeat(n_back_ticks)) .ok_or(())? + n_back_ticks; if self.get_byte()? == b'`' { // Skip excessive backticks. self.inc(); while let b'`' = self.get_byte()? { self.inc(); } } else { break; } } Ok(()) } /// Skip a full math block. /// Add `Event`s to mark the start and end of the math block and /// surrounding text blocks. /// Return `Err(())` if no more bytes to process. fn process_delimit(&mut self, inline: bool) -> Result<(), ()> { if self.index > 0 { self.events.push_back(Event::TextEnd(self.index)); } let delim = if inline { self.inline_delimiter } else { self.block_delimiter }; self.index += delim.left.len(); self.events.push_back(Event::Begin(self.index)); loop { self.index += self.string[self.index..].find(&delim.right).ok_or(())?; // Check `\`. let mut escaped = false; let mut checking = self.index; loop { checking -= 1; if self.bytes.get(checking) == Some(&b'\\') { escaped = !escaped; } else { break; } } if !escaped { let end_event = if inline { Event::InlineEnd(self.index) } else { Event::BlockEnd(self.index) }; self.events.push_back(end_event); self.index += delim.right.len(); self.events.push_back(Event::Begin(self.index)); break; } else { self.index += delim.right.len(); } } Ok(()) } } ================================================ FILE: src/tests/escape.rs ================================================ use super::*; fn test_render(raw_content: &str) -> (String, String) { let (stylesheet_header, mut rendered) = test_render_with_cfg(&[raw_content], KatexConfig::default()); (stylesheet_header, rendered.pop().unwrap()) } fn test_render_with_cfg(raw_contents: &[&str], cfg: KatexConfig) -> (String, Vec) { let extra_opts = cfg.build_extra_opts(); let stylesheet_header = KATEX_HEADER.to_owned(); let rendered = raw_contents .iter() .map(|raw_content| process_chapter_escape(raw_content, &extra_opts, &stylesheet_header)) .collect(); (stylesheet_header, rendered) } #[test] fn test_escape_without_math() { let raw_content = r"Some text, and more text."; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_dollar_escape() { let raw_content = r"Some text, \$\$ and more text."; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_escape_with_math() { let raw_content = r"A simple fomula, $\sum_{n=1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}$."; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + r"A simple fomula, $\\sum\_{n=1}^\\infty \\frac{1}{n^2} = \\frac{\\pi^2}{6}$."; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_escape_underscore() { let raw_content = r"A simple `f_f_f`, f_f_f, f`f$f_$f_` fomula, $\sum_{n=1}^\infty\\$."; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + r"A simple `f_f_f`, f_f_f, f`f$f_$f_` fomula, $\\sum\_{n=1}^\\infty\\\\$."; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_escape_vmatrix() { let raw_content = r"$$\begin{vmatrix}a&b\\c&d\end{vmatrix}$$"; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + r"$$\\begin{vmatrix}a&b\\\\c&d\\end{vmatrix}$$"; debug_assert_eq!(expected_output, rendered_content); } ================================================ FILE: src/tests/mod.rs ================================================ use super::*; #[test] fn test_name() { let pre = KatexProcessor; let preprocessor: &dyn Preprocessor = ⪯ assert_eq!(preprocessor.name(), "katex") } #[test] fn test_support_html() { let preprocessor = KatexProcessor; assert!(preprocessor.supports_renderer("html").unwrap()); assert!(preprocessor.supports_renderer("other_renderer").unwrap()) } mod escape; #[cfg(feature = "pre-render")] mod render; ================================================ FILE: src/tests/render/not_duktape.rs ================================================ use super::*; #[test] fn test_katex_rendering_vmatrix() { use crate::cfg::KatexConfig; let math_expr = r"\begin{vmatrix}a&b\\c&d\end{vmatrix}"; let cfg = KatexConfig::default(); let (_, display_opts) = cfg.build_opts_from_macros(HashMap::new()); let _ = katex::render_with_opts(math_expr, display_opts).unwrap(); } #[test] fn test_rendering_vmatrix() { let raw_content = r"$$\begin{vmatrix}a&b\\c&d\end{vmatrix}$$"; let (stylesheet_header, rendered_content) = test_render(raw_content); debug_assert_eq!( stylesheet_header+ "\u{200b}ac\u{200b}bd\u{200b}\u{200b}", rendered_content ); } ================================================ FILE: src/tests/render.rs ================================================ use super::*; fn test_render(raw_content: &str) -> (String, String) { let (stylesheet_header, mut rendered) = test_render_with_macro(&[raw_content], HashMap::new()); (stylesheet_header, rendered.pop().unwrap()) } fn test_render_with_macro( raw_contents: &[&str], macros: HashMap, ) -> (String, Vec) { test_render_with_cfg(raw_contents, macros, KatexConfig::default()) } fn test_render_with_cfg( raw_contents: &[&str], macros: HashMap, cfg: KatexConfig, ) -> (String, Vec) { let (inline_opts, display_opts) = cfg.build_opts_from_macros(macros); let extra_opts = cfg.build_extra_opts(); let stylesheet_header = KATEX_HEADER.to_owned(); let rendered = raw_contents .iter() .map(|raw_content| { process_chapter_prerender( raw_content, inline_opts.clone(), display_opts.clone(), &stylesheet_header, &extra_opts, ) }) .collect(); (stylesheet_header, rendered) } #[test] fn test_rendering_without_math() { let raw_content = r"Some text, and more text."; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_dollar_escaping() { let raw_content = r"Some text, \$\$ and more text."; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_dollar_escaping_inside_expr() { let raw_content = r"We randomly assign: $r \xleftarrow{\$} G $."; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + "We randomly assign: r$\u{200b}G."; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_inline_rendering() { let (stylesheet_header, rendered_content) = test_render(r"Some text, $\nabla f(x) \in \mathbb{R}^n$, and more text."); let expected_output=stylesheet_header+"Some text, f(x)Rn, and more text."; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_display_rendering() { let (stylesheet_header, rendered_content) = test_render(r"Some text, $\nabla f(x) \in \mathbb{R}^n$, and more text."); let expected_output=stylesheet_header+"Some text, f(x)Rn, and more text."; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_macros_without_argument() { let mut macros = HashMap::new(); macros.insert(String::from(r"\grad"), String::from(r"\nabla")); let raw_content_no_macro = r"Some text, $\nabla f(x) \in \mathbb{R}^n$, and more text."; let raw_content_macro = r"Some text, $\grad f(x) \in \mathbb{R}^n$, and more text."; let (_, rendered) = test_render_with_macro(&[raw_content_macro, raw_content_no_macro], macros); debug_assert_eq!(rendered[0], rendered[1]); } #[test] fn test_macros_with_argument() { let mut macros = HashMap::new(); macros.insert(String::from(r"\R"), String::from(r"\mathbb{R}^#1")); let raw_content_no_macro = r"Some text, $\nabla f(x) \in \mathbb{R}^1$, and more text."; let raw_content_macro = r"Some text, $\nabla f(x) \in \R{1}$, and more text."; let (_, rendered) = test_render_with_macro(&[raw_content_macro, raw_content_no_macro], macros); debug_assert_eq!(rendered[0], rendered[1]); } #[test] fn test_macro_file_loading() { let cfg_str = r#" [book] src = "src" [preprocessor.katex] macros = "macros.txt" "#; let book_cfg = cfg_str.parse().unwrap(); let cfg = get_config(&book_cfg).unwrap(); debug_assert_eq!( get_macro_path(PathBuf::from("book"), &cfg.macros), Some(PathBuf::from("book/macros.txt")) // We supply a root, just like the preproccessor context does ); } #[test] fn test_rendering_table_with_math() { let raw_content = r"| Syntax | Description | | --- | ----------- | | $\vec{a}$ | Title | | Paragraph | Text |"; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!( expected_output.lines().count(), rendered_content.lines().count() ); } #[test] fn test_rendering_delimiter_in_code_block() { let raw_content = r"``` $\omega$ ```"; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_rendering_delimiter_in_inline_code() { let raw_content = r"`$\omega$`"; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_rendering_delimiter_in_inline_code_when_inline_delimiter_starts_with_backtick() { let raw_content = r"`` `$\omega$` ``"; let cfg = KatexConfig { inline_delimiter: Delimiter { left: "`$".into(), right: "$`".into(), }, ..KatexConfig::default() }; let (stylesheet_header, mut rendered_content) = test_render_with_cfg(&[raw_content], HashMap::new(), cfg); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content.pop().unwrap()); } #[test] fn test_rendering_delimiter_in_block_code_when_block_delimiter_starts_with_backtick() { let raw_content = r#"```` ```math $\omega$ ``` ```` "#; let cfg = KatexConfig { block_delimiter: Delimiter { left: "```math".into(), right: "```".into(), }, ..KatexConfig::default() }; let (stylesheet_header, mut rendered_content) = test_render_with_cfg(&[raw_content], HashMap::new(), cfg); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content.pop().unwrap()); } #[test] fn test_rendering_delimiter_in_inline_code_when_block_delimiter_starts_with_backtick() { let raw_content = r"`$\omega$`"; let cfg = KatexConfig { block_delimiter: Delimiter { left: "```math".into(), right: "```".into(), }, ..KatexConfig::default() }; let (stylesheet_header, mut rendered_content) = test_render_with_cfg(&[raw_content], HashMap::new(), cfg); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content.pop().unwrap()); } #[test] fn test_invalid_expr_inline() { init_tracing(); let raw_content = r"$\<$"; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_invalid_expr_display() { init_tracing(); let raw_content = r"$$ \< $$"; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + raw_content; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_escaping_backtick() { let raw_content = r"\`$\omega$\`"; let (stylesheet_header, rendered_content) = test_render(raw_content); let expected_output = stylesheet_header + "\\`ω\\`"; debug_assert_eq!(expected_output, rendered_content); } #[test] fn test_include_src() { let raw_content = r"Define $f(x)$: $$ f(x)=x^2\\ x\in\R $$"; let (stylesheet_header, rendered_content) = test_render_with_cfg( &[raw_content], HashMap::new(), KatexConfig { include_src: true, ..KatexConfig::default() }, ); debug_assert_eq!(stylesheet_header + "Define f(x):\n\nf(x)=x2xR", rendered_content[0]); } #[test] fn test_fenced_code() { let raw_content = r"`\` and `` ` `` $\Leftarrow$ ``` `\` and `` ` `` ``` while ` ``` ` and ````` ```` ````` $\Leftarrow$ `````` ` ``` ` and ````` ```` ````` `````` $$ \Uparrow $$"; let (stylesheet_header, rendered_content) = test_render_with_cfg(&[raw_content], HashMap::new(), KatexConfig::default()); debug_assert_eq!( stylesheet_header + "`\\` and `` ` `` \n```\n`\\` and `` ` ``\n```\nwhile ` ``` ` and ````` ```` ````` \n``````\n` ``` ` and ````` ```` `````\n``````\n", rendered_content[0] ); } #[test] fn test_inline_rendering_w_custom_delimiter() { let raw_content = r"These $\(a\times b\) are from \[ \int_0^abdx \]"; let (stylesheet_header, rendered_content) = test_render_with_cfg( &[raw_content], HashMap::new(), KatexConfig { inline_delimiter: Delimiter { left: r"\(".into(), right: r"\)".into(), }, block_delimiter: Delimiter { left: r"\[".into(), right: r"\]".into(), }, ..KatexConfig::default() }, ); let expected_output = stylesheet_header + "These $a×b are from\n0a\u{200b}bdx"; debug_assert_eq!(expected_output, rendered_content[0]); } #[cfg(not(feature = "duktape"))] mod not_duktape;