feat(math): add katex-rs math rendering module

Add src/math.rs with render_math() wrapper around katex-rs for
build-time LaTeX to HTML conversion. Wire module into main.rs.

- Use KatexContext::default() for standard LaTeX function registry
- Settings: display_mode toggle, throw_on_error=false for graceful
  degradation
- Include unit tests for inline/display math rendering
This commit is contained in:
Timothy DeHerrera
2026-01-27 00:29:03 -07:00
parent a7338f5418
commit ebe1fd3b6e
4 changed files with 266 additions and 12 deletions

224
Cargo.lock generated
View File

@@ -74,6 +74,31 @@ dependencies = [
"wyz",
]
[[package]]
name = "bon"
version = "3.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234655ec178edd82b891e262ea7cf71f6584bcd09eff94db786be23f1821825c"
dependencies = [
"bon-macros",
"rustversion",
]
[[package]]
name = "bon-macros"
version = "3.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365"
dependencies = [
"darling",
"ident_case",
"prettyplease",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.114",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
@@ -187,7 +212,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf",
"phf 0.11.3",
"smallvec",
]
@@ -210,6 +235,40 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.114",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core",
"quote",
"syn 2.0.114",
]
[[package]]
name = "dashmap"
version = "5.5.3"
@@ -274,6 +333,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
@@ -364,6 +429,18 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -401,6 +478,24 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "katex-rs"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261b6bfdd542a6e476cb4fcb2c4fa639c176ec97a7c39fc339ba646ead29118"
dependencies = [
"bon",
"phf 0.13.1",
"phf_codegen 0.13.1",
"rapidhash",
"serde",
"serde_json",
"strum",
"strum_macros",
"thiserror",
"unicode-normalization",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -507,6 +602,7 @@ name = "nrd-sh"
version = "0.1.0"
dependencies = [
"gray_matter",
"katex-rs",
"lightningcss",
"maud",
"pulldown-cmark",
@@ -551,8 +647,8 @@ dependencies = [
"bitflags",
"cssparser",
"log",
"phf",
"phf_codegen",
"phf 0.11.3",
"phf_codegen 0.11.3",
"precomputed-hash",
"rustc-hash",
"smallvec",
@@ -603,8 +699,19 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared",
"phf_macros 0.11.3",
"phf_shared 0.11.3",
]
[[package]]
name = "phf"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_macros 0.13.1",
"phf_shared 0.13.1",
"serde",
]
[[package]]
@@ -613,8 +720,18 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
"phf_generator 0.11.3",
"phf_shared 0.11.3",
]
[[package]]
name = "phf_codegen"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
dependencies = [
"phf_generator 0.13.1",
"phf_shared 0.13.1",
]
[[package]]
@@ -623,18 +740,41 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"phf_shared 0.11.3",
"rand",
]
[[package]]
name = "phf_generator"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand",
"phf_shared 0.13.1",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator",
"phf_shared",
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "phf_macros"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
"phf_generator 0.13.1",
"phf_shared 0.13.1",
"proc-macro2",
"quote",
"syn 2.0.114",
@@ -649,12 +789,31 @@ dependencies = [
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.114",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -762,6 +921,15 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "rapidhash"
version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8b5b858a440a0bc02625b62dd95131b9201aa9f69f411195dd4a7cfb1de3d7"
dependencies = [
"rustversion",
]
[[package]]
name = "rayon"
version = "1.11.0"
@@ -992,6 +1160,33 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "syn"
version = "1.0.109"
@@ -1269,6 +1464,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.12.0"

View File

@@ -32,5 +32,6 @@ tree-sitter-yaml = "0.7"
lightningcss = "1.0.0-alpha.70"
# Config parsing
katex-rs = "0.2.3"
serde = { version = "1", features = ["derive"] }
toml = "0.8"

View File

@@ -8,6 +8,7 @@ mod css;
mod error;
mod feed;
mod highlight;
mod math;
mod render;
mod templates;

48
src/math.rs Normal file
View File

@@ -0,0 +1,48 @@
//! Math rendering via katex-rs.
//!
//! Converts LaTeX math expressions to HTML at build-time.
use katex::{render_to_string, KatexContext, Settings};
/// Render a LaTeX math expression to HTML.
///
/// # Arguments
/// * `latex` - The LaTeX source string
/// * `display_mode` - `true` for block equations, `false` for inline
///
/// # Returns
/// The rendered HTML string, or an error message on failure.
pub fn render_math(latex: &str, display_mode: bool) -> Result<String, String> {
let ctx = KatexContext::default();
let settings = Settings::builder()
.display_mode(display_mode)
.throw_on_error(false)
.build();
render_to_string(&ctx, latex, &settings).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inline_math() {
let result = render_math("x^2", false).unwrap();
assert!(result.contains("<span"));
}
#[test]
fn test_display_math() {
let result = render_math(r"\sum_{i=1}^n i", true).unwrap();
assert!(result.contains("<span"));
}
#[test]
fn test_invalid_latex_no_panic() {
// Should not panic, returns error or graceful fallback
let result = render_math(r"\invalidcommand", false);
// katex-rs with throw_on_error=false returns error markup
assert!(result.is_ok() || result.is_err());
}
}