From 4efc57a594fa25d3f4ad8b56e5a32bb818a6cfb2 Mon Sep 17 00:00:00 2001 From: Wes Turner Date: Sat, 20 Jun 2026 14:32:07 +0000 Subject: [PATCH] chore: cargo fmt --all --- crates/ratex-ffi/src/jni.rs | 30 +- crates/ratex-ffi/src/lib.rs | 27 +- crates/ratex-font-loader/src/outline_cache.rs | 5 +- crates/ratex-font/src/lib.rs | 4 +- crates/ratex-font/src/math_alpha.rs | 9 +- crates/ratex-font/src/metrics.rs | 231 +++- crates/ratex-font/src/symbols.rs | 11 +- crates/ratex-layout/src/engine.rs | 1003 +++++++++++------ crates/ratex-layout/src/katex_svg.rs | 791 +++++++++---- crates/ratex-layout/src/layout_box.rs | 10 +- crates/ratex-layout/src/lib.rs | 2 +- crates/ratex-layout/src/spacing.rs | 22 +- crates/ratex-layout/src/stacked_delim.rs | 53 +- crates/ratex-layout/src/to_display.rs | 129 ++- .../ratex-layout/tests/ce_co2_cjk_glyphs.rs | 5 +- crates/ratex-layout/tests/layout_vs_katex.rs | 52 +- crates/ratex-lexer/src/lexer.rs | 80 +- crates/ratex-parser/src/environments.rs | 281 +++-- crates/ratex-parser/src/functions/accent.rs | 93 +- crates/ratex-parser/src/functions/arrow.rs | 39 +- crates/ratex-parser/src/functions/char_cmd.rs | 10 +- crates/ratex-parser/src/functions/color.rs | 30 +- crates/ratex-parser/src/functions/cr.rs | 9 +- crates/ratex-parser/src/functions/def.rs | 47 +- .../ratex-parser/src/functions/delimsizing.rs | 150 ++- crates/ratex-parser/src/functions/enclose.rs | 38 +- crates/ratex-parser/src/functions/font.rs | 56 +- crates/ratex-parser/src/functions/genfrac.rs | 88 +- crates/ratex-parser/src/functions/hbox.rs | 9 +- .../ratex-parser/src/functions/horiz_brace.rs | 10 +- crates/ratex-parser/src/functions/href.rs | 18 +- .../ratex-parser/src/functions/htmlmathml.rs | 19 +- crates/ratex-parser/src/functions/kern.rs | 10 +- crates/ratex-parser/src/functions/lap.rs | 10 +- .../ratex-parser/src/functions/left_right.rs | 27 +- crates/ratex-parser/src/functions/math.rs | 20 +- .../ratex-parser/src/functions/mathchoice.rs | 10 +- crates/ratex-parser/src/functions/mclass.rs | 39 +- crates/ratex-parser/src/functions/mod.rs | 71 +- crates/ratex-parser/src/functions/op.rs | 91 +- .../src/functions/operatorname.rs | 10 +- crates/ratex-parser/src/functions/overline.rs | 19 +- crates/ratex-parser/src/functions/phantom.rs | 30 +- crates/ratex-parser/src/functions/pmb.rs | 16 +- crates/ratex-parser/src/functions/raisebox.rs | 9 +- crates/ratex-parser/src/functions/relax.rs | 11 +- crates/ratex-parser/src/functions/rule.rs | 9 +- crates/ratex-parser/src/functions/sizing.rs | 20 +- crates/ratex-parser/src/functions/spacing.rs | 38 +- crates/ratex-parser/src/functions/sqrt.rs | 2 +- crates/ratex-parser/src/functions/styling.rs | 9 +- crates/ratex-parser/src/functions/text.rs | 25 +- crates/ratex-parser/src/functions/vcenter.rs | 9 +- crates/ratex-parser/src/lib.rs | 2 +- crates/ratex-parser/src/macro_expander.rs | 39 +- crates/ratex-parser/src/mhchem/actions.rs | 77 +- crates/ratex-parser/src/mhchem/data.rs | 5 +- crates/ratex-parser/src/mhchem/engine.rs | 6 +- crates/ratex-parser/src/mhchem/mod.rs | 7 +- crates/ratex-parser/src/mhchem/patterns.rs | 75 +- crates/ratex-parser/src/mhchem/texify.rs | 14 +- crates/ratex-parser/src/parse_node.rs | 12 +- crates/ratex-parser/src/parser.rs | 92 +- crates/ratex-parser/src/tests.rs | 171 +-- crates/ratex-pdf/src/bin/render_pdf.rs | 20 +- crates/ratex-pdf/src/fonts.rs | 18 +- crates/ratex-render/src/main.rs | 21 +- crates/ratex-render/tests/bench_render.rs | 305 +++-- .../ratex-render/tests/font_cache_timing.rs | 3 +- crates/ratex-render/tests/golden_test.rs | 143 ++- crates/ratex-render/tests/phase_breakdown.rs | 28 +- crates/ratex-svg/src/bin/render_svg.rs | 21 +- crates/ratex-svg/src/standalone.rs | 10 +- crates/ratex-svg/tests/golden_svg.rs | 5 +- crates/ratex-types/src/color.rs | 6 +- crates/ratex-types/src/lib.rs | 2 +- crates/ratex-types/src/math_style.rs | 12 +- crates/ratex-types/src/path_command.rs | 41 +- crates/ratex-types/src/unicode_scripts.rs | 5 +- crates/ratex-unicode-font/src/lib.rs | 28 +- crates/ratex-wasm/src/lib.rs | 25 +- 81 files changed, 3479 insertions(+), 1560 deletions(-) diff --git a/crates/ratex-ffi/src/jni.rs b/crates/ratex-ffi/src/jni.rs index 8cdf9f9f..2cea0401 100644 --- a/crates/ratex-ffi/src/jni.rs +++ b/crates/ratex-ffi/src/jni.rs @@ -5,11 +5,11 @@ //! //! Compiled only when `target_os = "android"` (see lib.rs). -use jni::objects::{JClass, JString, JFloatArray}; -use jni::sys::{jboolean, jstring, jobject, JNI_TRUE}; +use jni::objects::{JClass, JFloatArray, JString}; +use jni::sys::{jboolean, jobject, jstring, JNI_TRUE}; use jni::JNIEnv; -use crate::{ratex_parse_and_layout, ratex_get_last_error, RatexOptions, RatexColor}; +use crate::{ratex_get_last_error, ratex_parse_and_layout, RatexColor, RatexOptions}; use std::ffi::CString; /// JNI entry point for `RaTeXEngine.nativeParseAndLayout(latex: String, displayMode: Boolean, rgba: FloatArray): String?` @@ -30,8 +30,10 @@ pub extern "system" fn Java_io_ratex_RaTeXEngine_nativeParseAndLayout( let latex_str: String = match env.get_string(&latex) { Ok(s) => s.into(), Err(e) => { - let _ = env.throw_new("java/lang/IllegalArgumentException", - format!("invalid latex string: {e}")); + let _ = env.throw_new( + "java/lang/IllegalArgumentException", + format!("invalid latex string: {e}"), + ); return std::ptr::null_mut(); } }; @@ -39,8 +41,10 @@ pub extern "system" fn Java_io_ratex_RaTeXEngine_nativeParseAndLayout( let c_latex = match CString::new(latex_str) { Ok(cs) => cs, Err(e) => { - let _ = env.throw_new("java/lang/IllegalArgumentException", - format!("latex contains null byte: {e}")); + let _ = env.throw_new( + "java/lang/IllegalArgumentException", + format!("latex contains null byte: {e}"), + ); return std::ptr::null_mut(); } }; @@ -91,14 +95,20 @@ pub extern "system" fn Java_io_ratex_RaTeXEngine_nativeParseAndLayout( return std::ptr::null_mut(); } - let json = unsafe { std::ffi::CStr::from_ptr(result.data).to_string_lossy().into_owned() }; + let json = unsafe { + std::ffi::CStr::from_ptr(result.data) + .to_string_lossy() + .into_owned() + }; unsafe { crate::ratex_free_display_list(result.data) }; match env.new_string(json) { Ok(s) => s.into_raw(), Err(e) => { - let _ = env.throw_new("java/lang/RuntimeException", - format!("failed to create Java string: {e}")); + let _ = env.throw_new( + "java/lang/RuntimeException", + format!("failed to create Java string: {e}"), + ); std::ptr::null_mut() } } diff --git a/crates/ratex-ffi/src/lib.rs b/crates/ratex-ffi/src/lib.rs index ecb03146..60e43a7a 100644 --- a/crates/ratex-ffi/src/lib.rs +++ b/crates/ratex-ffi/src/lib.rs @@ -83,12 +83,17 @@ fn sanitize_json_numbers(v: Value) -> Value { } } -fn do_layout(latex_str: &str, style: MathStyle, color: ratex_types::color::Color) -> Result { +fn do_layout( + latex_str: &str, + style: MathStyle, + color: ratex_types::color::Color, +) -> Result { let nodes = parse(latex_str).map_err(|e| format!("parse error: {e}"))?; let options = LayoutOptions::default().with_style(style).with_color(color); let layout_box = layout(&nodes, &options); let display_list = to_display_list(&layout_box); - let value = serde_json::to_value(&display_list).map_err(|e| format!("serialization error: {e}"))?; + let value = + serde_json::to_value(&display_list).map_err(|e| format!("serialization error: {e}"))?; let mut sanitized = sanitize_json_numbers(value); // Add a protocol version at the top level for forward-compatible decoding. if let Value::Object(ref mut map) = sanitized { @@ -199,7 +204,10 @@ pub unsafe extern "C" fn ratex_parse_and_layout( ) -> RatexResult { let err_result = |msg: &str| -> RatexResult { set_last_error(msg); - RatexResult { data: std::ptr::null_mut(), error_code: 1 } + RatexResult { + data: std::ptr::null_mut(), + error_code: 1, + } }; clear_last_error(); @@ -217,8 +225,8 @@ pub unsafe extern "C" fn ratex_parse_and_layout( MathStyle::Display } else { let opts_ref = unsafe { &*opts }; - let min_size = std::mem::offset_of!(RatexOptions, display_mode) - + std::mem::size_of::(); + let min_size = + std::mem::offset_of!(RatexOptions, display_mode) + std::mem::size_of::(); if opts_ref.struct_size >= min_size && opts_ref.display_mode == 0 { MathStyle::Text } else { @@ -230,8 +238,8 @@ pub unsafe extern "C" fn ratex_parse_and_layout( ratex_types::color::Color::BLACK } else { let opts_ref = unsafe { &*opts }; - let color_size = std::mem::offset_of!(RatexOptions, color) - + std::mem::size_of::<*const RatexColor>(); + let color_size = + std::mem::offset_of!(RatexOptions, color) + std::mem::size_of::<*const RatexColor>(); if opts_ref.struct_size >= color_size && !opts_ref.color.is_null() { match validate_color(unsafe { *opts_ref.color }) { @@ -245,7 +253,10 @@ pub unsafe extern "C" fn ratex_parse_and_layout( match do_layout(latex_str, style, color) { Ok(json) => match CString::new(json) { - Ok(cs) => RatexResult { data: cs.into_raw(), error_code: 0 }, + Ok(cs) => RatexResult { + data: cs.into_raw(), + error_code: 0, + }, Err(e) => err_result(&format!("JSON contains interior null byte: {e}")), }, Err(e) => err_result(&e), diff --git a/crates/ratex-font-loader/src/outline_cache.rs b/crates/ratex-font-loader/src/outline_cache.rs index 02533a46..a39a5f06 100644 --- a/crates/ratex-font-loader/src/outline_cache.rs +++ b/crates/ratex-font-loader/src/outline_cache.rs @@ -40,10 +40,7 @@ pub fn get_or_compute_outline( // Slow path: compute outline + write-lock. // For variable fonts, clone + pin to wght=400; non-variable fonts use the original directly. // Keep in sync with `variable_weight` in ratex-pdf/src/fonts.rs. - let needs_variation = font - .variations() - .iter() - .any(|axis| &axis.tag == b"wght"); + let needs_variation = font.variations().iter().any(|axis| &axis.tag == b"wght"); let outline = if needs_variation { let mut instance = font.clone(); diff --git a/crates/ratex-font/src/lib.rs b/crates/ratex-font/src/lib.rs index f71d23a6..617e9935 100644 --- a/crates/ratex-font/src/lib.rs +++ b/crates/ratex-font/src/lib.rs @@ -10,4 +10,6 @@ pub use metrics::{ get_char_metrics, get_char_metrics_for_mode, get_char_metrics_with_fallback, get_global_metrics, CharMetrics, MathConstants, MATH_CONSTANTS_BY_SIZE, }; -pub use symbols::{get_math_symbol, get_symbol, get_text_symbol, Group, Mode, SymbolFont, SymbolInfo}; +pub use symbols::{ + get_math_symbol, get_symbol, get_text_symbol, Group, Mode, SymbolFont, SymbolInfo, +}; diff --git a/crates/ratex-font/src/math_alpha.rs b/crates/ratex-font/src/math_alpha.rs index 9d422f4a..d9957aca 100644 --- a/crates/ratex-font/src/math_alpha.rs +++ b/crates/ratex-font/src/math_alpha.rs @@ -26,11 +26,7 @@ pub fn font_and_metric_for_mathematical_alphanumeric(cp: u32) -> Option<(FontId, for &(base, fid) in BASES_LETTERS { if cp >= base && cp < base + LETTERS52 { let i = cp - base; - let metric = if i < 26 { - 0x41 + i - } else { - 0x61 + (i - 26) - }; + let metric = if i < 26 { 0x41 + i } else { 0x61 + (i - 26) }; return Some((fid, metric)); } } @@ -61,7 +57,8 @@ pub fn font_and_metric_for_mathematical_alphanumeric(cp: u32) -> Option<(FontId, /// The display list keeps the real Unicode scalar in `char_code` (for web canvas / SVG ``). /// Outlines in shipped KaTeX fonts are keyed by ASCII letters and digits for these ranges. pub fn katex_ttf_glyph_char(font_id: FontId, display_char_code: u32) -> char { - if let Some((mapped_font, metric)) = font_and_metric_for_mathematical_alphanumeric(display_char_code) + if let Some((mapped_font, metric)) = + font_and_metric_for_mathematical_alphanumeric(display_char_code) { if mapped_font == font_id { return char::from_u32(metric).unwrap_or('\u{fffd}'); diff --git a/crates/ratex-font/src/metrics.rs b/crates/ratex-font/src/metrics.rs index 705ae347..8c097f35 100644 --- a/crates/ratex-font/src/metrics.rs +++ b/crates/ratex-font/src/metrics.rs @@ -68,60 +68,114 @@ impl MathConstants { pub static MATH_CONSTANTS_BY_SIZE: [MathConstants; 3] = [ // [0] textstyle (size index >= 5, >= 9pt) — from cmsy10 MathConstants { - slant: 0.250, space: 0.0, stretch: 0.0, shrink: 0.0, - x_height: 0.431, quad: 1.0, extra_space: 0.0, - num1: 0.677, num2: 0.394, num3: 0.444, - denom1: 0.686, denom2: 0.345, - sup1: 0.413, sup2: 0.363, sup3: 0.289, - sub1: 0.150, sub2: 0.247, - sup_drop: 0.386, sub_drop: 0.050, - delim1: 2.390, delim2: 1.010, + slant: 0.250, + space: 0.0, + stretch: 0.0, + shrink: 0.0, + x_height: 0.431, + quad: 1.0, + extra_space: 0.0, + num1: 0.677, + num2: 0.394, + num3: 0.444, + denom1: 0.686, + denom2: 0.345, + sup1: 0.413, + sup2: 0.363, + sup3: 0.289, + sub1: 0.150, + sub2: 0.247, + sup_drop: 0.386, + sub_drop: 0.050, + delim1: 2.390, + delim2: 1.010, axis_height: 0.250, default_rule_thickness: 0.04, - big_op_spacing1: 0.111, big_op_spacing2: 0.166, - big_op_spacing3: 0.2, big_op_spacing4: 0.6, big_op_spacing5: 0.1, + big_op_spacing1: 0.111, + big_op_spacing2: 0.166, + big_op_spacing3: 0.2, + big_op_spacing4: 0.6, + big_op_spacing5: 0.1, sqrt_rule_thickness: 0.04, pt_per_em: 10.0, - double_rule_sep: 0.2, array_rule_width: 0.04, - fboxsep: 0.3, fboxrule: 0.04, + double_rule_sep: 0.2, + array_rule_width: 0.04, + fboxsep: 0.3, + fboxrule: 0.04, }, // [1] scriptstyle (size index 3-4, 7-8pt) — from cmsy7 MathConstants { - slant: 0.250, space: 0.0, stretch: 0.0, shrink: 0.0, - x_height: 0.431, quad: 1.171, extra_space: 0.0, - num1: 0.732, num2: 0.384, num3: 0.471, - denom1: 0.752, denom2: 0.344, - sup1: 0.503, sup2: 0.431, sup3: 0.286, - sub1: 0.143, sub2: 0.286, - sup_drop: 0.353, sub_drop: 0.071, - delim1: 1.700, delim2: 1.157, + slant: 0.250, + space: 0.0, + stretch: 0.0, + shrink: 0.0, + x_height: 0.431, + quad: 1.171, + extra_space: 0.0, + num1: 0.732, + num2: 0.384, + num3: 0.471, + denom1: 0.752, + denom2: 0.344, + sup1: 0.503, + sup2: 0.431, + sup3: 0.286, + sub1: 0.143, + sub2: 0.286, + sup_drop: 0.353, + sub_drop: 0.071, + delim1: 1.700, + delim2: 1.157, axis_height: 0.250, default_rule_thickness: 0.049, - big_op_spacing1: 0.111, big_op_spacing2: 0.166, - big_op_spacing3: 0.2, big_op_spacing4: 0.611, big_op_spacing5: 0.143, + big_op_spacing1: 0.111, + big_op_spacing2: 0.166, + big_op_spacing3: 0.2, + big_op_spacing4: 0.611, + big_op_spacing5: 0.143, sqrt_rule_thickness: 0.04, pt_per_em: 10.0, - double_rule_sep: 0.2, array_rule_width: 0.04, - fboxsep: 0.3, fboxrule: 0.04, + double_rule_sep: 0.2, + array_rule_width: 0.04, + fboxsep: 0.3, + fboxrule: 0.04, }, // [2] scriptscriptstyle (size index 1-2, 5-6pt) — from cmsy5 MathConstants { - slant: 0.250, space: 0.0, stretch: 0.0, shrink: 0.0, - x_height: 0.431, quad: 1.472, extra_space: 0.0, - num1: 0.925, num2: 0.387, num3: 0.504, - denom1: 1.025, denom2: 0.532, - sup1: 0.504, sup2: 0.404, sup3: 0.294, - sub1: 0.200, sub2: 0.400, - sup_drop: 0.494, sub_drop: 0.100, - delim1: 1.980, delim2: 1.420, + slant: 0.250, + space: 0.0, + stretch: 0.0, + shrink: 0.0, + x_height: 0.431, + quad: 1.472, + extra_space: 0.0, + num1: 0.925, + num2: 0.387, + num3: 0.504, + denom1: 1.025, + denom2: 0.532, + sup1: 0.504, + sup2: 0.404, + sup3: 0.294, + sub1: 0.200, + sub2: 0.400, + sup_drop: 0.494, + sub_drop: 0.100, + delim1: 1.980, + delim2: 1.420, axis_height: 0.250, default_rule_thickness: 0.049, - big_op_spacing1: 0.111, big_op_spacing2: 0.166, - big_op_spacing3: 0.2, big_op_spacing4: 0.611, big_op_spacing5: 0.143, + big_op_spacing1: 0.111, + big_op_spacing2: 0.166, + big_op_spacing3: 0.2, + big_op_spacing4: 0.611, + big_op_spacing5: 0.143, sqrt_rule_thickness: 0.04, pt_per_em: 10.0, - double_rule_sep: 0.2, array_rule_width: 0.04, - fboxsep: 0.3, fboxrule: 0.04, + double_rule_sep: 0.2, + array_rule_width: 0.04, + fboxsep: 0.3, + fboxrule: 0.04, }, ]; @@ -182,21 +236,76 @@ pub fn get_char_metrics(font_id: FontId, char_code: u32) -> Option fn extra_character_fallback(ch: u32) -> Option { let c = char::from_u32(ch)?; let mapped = match c { - 'Å' => 'A', 'Ð' => 'D', 'Þ' => 'o', 'å' => 'a', 'ð' => 'd', 'þ' => 'o', - 'А' => 'A', 'Б' => 'B', 'В' => 'B', 'Г' => 'F', 'Д' => 'A', - 'Е' => 'E', 'Ж' => 'K', 'З' => '3', 'И' => 'N', 'Й' => 'N', - 'К' => 'K', 'Л' => 'N', 'М' => 'M', 'Н' => 'H', 'О' => 'O', - 'П' => 'N', 'Р' => 'P', 'С' => 'C', 'Т' => 'T', 'У' => 'y', - 'Ф' => 'O', 'Х' => 'X', 'Ц' => 'U', 'Ч' => 'h', 'Ш' => 'W', - 'Щ' => 'W', 'Ъ' => 'B', 'Ы' => 'X', 'Ь' => 'B', 'Э' => '3', - 'Ю' => 'X', 'Я' => 'R', - 'а' => 'a', 'б' => 'b', 'в' => 'a', 'г' => 'r', 'д' => 'y', - 'е' => 'e', 'ж' => 'm', 'з' => 'e', 'и' => 'n', 'й' => 'n', - 'к' => 'n', 'л' => 'n', 'м' => 'm', 'н' => 'n', 'о' => 'o', - 'п' => 'n', 'р' => 'p', 'с' => 'c', 'т' => 'o', 'у' => 'y', - 'ф' => 'b', 'х' => 'x', 'ц' => 'n', 'ч' => 'n', 'ш' => 'w', - 'щ' => 'w', 'ъ' => 'a', 'ы' => 'm', 'ь' => 'a', 'э' => 'e', - 'ю' => 'm', 'я' => 'r', + 'Å' => 'A', + 'Ð' => 'D', + 'Þ' => 'o', + 'å' => 'a', + 'ð' => 'd', + 'þ' => 'o', + 'А' => 'A', + 'Б' => 'B', + 'В' => 'B', + 'Г' => 'F', + 'Д' => 'A', + 'Е' => 'E', + 'Ж' => 'K', + 'З' => '3', + 'И' => 'N', + 'Й' => 'N', + 'К' => 'K', + 'Л' => 'N', + 'М' => 'M', + 'Н' => 'H', + 'О' => 'O', + 'П' => 'N', + 'Р' => 'P', + 'С' => 'C', + 'Т' => 'T', + 'У' => 'y', + 'Ф' => 'O', + 'Х' => 'X', + 'Ц' => 'U', + 'Ч' => 'h', + 'Ш' => 'W', + 'Щ' => 'W', + 'Ъ' => 'B', + 'Ы' => 'X', + 'Ь' => 'B', + 'Э' => '3', + 'Ю' => 'X', + 'Я' => 'R', + 'а' => 'a', + 'б' => 'b', + 'в' => 'a', + 'г' => 'r', + 'д' => 'y', + 'е' => 'e', + 'ж' => 'm', + 'з' => 'e', + 'и' => 'n', + 'й' => 'n', + 'к' => 'n', + 'л' => 'n', + 'м' => 'm', + 'н' => 'n', + 'о' => 'o', + 'п' => 'n', + 'р' => 'p', + 'с' => 'c', + 'т' => 'o', + 'у' => 'y', + 'ф' => 'b', + 'х' => 'x', + 'ц' => 'n', + 'ч' => 'n', + 'ш' => 'w', + 'щ' => 'w', + 'ъ' => 'a', + 'ы' => 'm', + 'ь' => 'a', + 'э' => 'e', + 'ю' => 'm', + 'я' => 'r', _ => return None, }; Some(mapped as u32) @@ -206,11 +315,9 @@ fn extra_character_fallback(ch: u32) -> Option { /// /// First tries direct lookup, then falls back to `extraCharacterMap` approximation. pub fn get_char_metrics_with_fallback(font_id: FontId, char_code: u32) -> Option { - get_char_metrics(font_id, char_code) - .or_else(|| { - extra_character_fallback(char_code) - .and_then(|fallback| get_char_metrics(font_id, fallback)) - }) + get_char_metrics(font_id, char_code).or_else(|| { + extra_character_fallback(char_code).and_then(|fallback| get_char_metrics(font_id, fallback)) + }) } /// Look up character metrics with full KaTeX-compatible fallback chain. @@ -241,7 +348,11 @@ mod tests { #[test] fn test_main_regular_lowercase_a() { let m = get_char_metrics(FontId::MainRegular, 97).unwrap(); - assert!((m.height - 0.43056).abs() < 0.001, "height of 'a': {}", m.height); + assert!( + (m.height - 0.43056).abs() < 0.001, + "height of 'a': {}", + m.height + ); assert!(m.depth.abs() < 0.001, "depth of 'a': {}", m.depth); assert!(m.width > 0.0, "width of 'a' should be positive"); } @@ -249,7 +360,11 @@ mod tests { #[test] fn test_main_regular_uppercase_a() { let m = get_char_metrics(FontId::MainRegular, 65).unwrap(); - assert!((m.height - 0.68333).abs() < 0.001, "height of 'A': {}", m.height); + assert!( + (m.height - 0.68333).abs() < 0.001, + "height of 'A': {}", + m.height + ); assert!(m.depth.abs() < 0.001); } diff --git a/crates/ratex-font/src/symbols.rs b/crates/ratex-font/src/symbols.rs index 8a77a4eb..df1350d0 100644 --- a/crates/ratex-font/src/symbols.rs +++ b/crates/ratex-font/src/symbols.rs @@ -106,7 +106,10 @@ fn symbol_maps() -> &'static SymbolMaps { by_codepoint.entry((mode, ch)).or_insert(i); } } - SymbolMaps { by_name, by_codepoint } + SymbolMaps { + by_name, + by_codepoint, + } }) } @@ -115,7 +118,11 @@ fn entry_to_info(idx: usize, mode: Mode) -> SymbolInfo { SymbolInfo { name: n, mode, - font: if f == 0 { SymbolFont::Main } else { SymbolFont::Ams }, + font: if f == 0 { + SymbolFont::Main + } else { + SymbolFont::Ams + }, group: Group::parse(g).unwrap_or(Group::MathOrd), codepoint: cp, } diff --git a/crates/ratex-layout/src/engine.rs b/crates/ratex-layout/src/engine.rs index 847c8d4f..ca7693a0 100644 --- a/crates/ratex-layout/src/engine.rs +++ b/crates/ratex-layout/src/engine.rs @@ -37,8 +37,7 @@ fn apply_bin_cancellation(raw: &[Option]) -> Vec> { let prev = if i == 0 { None } else { raw[i - 1] }; let left_cancel = matches!( prev, - None - | Some(MathClass::Bin) + None | Some(MathClass::Bin) | Some(MathClass::Open) | Some(MathClass::Rel) | Some(MathClass::Op) @@ -88,8 +87,7 @@ fn layout_expression( return layout_multiline(nodes, options, is_real_group); } - let raw_classes: Vec> = - nodes.iter().map(node_math_class).collect(); + let raw_classes: Vec> = nodes.iter().map(node_math_class).collect(); let eff_classes = apply_bin_cancellation(&raw_classes); let mut children = Vec::new(); @@ -103,8 +101,8 @@ fn layout_expression( if is_real_group { if let (Some(prev), Some(cur)) = (prev_class, cur_class) { - let prev_middle = prev_class_node_idx - .is_some_and(|j| node_is_middle_fence(&nodes[j])); + let prev_middle = + prev_class_node_idx.is_some_and(|j| node_is_middle_fence(&nodes[j])); let cur_middle = node_is_middle_fence(node); let mu = if prev_middle || cur_middle { 0.0 @@ -176,7 +174,10 @@ fn layout_multiline( // TeX baselineskip: gap = baselineskip - prev_depth - cur_height let prev_depth = row_boxes[i - 1].depth; let gap = (baselineskip - prev_depth - row.height).max(lineskip); - vchildren.push(VBoxChild { kind: VBoxChildKind::Kern(gap), shift: 0.0 }); + vchildren.push(VBoxChild { + kind: VBoxChildKind::Kern(gap), + shift: 0.0, + }); h += gap + row.height + prev_depth; } vchildren.push(VBoxChild { @@ -194,7 +195,6 @@ fn layout_multiline( } } - /// Lay out a single ParseNode. fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { match node { @@ -205,15 +205,24 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { ParseNode::OrdGroup { body, .. } => layout_expression(body, options, true), - ParseNode::SupSub { - base, sup, sub, .. - } => { + ParseNode::SupSub { base, sup, sub, .. } => { if let Some(base_node) = base.as_deref() { if should_use_op_limits(base_node, options) { - return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options); + return layout_op_with_limits( + base_node, + sup.as_deref(), + sub.as_deref(), + options, + ); } } - layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, None) + layout_supsub( + base.as_deref(), + sup.as_deref(), + sub.as_deref(), + options, + None, + ) } ParseNode::GenFrac { @@ -236,8 +245,12 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { }; let frac = layout_fraction(numer, denom, bar_thickness, *continued, options); - let has_left = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != "."); - let has_right = right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != "."); + let has_left = left_delim + .as_ref() + .is_some_and(|d| !d.is_empty() && d != "."); + let has_right = right_delim + .as_ref() + .is_some_and(|d| !d.is_empty() && d != "."); if has_left || has_right { let total_h = genfrac_delim_target_height(options); @@ -262,7 +275,11 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { color: options.color, } } else { - let right_nds = if *continued { 0.0 } else { NULL_DELIMITER_SPACE }; + let right_nds = if *continued { + 0.0 + } else { + NULL_DELIMITER_SPACE + }; make_hbox(vec![ LayoutBox::new_kern(NULL_DELIMITER_SPACE), frac, @@ -271,9 +288,7 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { } } - ParseNode::Sqrt { body, index, .. } => { - layout_radical(body, index.as_deref(), options) - } + ParseNode::Sqrt { body, index, .. } => layout_radical(body, index.as_deref(), options), ParseNode::Op { name, @@ -335,24 +350,43 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { } ParseNode::Accent { - label, base, is_stretchy, is_shifty, .. + label, + base, + is_stretchy, + is_shifty, + .. } => { // Some text accents (e.g. \c cedilla) place the mark below let is_below = matches!(label.as_str(), "\\c"); - layout_accent(label, base, is_stretchy.unwrap_or(false), is_shifty.unwrap_or(false), is_below, options) + layout_accent( + label, + base, + is_stretchy.unwrap_or(false), + is_shifty.unwrap_or(false), + is_below, + options, + ) } ParseNode::AccentUnder { - label, base, is_stretchy, .. - } => layout_accent(label, base, is_stretchy.unwrap_or(false), false, true, options), + label, + base, + is_stretchy, + .. + } => layout_accent( + label, + base, + is_stretchy.unwrap_or(false), + false, + true, + options, + ), ParseNode::LeftRight { body, left, right, .. } => layout_left_right(body, left, right, options), - ParseNode::DelimSizing { - size, delim, .. - } => layout_delim_sizing(*size, delim, options), + ParseNode::DelimSizing { size, delim, .. } => layout_delim_sizing(*size, delim, options), ParseNode::Array { body, @@ -392,13 +426,23 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { label_above, label_below, .. - } => layout_cd_arrow(direction, label_above.as_deref(), label_below.as_deref(), 0.0, 0.0, 0.0, options), + } => layout_cd_arrow( + direction, + label_above.as_deref(), + label_below.as_deref(), + 0.0, + 0.0, + 0.0, + options, + ), ParseNode::ProofTree { tree, .. } => layout_proof_tree(tree, options), ParseNode::Sizing { size, body, .. } => layout_sizing(*size, body, options), - ParseNode::Text { body, font, mode, .. } => match font.as_deref() { + ParseNode::Text { + body, font, mode, .. + } => match font.as_deref() { Some(f) => { let group = ParseNode::OrdGroup { mode: *mode, @@ -457,10 +501,19 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { } } - ParseNode::Smash { body, smash_height, smash_depth, .. } => { + ParseNode::Smash { + body, + smash_height, + smash_depth, + .. + } => { let mut inner = layout_node(body, options); - if *smash_height { inner.height = 0.0; } - if *smash_depth { inner.depth = 0.0; } + if *smash_height { + inner.height = 0.0; + } + if *smash_depth { + inner.depth = 0.0; + } inner } @@ -481,16 +534,20 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { } } - ParseNode::HtmlMathMl { html, .. } => { - layout_expression(html, options, true) - } + ParseNode::HtmlMathMl { html, .. } => layout_expression(html, options, true), - ParseNode::Html { attributes, body, .. } => layout_html(attributes, body, options), + ParseNode::Html { + attributes, body, .. + } => layout_html(attributes, body, options), ParseNode::MClass { body, .. } => layout_expression(body, options, true), ParseNode::MathChoice { - display, text, script, scriptscript, .. + display, + text, + script, + scriptscript, + .. } => { let branch = match options.style { MathStyle::Display | MathStyle::DisplayCramped => display, @@ -501,7 +558,9 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { layout_expression(branch, options, true) } - ParseNode::Lap { alignment, body, .. } => { + ParseNode::Lap { + alignment, body, .. + } => { let inner = layout_node(body, options); let shift = match alignment.as_str() { "llap" => -inner.width, @@ -539,9 +598,19 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { ParseNode::HBox { body, .. } => layout_text(body, options), - ParseNode::Enclose { label, background_color, border_color, body, .. } => { - layout_enclose(label, background_color.as_deref(), border_color.as_deref(), body, options) - } + ParseNode::Enclose { + label, + background_color, + border_color, + body, + .. + } => layout_enclose( + label, + background_color.as_deref(), + border_color.as_deref(), + body, + options, + ), ParseNode::RaiseBox { dy, body, .. } => { let shift = measurement_to_em(dy, options); @@ -569,7 +638,7 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox { ParseNode::Tag { tag, .. } => { let text_opts = options.with_style(options.style.text()); layout_expression(tag, &text_opts, true) - }, + } // Fallback for unhandled node types: produce empty box _ => LayoutBox::new_empty(), @@ -647,7 +716,7 @@ fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox { // Synthetic symbols not present in any KaTeX font; built from SVG paths. match ch as u32 { - 0x22B7 => return layout_imageof_origof(true, options), // \imageof •—○ + 0x22B7 => return layout_imageof_origof(true, options), // \imageof •—○ 0x22B6 => return layout_imageof_origof(false, options), // \origof ○—• _ => {} } @@ -666,10 +735,7 @@ fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox { width, height, depth, - content: BoxContent::Glyph { - font_id, - char_code, - }, + content: BoxContent::Glyph { font_id, char_code }, color: options.color, }; } @@ -717,10 +783,7 @@ fn layout_symbol(text: &str, mode: Mode, options: &LayoutOptions) -> LayoutBox { width, height, depth, - content: BoxContent::Glyph { - font_id, - char_code, - }, + content: BoxContent::Glyph { font_id, char_code }, color: options.color, } } @@ -781,10 +844,9 @@ fn select_font(text: &str, resolved_char: char, mode: Mode, _options: &LayoutOpt /// Lowercase Greek letters and variant forms use Math-Italic in math mode. /// Uppercase Greek (U+0391–U+03A9) stays upright in Main-Regular per TeX convention. fn is_math_italic_greek(ch: char) -> bool { - matches!(ch, - '\u{03B1}'..='\u{03C9}' | - '\u{03D1}' | '\u{03D5}' | '\u{03D6}' | - '\u{03F1}' | '\u{03F5}' + matches!( + ch, + '\u{03B1}'..='\u{03C9}' | '\u{03D1}' | '\u{03D5}' | '\u{03D6}' | '\u{03F1}' | '\u{03F5}' ) } @@ -928,20 +990,8 @@ fn layout_supsub( None => layout_node(n, opts), }; - let horiz_brace_over = matches!( - base, - Some(ParseNode::HorizBrace { - is_over: true, - .. - }) - ); - let horiz_brace_under = matches!( - base, - Some(ParseNode::HorizBrace { - is_over: false, - .. - }) - ); + let horiz_brace_over = matches!(base, Some(ParseNode::HorizBrace { is_over: true, .. })); + let horiz_brace_under = matches!(base, Some(ParseNode::HorizBrace { is_over: false, .. })); let center_scripts = horiz_brace_over || horiz_brace_under; let base_box = base @@ -971,9 +1021,15 @@ fn layout_supsub( layout_child(s, &sub_opts) }); - let sup_height_scaled = sup_box.as_ref().map(|b| b.height * sup_ratio).unwrap_or(0.0); + let sup_height_scaled = sup_box + .as_ref() + .map(|b| b.height * sup_ratio) + .unwrap_or(0.0); let sup_depth_scaled = sup_box.as_ref().map(|b| b.depth * sup_ratio).unwrap_or(0.0); - let sub_height_scaled = sub_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0); + let sub_height_scaled = sub_box + .as_ref() + .map(|b| b.height * sub_ratio) + .unwrap_or(0.0); let sub_depth_scaled = sub_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0); // KaTeX uses the CHILD style's metrics for supDrop/subDrop, not the parent's @@ -1064,9 +1120,8 @@ fn layout_supsub( if center_scripts { total_width = total_width.max(sup_b.width * sup_ratio + script_space); } else { - total_width = total_width.max( - base_box.width + italic_correction + sup_b.width * sup_ratio + script_space, - ); + total_width = total_width + .max(base_box.width + italic_correction + sup_b.width * sup_ratio + script_space); } } if let Some(ref sub_b) = sub_box { @@ -1074,9 +1129,8 @@ fn layout_supsub( if center_scripts { total_width = total_width.max(sub_b.width * sub_ratio + script_space); } else { - total_width = total_width.max( - base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space, - ); + total_width = total_width + .max(base_box.width + sub_h_kern + sub_b.width * sub_ratio + script_space); } } @@ -1152,8 +1206,7 @@ fn layout_radical( // Check if delimiter is taller than needed → center the extra space let delim_depth = tex_height - rule_width; if delim_depth > body_box.height + body_box.depth + line_clearance { - line_clearance = - (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0; + line_clearance = (line_clearance + delim_depth - body_box.height - body_box.depth) / 2.0; } let img_shift = tex_height - body_box.height - line_clearance - rule_width; @@ -1224,19 +1277,12 @@ fn should_use_op_limits(base: &ParseNode, options: &LayoutOptions) -> bool { limits, always_handle_sup_sub, .. - } => { - *limits - && (options.style.is_display() - || always_handle_sup_sub.unwrap_or(false)) - } + } => *limits && (options.style.is_display() || always_handle_sup_sub.unwrap_or(false)), ParseNode::OperatorName { always_handle_sup_sub, limits, .. - } => { - *always_handle_sup_sub - && (options.style.is_display() || *limits) - } + } => *always_handle_sup_sub && (options.style.is_display() || *limits), _ => false, } } @@ -1302,8 +1348,7 @@ fn build_op_base( options: &LayoutOptions, ) -> (LayoutBox, f64) { if symbol { - let large = options.style.is_display() - && !NO_SUCCESSOR.contains(&name.unwrap_or("")); + let large = options.style.is_display() && !NO_SUCCESSOR.contains(&name.unwrap_or("")); let font_id = if large { FontId::Size2Regular } else { @@ -1327,10 +1372,7 @@ fn build_op_base( width: width_with_italic, height, depth, - content: BoxContent::Glyph { - font_id, - char_code, - }, + content: BoxContent::Glyph { font_id, char_code }, color: options.color, }; @@ -1399,7 +1441,7 @@ fn resolve_op_char(name: &str) -> char { // \oiint and \oiiint: use ∬/∭ as base glyph; circle overlay is drawn in build_op_base // (same idea as \oint’s circle, but U+222F/U+2230 often missing in math fonts). match name { - "\\oiint" => return '\u{222C}', // ∬ (double integral) + "\\oiint" => return '\u{222C}', // ∬ (double integral) "\\oiiint" => return '\u{222D}', // ∭ (triple integral) _ => {} } @@ -1478,7 +1520,11 @@ fn layout_op_limits_inner( let sup_ratio = sup_style.size_multiplier() / options.style.size_multiplier(); let sub_ratio = sub_style.size_multiplier() / options.style.size_multiplier(); - let extra_kern = if legacy_limit_kern_padding { 0.08_f64 } else { 0.0_f64 }; + let extra_kern = if legacy_limit_kern_padding { + 0.08_f64 + } else { + 0.0_f64 + }; let sup_data = sup_node.map(|s| { let sup_opts = options.with_style(sup_style); @@ -1491,7 +1537,8 @@ fn layout_op_limits_inner( } else { elem.depth }; - let kern = (metrics.big_op_spacing1 + extra_kern).max(metrics.big_op_spacing3 - d + extra_kern); + let kern = + (metrics.big_op_spacing1 + extra_kern).max(metrics.big_op_spacing3 - d + extra_kern); (elem, kern) }); @@ -1503,7 +1550,8 @@ fn layout_op_limits_inner( } else { elem.height }; - let kern = (metrics.big_op_spacing2 + extra_kern).max(metrics.big_op_spacing4 - h + extra_kern); + let kern = + (metrics.big_op_spacing2 + extra_kern).max(metrics.big_op_spacing4 - h + extra_kern); (elem, kern) }); @@ -1520,11 +1568,7 @@ fn layout_op_limits_inner( let bottom = sp5 + sub_h + sub_d + sub_kern + base.depth + base_shift; - let height = bottom - + base.height - base_shift - + sup_kern - + sup_h + sup_d - + sp5 + let height = bottom + base.height - base_shift + sup_kern + sup_h + sup_d + sp5 - (base.height + base.depth); let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5; @@ -1555,8 +1599,7 @@ fn layout_op_limits_inner( let sup_h = sup_elem.height * sup_ratio; let sup_d = sup_elem.depth * sup_ratio; - let total_h = - base.height - base_shift + sup_kern + sup_h + sup_d + sp5; + let total_h = base.height - base_shift + sup_kern + sup_h + sup_d + sp5; let total_d = base.depth + base_shift; let w = base.width.max(sup_elem.width * sup_ratio); @@ -1633,14 +1676,10 @@ const VEC_SKEW_EXTRA_RIGHT_EM: f64 = 0.018; /// so the superscript starts at advance_width + italic_correction (not just advance_width). fn glyph_italic(lb: &LayoutBox) -> f64 { match &lb.content { - BoxContent::Glyph { font_id, char_code } => { - get_char_metrics(*font_id, *char_code) - .map(|m| m.italic) - .unwrap_or(0.0) - } - BoxContent::HBox(children) => { - children.last().map(glyph_italic).unwrap_or(0.0) - } + BoxContent::Glyph { font_id, char_code } => get_char_metrics(*font_id, *char_code) + .map(|m| m.italic) + .unwrap_or(0.0), + BoxContent::HBox(children) => children.last().map(glyph_italic).unwrap_or(0.0), _ => 0.0, } } @@ -1658,14 +1697,10 @@ fn accent_ordgroup_len(base: &ParseNode) -> usize { fn glyph_skew(lb: &LayoutBox) -> f64 { match &lb.content { - BoxContent::Glyph { font_id, char_code } => { - get_char_metrics(*font_id, *char_code) - .map(|m| m.skew) - .unwrap_or(0.0) - } - BoxContent::HBox(children) => { - children.last().map(glyph_skew).unwrap_or(0.0) - } + BoxContent::Glyph { font_id, char_code } => get_char_metrics(*font_id, *char_code) + .map(|m| m.skew) + .unwrap_or(0.0), + BoxContent::HBox(children) => children.last().map(glyph_skew).unwrap_or(0.0), _ => 0.0, } } @@ -1844,9 +1879,12 @@ fn layout_accent( // correction`, with `\bar`/`\=` exceptions) instead of `inner_clearance + ε`, which // double-counted stacked accent depths and inflated nested spacing vs KaTeX. let base_clearance = match &body_box.content { - BoxContent::Accent { clearance: inner_cl, is_below, accent: inner_accent, .. } - if !is_below => - { + BoxContent::Accent { + clearance: inner_cl, + is_below, + accent: inner_accent, + .. + } if !is_below => { // For SVG accents (height≈0, e.g. \vec): body_box.height = clearance + H_EM, // which matches KaTeX's body.height. Use min(body.height, xHeight) exactly as // KaTeX does: clearance = min(body.height, xHeight). @@ -1920,7 +1958,10 @@ fn layout_accent( }; let (height, depth) = if is_below { - (body_box.height, body_box.depth + accent_box.height + accent_box.depth + gap) + ( + body_box.height, + body_box.depth + accent_box.height + accent_box.depth + gap, + ) } else if use_arrow_path { (body_box.height + gap + accent_box.height, body_box.depth) } else { @@ -1998,9 +2039,9 @@ fn node_contains_middle(node: &ParseNode) -> bool { ParseNode::VPhantom { body, .. } | ParseNode::Smash { body, .. } => { node_contains_middle(body) } - ParseNode::Array { body, .. } => body - .iter() - .any(|row| row.iter().any(node_contains_middle)), + ParseNode::Array { body, .. } => { + body.iter().any(|row| row.iter().any(node_contains_middle)) + } ParseNode::Enclose { body, .. } | ParseNode::Lap { body, .. } | ParseNode::RaiseBox { body, .. } @@ -2009,7 +2050,11 @@ fn node_contains_middle(node: &ParseNode) -> bool { ParseNode::XArrow { body, below, .. } => { node_contains_middle(body) || below.as_deref().is_some_and(node_contains_middle) } - ParseNode::CdArrow { label_above, label_below, .. } => { + ParseNode::CdArrow { + label_above, + label_below, + .. + } => { label_above.as_deref().is_some_and(node_contains_middle) || label_below.as_deref().is_some_and(node_contains_middle) } @@ -2047,10 +2092,7 @@ fn genfrac_delim_target_height(options: &LayoutOptions) -> f64 { options.style, MathStyle::ScriptScript | MathStyle::ScriptScriptCramped ) { - options - .with_style(MathStyle::Script) - .metrics() - .delim2 + options.with_style(MathStyle::Script).metrics().delim2 } else { m.delim2 } @@ -2188,14 +2230,8 @@ fn scale_svg_path_to_em(cmds: &[PathCommand]) -> Vec { let s = 0.001_f64; cmds.iter() .map(|c| match *c { - PathCommand::MoveTo { x, y } => PathCommand::MoveTo { - x: x * s, - y: y * s, - }, - PathCommand::LineTo { x, y } => PathCommand::LineTo { - x: x * s, - y: y * s, - }, + PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x: x * s, y: y * s }, + PathCommand::LineTo { x, y } => PathCommand::LineTo { x: x * s, y: y * s }, PathCommand::CubicTo { x1, y1, @@ -2291,7 +2327,10 @@ fn make_vert_delim_box(total_height: f64, is_double: bool, options: &LayoutOptio width, height, depth, - content: BoxContent::SvgPath { commands, fill: true }, + content: BoxContent::SvgPath { + commands, + fill: true, + }, color: options.color, } } @@ -2716,7 +2755,11 @@ struct HtmlStyle { underline: bool, } -fn layout_html(attributes: &HashMap, body: &[ParseNode], options: &LayoutOptions) -> LayoutBox { +fn layout_html( + attributes: &HashMap, + body: &[ParseNode], + options: &LayoutOptions, +) -> LayoutBox { let style = attributes .get("style") .map(|style| parse_html_style(style)) @@ -2813,7 +2856,10 @@ fn parse_css_font_size(value: &str) -> Option { let parse_number = |s: &str| s.parse::().ok().filter(|n| n.is_finite() && *n > 0.0); if let Some(px) = value.strip_suffix("px") { parse_number(px).map(|n| n / 16.0) - } else if let Some(em) = value.strip_suffix("em").or_else(|| value.strip_suffix("rem")) { + } else if let Some(em) = value + .strip_suffix("em") + .or_else(|| value.strip_suffix("rem")) + { parse_number(em) } else if let Some(percent) = value.strip_suffix('%') { parse_number(percent).map(|n| n / 100.0) @@ -2850,12 +2896,7 @@ fn layout_verb(body: &str, star: bool, options: &LayoutOptions) -> LayoutBox { Some(m) => (FontId::TypewriterRegular, m.width, m.height, m.depth), None => match get_char_metrics(FontId::MainRegular, code) { Some(m) => (FontId::MainRegular, m.width, m.height, m.depth), - None => ( - FontId::TypewriterRegular, - 0.5, - metrics.x_height, - 0.0, - ), + None => (FontId::TypewriterRegular, 0.5, metrics.x_height, 0.0), }, }; children.push(LayoutBox { @@ -2924,12 +2965,7 @@ fn layout_pmb(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox { // Simplest approximation: just render body once (the shadow is < 1px at normal size) // but with a tiny kern to hint at bold width. // Better: use a simple 2-layer HBox with overlap. - let children = vec![ - kern_x, - shadow, - kern_back, - base, - ]; + let children = vec![kern_x, shadow, kern_back, base]; // Width should be original base width, not doubled let hbox = make_hbox(children); // Return a box with original dimensions (shadow overflow is clipped) @@ -3043,11 +3079,7 @@ fn is_single_char_body(node: &ParseNode) -> bool { /// Matches KaTeX `enclose.ts` + `stretchy.ts` geometry: /// • single char → v_pad = 0.2em, h_pad = 0 (line corner-to-corner of w × (h+d+0.4) box) /// • multi char → v_pad = 0, h_pad = 0.2em (cancel-pad: line extends 0.2em each side) -fn layout_cancel( - label: &str, - body: &ParseNode, - options: &LayoutOptions, -) -> LayoutBox { +fn layout_cancel(label: &str, body: &ParseNode, options: &LayoutOptions) -> LayoutBox { use crate::layout_box::BoxContent; let inner = layout_node(body, options); let w = inner.width.max(0.01); @@ -3070,25 +3102,49 @@ fn layout_cancel( // \bcancel = "\" diagonal: top-left → bottom-right let commands: Vec = match label { "\\cancel" => vec![ - PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, // bottom-left - PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, // top-right + PathCommand::MoveTo { + x: -h_pad, + y: d + v_pad, + }, // bottom-left + PathCommand::LineTo { + x: w + h_pad, + y: -h - v_pad, + }, // top-right ], "\\bcancel" => vec![ - PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, // top-left - PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, // bottom-right + PathCommand::MoveTo { + x: -h_pad, + y: -h - v_pad, + }, // top-left + PathCommand::LineTo { + x: w + h_pad, + y: d + v_pad, + }, // bottom-right ], "\\xcancel" => vec![ - PathCommand::MoveTo { x: -h_pad, y: d + v_pad }, - PathCommand::LineTo { x: w + h_pad, y: -h - v_pad }, - PathCommand::MoveTo { x: -h_pad, y: -h - v_pad }, - PathCommand::LineTo { x: w + h_pad, y: d + v_pad }, + PathCommand::MoveTo { + x: -h_pad, + y: d + v_pad, + }, + PathCommand::LineTo { + x: w + h_pad, + y: -h - v_pad, + }, + PathCommand::MoveTo { + x: -h_pad, + y: -h - v_pad, + }, + PathCommand::LineTo { + x: w + h_pad, + y: d + v_pad, + }, ], "\\sout" => { // Horizontal line at –0.5× x-height, extended to content edges. let mid_y = -0.5 * options.metrics().x_height; vec![ PathCommand::MoveTo { x: 0.0, y: mid_y }, - PathCommand::LineTo { x: w, y: mid_y }, + PathCommand::LineTo { x: w, y: mid_y }, ] } _ => vec![], @@ -3101,7 +3157,10 @@ fn layout_cancel( width: line_w, height: line_h, depth: line_d, - content: BoxContent::SvgPath { commands, fill: false }, + content: BoxContent::SvgPath { + commands, + fill: false, + }, color: options.color, }; @@ -3147,10 +3206,22 @@ fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox { // phasePath(y): M400000 y H0 L y/2 0 l65 45 L145 y-80 H400000z let x_peak = y_svg / 2.0; let commands = vec![ - PathCommand::MoveTo { x: right_x, y: vy(y_svg) }, - PathCommand::LineTo { x: 0.0, y: vy(y_svg) }, - PathCommand::LineTo { x: x_peak * sx, y: vy(0.0) }, - PathCommand::LineTo { x: (x_peak + 65.0) * sx, y: vy(45.0) }, + PathCommand::MoveTo { + x: right_x, + y: vy(y_svg), + }, + PathCommand::LineTo { + x: 0.0, + y: vy(y_svg), + }, + PathCommand::LineTo { + x: x_peak * sx, + y: vy(0.0), + }, + PathCommand::LineTo { + x: (x_peak + 65.0) * sx, + y: vy(45.0), + }, PathCommand::LineTo { x: 145.0 * sx, y: vy(y_svg - 80.0), @@ -3162,10 +3233,7 @@ fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox { PathCommand::Close, ]; - let body_shifted = make_hbox(vec![ - LayoutBox::new_kern(left_pad), - inner.clone(), - ]); + let body_shifted = make_hbox(vec![LayoutBox::new_kern(left_pad), inner.clone()]); let path_height = inner.height; let path_depth = bottom_y; @@ -3179,7 +3247,10 @@ fn layout_phase(body: &ParseNode, options: &LayoutOptions) -> LayoutBox { width, height: path_height, depth: path_depth, - content: BoxContent::SvgPath { commands, fill: true }, + content: BoxContent::SvgPath { + commands, + fill: true, + }, color: options.color, }, LayoutBox::new_kern(-width), @@ -3203,7 +3274,10 @@ fn layout_angl(body: &ParseNode, options: &LayoutOptions) -> LayoutBox { let path_commands = vec![ PathCommand::MoveTo { x: 0.0, y: -arc_h }, PathCommand::LineTo { x: w, y: -arc_h }, - PathCommand::LineTo { x: w, y: inner.depth + 0.3_f64}, + PathCommand::LineTo { + x: w, + y: inner.depth + 0.3_f64, + }, ]; let height = arc_h; @@ -3254,15 +3328,24 @@ fn layout_with_font(node: &ParseNode, font_id: FontId, options: &LayoutOptions) } make_hbox(children) } - ParseNode::SupSub { - base, sup, sub, .. - } => { + ParseNode::SupSub { base, sup, sub, .. } => { if let Some(base_node) = base.as_deref() { if should_use_op_limits(base_node, options) { - return layout_op_with_limits(base_node, sup.as_deref(), sub.as_deref(), options); + return layout_op_with_limits( + base_node, + sup.as_deref(), + sub.as_deref(), + options, + ); } } - layout_supsub(base.as_deref(), sup.as_deref(), sub.as_deref(), options, Some(font_id)) + layout_supsub( + base.as_deref(), + sup.as_deref(), + sub.as_deref(), + options, + Some(font_id), + ) } ParseNode::MathOrd { text, mode, .. } | ParseNode::TextOrd { text, mode, .. } @@ -3337,15 +3420,17 @@ fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox { fn layout_href(body: &[ParseNode], options: &LayoutOptions) -> LayoutBox { let link_color = Color::from_name("blue").unwrap_or_else(|| Color::rgb(0.0, 0.0, 1.0)); // Slight tracking matches KaTeX/browser monospace link width in golden PNGs. - let body_opts = options - .with_color(link_color) - .with_inter_glyph_kern(0.024); + let body_opts = options.with_color(link_color).with_inter_glyph_kern(0.024); let body_box = layout_expression(body, &body_opts, true); layout_underline_laid_out(body_box, options, link_color) } /// Same geometry as [`layout_underline`], but for an already computed inner box. -fn layout_underline_laid_out(body_box: LayoutBox, options: &LayoutOptions, color: Color) -> LayoutBox { +fn layout_underline_laid_out( + body_box: LayoutBox, + options: &LayoutOptions, + color: Color, +) -> LayoutBox { let metrics = options.metrics(); let rule = metrics.default_rule_thickness; let depth = body_box.depth + 3.0 * rule; @@ -3427,18 +3512,30 @@ fn node_math_class(node: &ParseNode) -> Option { match node { ParseNode::MathOrd { .. } | ParseNode::TextOrd { .. } => Some(MathClass::Ord), ParseNode::Atom { family, .. } => Some(family_to_math_class(*family)), - ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => Some(MathClass::Op), + ParseNode::OpToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => { + Some(MathClass::Op) + } ParseNode::OrdGroup { .. } => Some(MathClass::Ord), // KaTeX genfrac.js: with delimiters (e.g. \binom) → mord; without (e.g. \frac) → minner. - ParseNode::GenFrac { left_delim, right_delim, .. } => { - let has_delim = left_delim.as_ref().is_some_and(|d| !d.is_empty() && d != ".") - || right_delim.as_ref().is_some_and(|d| !d.is_empty() && d != "."); - if has_delim { Some(MathClass::Ord) } else { Some(MathClass::Inner) } + ParseNode::GenFrac { + left_delim, + right_delim, + .. + } => { + let has_delim = left_delim + .as_ref() + .is_some_and(|d| !d.is_empty() && d != ".") + || right_delim + .as_ref() + .is_some_and(|d| !d.is_empty() && d != "."); + if has_delim { + Some(MathClass::Ord) + } else { + Some(MathClass::Inner) + } } ParseNode::Sqrt { .. } => Some(MathClass::Ord), - ParseNode::SupSub { base, .. } => { - base.as_ref().and_then(|b| node_math_class(b)) - } + ParseNode::SupSub { base, .. } => base.as_ref().and_then(|b| node_math_class(b)), ParseNode::MClass { mclass, .. } => Some(mclass_str_to_math_class(mclass)), ParseNode::SpacingNode { .. } => None, ParseNode::Kern { .. } => None, @@ -3533,9 +3630,7 @@ fn layout_horiz_brace( let body_box = layout_node(base, options); let w = body_box.width.max(0.5); - let is_bracket = func_label - .trim_start_matches('\\') - .ends_with("bracket"); + let is_bracket = func_label.trim_start_matches('\\').ends_with("bracket"); // `\overbrace`/`\underbrace` and mathtools `\overbracket`/`\underbracket`: KaTeX stretchy SVG (filled paths). let stretch_key = if is_bracket { @@ -3671,9 +3766,9 @@ fn layout_xarrow( // KaTeX positions xarrows centered on the math axis, with a 0.111em (2mu) gap // between the arrow and the text above/below (see amsmath.dtx reference). let metrics = options.metrics(); - let axis = metrics.axis_height; // 0.25em + let axis = metrics.axis_height; // 0.25em let arrow_half = actual_arrow_h / 2.0; - let gap = 0.111; // 2mu gap (KaTeX constant) + let gap = 0.111; // 2mu gap (KaTeX constant) // Center the arrow on the math axis by shifting it up. let base_shift = -axis; @@ -3742,24 +3837,36 @@ fn layout_textcircled(body_box: LayoutBox, options: &LayoutOptions) -> LayoutBox let circle_commands = vec![ PathCommand::MoveTo { x: cx + r, y: cy }, PathCommand::CubicTo { - x1: cx + r, y1: cy - k * r, - x2: cx + k * r, y2: cy - r, - x: cx, y: cy - r, + x1: cx + r, + y1: cy - k * r, + x2: cx + k * r, + y2: cy - r, + x: cx, + y: cy - r, }, PathCommand::CubicTo { - x1: cx - k * r, y1: cy - r, - x2: cx - r, y2: cy - k * r, - x: cx - r, y: cy, + x1: cx - k * r, + y1: cy - r, + x2: cx - r, + y2: cy - k * r, + x: cx - r, + y: cy, }, PathCommand::CubicTo { - x1: cx - r, y1: cy + k * r, - x2: cx - k * r, y2: cy + r, - x: cx, y: cy + r, + x1: cx - r, + y1: cy + k * r, + x2: cx - k * r, + y2: cy + r, + x: cx, + y: cy + r, }, PathCommand::CubicTo { - x1: cx + k * r, y1: cy + r, - x2: cx + r, y2: cy + k * r, - x: cx + r, y: cy, + x1: cx + k * r, + y1: cy + r, + x2: cx + r, + y2: cy + k * r, + x: cx + r, + y: cy, }, PathCommand::Close, ]; @@ -3847,24 +3954,36 @@ fn layout_imageof_origof(imageof: bool, options: &LayoutOptions) -> LayoutBox { vec![ PathCommand::MoveTo { x: ox + rad, y: cy }, PathCommand::CubicTo { - x1: ox + rad, y1: cy - k * rad, - x2: ox + k * rad, y2: cy - rad, - x: ox, y: cy - rad, + x1: ox + rad, + y1: cy - k * rad, + x2: ox + k * rad, + y2: cy - rad, + x: ox, + y: cy - rad, }, PathCommand::CubicTo { - x1: ox - k * rad, y1: cy - rad, - x2: ox - rad, y2: cy - k * rad, - x: ox - rad, y: cy, + x1: ox - k * rad, + y1: cy - rad, + x2: ox - rad, + y2: cy - k * rad, + x: ox - rad, + y: cy, }, PathCommand::CubicTo { - x1: ox - rad, y1: cy + k * rad, - x2: ox - k * rad, y2: cy + rad, - x: ox, y: cy + rad, + x1: ox - rad, + y1: cy + k * rad, + x2: ox - k * rad, + y2: cy + rad, + x: ox, + y: cy + rad, }, PathCommand::CubicTo { - x1: ox + k * rad, y1: cy + rad, - x2: ox + rad, y2: cy + k * rad, - x: ox + rad, y: cy, + x1: ox + k * rad, + y1: cy + rad, + x2: ox + rad, + y2: cy + k * rad, + x: ox + rad, + y: cy, }, PathCommand::Close, ] @@ -3925,8 +4044,8 @@ fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec let cx = width / 2.0; let cy = (depth - height) / 2.0; // vertical center let a = width * 0.402_f64; // horizontal semi-axis (0.36 * 1.2) - let b = 0.3_f64; // vertical semi-axis (0.1 * 2) - let k = 0.62_f64; // Bezier factor: larger = fuller ellipse (0.5523 ≈ exact circle) + let b = 0.3_f64; // vertical semi-axis (0.1 * 2) + let k = 0.62_f64; // Bezier factor: larger = fuller ellipse (0.5523 ≈ exact circle) vec![ PathCommand::MoveTo { x: cx + a, y: cy }, PathCommand::CubicTo { @@ -3966,17 +4085,34 @@ fn ellipse_overlay_path(width: f64, height: f64, depth: f64) -> Vec } fn shift_path_y(cmds: Vec, dy: f64) -> Vec { - cmds.into_iter().map(|c| match c { - PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy }, - PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy }, - PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo { - x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy, - }, - PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo { - x1, y1: y1 + dy, x, y: y + dy, - }, - PathCommand::Close => PathCommand::Close, - }).collect() + cmds.into_iter() + .map(|c| match c { + PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy }, + PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy }, + PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + } => PathCommand::CubicTo { + x1, + y1: y1 + dy, + x2, + y2: y2 + dy, + x, + y: y + dy, + }, + PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo { + x1, + y1: y1 + dy, + x, + y: y + dy, + }, + PathCommand::Close => PathCommand::Close, + }) + .collect() } fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec { @@ -3989,53 +4125,110 @@ fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec { vec![ - PathCommand::MoveTo { x: ah, y: mid_y - ah }, + PathCommand::MoveTo { + x: ah, + y: mid_y - ah, + }, PathCommand::LineTo { x: 0.0, y: mid_y }, - PathCommand::LineTo { x: ah, y: mid_y + ah }, + PathCommand::LineTo { + x: ah, + y: mid_y + ah, + }, PathCommand::MoveTo { x: 0.0, y: mid_y }, PathCommand::LineTo { x: width, y: mid_y }, ] } - "\\overleftrightarrow" | "\\underleftrightarrow" - | "\\xleftrightarrow" | "\\xLeftrightarrow" => { + "\\overleftrightarrow" + | "\\underleftrightarrow" + | "\\xleftrightarrow" + | "\\xLeftrightarrow" => { vec![ - PathCommand::MoveTo { x: ah, y: mid_y - ah }, + PathCommand::MoveTo { + x: ah, + y: mid_y - ah, + }, PathCommand::LineTo { x: 0.0, y: mid_y }, - PathCommand::LineTo { x: ah, y: mid_y + ah }, + PathCommand::LineTo { + x: ah, + y: mid_y + ah, + }, PathCommand::MoveTo { x: 0.0, y: mid_y }, PathCommand::LineTo { x: width, y: mid_y }, - PathCommand::MoveTo { x: width - ah, y: mid_y - ah }, + PathCommand::MoveTo { + x: width - ah, + y: mid_y - ah, + }, PathCommand::LineTo { x: width, y: mid_y }, - PathCommand::LineTo { x: width - ah, y: mid_y + ah }, + PathCommand::LineTo { + x: width - ah, + y: mid_y + ah, + }, ] } "\\xlongequal" => { let gap = 0.04; vec![ - PathCommand::MoveTo { x: 0.0, y: mid_y - gap }, - PathCommand::LineTo { x: width, y: mid_y - gap }, - PathCommand::MoveTo { x: 0.0, y: mid_y + gap }, - PathCommand::LineTo { x: width, y: mid_y + gap }, + PathCommand::MoveTo { + x: 0.0, + y: mid_y - gap, + }, + PathCommand::LineTo { + x: width, + y: mid_y - gap, + }, + PathCommand::MoveTo { + x: 0.0, + y: mid_y + gap, + }, + PathCommand::LineTo { + x: width, + y: mid_y + gap, + }, ] } "\\xhookleftarrow" => { vec![ - PathCommand::MoveTo { x: ah, y: mid_y - ah }, + PathCommand::MoveTo { + x: ah, + y: mid_y - ah, + }, PathCommand::LineTo { x: 0.0, y: mid_y }, - PathCommand::LineTo { x: ah, y: mid_y + ah }, + PathCommand::LineTo { + x: ah, + y: mid_y + ah, + }, PathCommand::MoveTo { x: 0.0, y: mid_y }, PathCommand::LineTo { x: width, y: mid_y }, - PathCommand::QuadTo { x1: width + ah, y1: mid_y, x: width + ah, y: mid_y + ah }, + PathCommand::QuadTo { + x1: width + ah, + y1: mid_y, + x: width + ah, + y: mid_y + ah, + }, ] } "\\xhookrightarrow" => { vec![ - PathCommand::MoveTo { x: 0.0 - ah, y: mid_y - ah }, - PathCommand::QuadTo { x1: 0.0 - ah, y1: mid_y, x: 0.0, y: mid_y }, + PathCommand::MoveTo { + x: 0.0 - ah, + y: mid_y - ah, + }, + PathCommand::QuadTo { + x1: 0.0 - ah, + y1: mid_y, + x: 0.0, + y: mid_y, + }, PathCommand::LineTo { x: width, y: mid_y }, - PathCommand::MoveTo { x: width - ah, y: mid_y - ah }, + PathCommand::MoveTo { + x: width - ah, + y: mid_y - ah, + }, PathCommand::LineTo { x: width, y: mid_y }, - PathCommand::LineTo { x: width - ah, y: mid_y + ah }, + PathCommand::LineTo { + x: width - ah, + y: mid_y + ah, + }, ] } "\\xrightharpoonup" | "\\xleftharpoonup" => { @@ -4044,12 +4237,18 @@ fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec Vec Vec { let gap = 0.06; vec![ - PathCommand::MoveTo { x: 0.0, y: mid_y - gap }, - PathCommand::LineTo { x: width, y: mid_y - gap }, - PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah }, - PathCommand::LineTo { x: width, y: mid_y - gap }, - PathCommand::MoveTo { x: width, y: mid_y + gap }, - PathCommand::LineTo { x: 0.0, y: mid_y + gap }, - PathCommand::MoveTo { x: ah, y: mid_y + gap + ah }, - PathCommand::LineTo { x: 0.0, y: mid_y + gap }, + PathCommand::MoveTo { + x: 0.0, + y: mid_y - gap, + }, + PathCommand::LineTo { + x: width, + y: mid_y - gap, + }, + PathCommand::MoveTo { + x: width - ah, + y: mid_y - gap - ah, + }, + PathCommand::LineTo { + x: width, + y: mid_y - gap, + }, + PathCommand::MoveTo { + x: width, + y: mid_y + gap, + }, + PathCommand::LineTo { + x: 0.0, + y: mid_y + gap, + }, + PathCommand::MoveTo { + x: ah, + y: mid_y + gap + ah, + }, + PathCommand::LineTo { + x: 0.0, + y: mid_y + gap, + }, ] } "\\xtofrom" | "\\xrightleftarrows" => { let gap = 0.06; vec![ - PathCommand::MoveTo { x: 0.0, y: mid_y - gap }, - PathCommand::LineTo { x: width, y: mid_y - gap }, - PathCommand::MoveTo { x: width - ah, y: mid_y - gap - ah }, - PathCommand::LineTo { x: width, y: mid_y - gap }, - PathCommand::LineTo { x: width - ah, y: mid_y - gap + ah }, - PathCommand::MoveTo { x: width, y: mid_y + gap }, - PathCommand::LineTo { x: 0.0, y: mid_y + gap }, - PathCommand::MoveTo { x: ah, y: mid_y + gap - ah }, - PathCommand::LineTo { x: 0.0, y: mid_y + gap }, - PathCommand::LineTo { x: ah, y: mid_y + gap + ah }, + PathCommand::MoveTo { + x: 0.0, + y: mid_y - gap, + }, + PathCommand::LineTo { + x: width, + y: mid_y - gap, + }, + PathCommand::MoveTo { + x: width - ah, + y: mid_y - gap - ah, + }, + PathCommand::LineTo { + x: width, + y: mid_y - gap, + }, + PathCommand::LineTo { + x: width - ah, + y: mid_y - gap + ah, + }, + PathCommand::MoveTo { + x: width, + y: mid_y + gap, + }, + PathCommand::LineTo { + x: 0.0, + y: mid_y + gap, + }, + PathCommand::MoveTo { + x: ah, + y: mid_y + gap - ah, + }, + PathCommand::LineTo { + x: 0.0, + y: mid_y + gap, + }, + PathCommand::LineTo { + x: ah, + y: mid_y + gap + ah, + }, ] } "\\overlinesegment" | "\\underlinesegment" => { @@ -4110,9 +4369,15 @@ fn stretchy_accent_path(label: &str, width: f64, height: f64) -> Vec 0.25 // (at the label's own scale). Otherwise the label baseline stays fixed and // depth extends into the gap without increasing the cell height. @@ -4362,8 +4649,14 @@ fn layout_cd_arrow( 0.0 }; let height = axis + arrow_half + gap + sup_h + sup_d_contrib; - let sub_h_raw = below_box.as_ref().map(|b| b.height * sub_ratio).unwrap_or(0.0); - let sub_d_raw = below_box.as_ref().map(|b| b.depth * sub_ratio).unwrap_or(0.0); + let sub_h_raw = below_box + .as_ref() + .map(|b| b.height * sub_ratio) + .unwrap_or(0.0); + let sub_d_raw = below_box + .as_ref() + .map(|b| b.depth * sub_ratio) + .unwrap_or(0.0); let depth = if below_box.is_some() { (arrow_half - axis).max(0.0) + gap + sub_h_raw + sub_d_raw } else { @@ -4427,16 +4720,35 @@ fn layout_cd_arrow( // Side labels: KaTeX uses `style.sup()` for both left and right; scale via `Scaled` // so `to_display::RaiseBox` does not leave them at display size (unlike `OpLimits`). let left_box = label_above.map(|n| { - cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color) + cd_vcenter_side_label( + cd_side_label_scaled(n, options), + box_h, + box_d, + options.color, + ) }); let right_box = label_below.map(|n| { - cd_vcenter_side_label(cd_side_label_scaled(n, options), box_h, box_d, options.color) + cd_vcenter_side_label( + cd_side_label_scaled(n, options), + box_h, + box_d, + options.color, + ) }); let left_w = left_box.as_ref().map(|b| b.width).unwrap_or(0.0); let right_w = right_box.as_ref().map(|b| b.width).unwrap_or(0.0); - let left_part = left_w + if left_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }; - let right_part = (if right_w > 0.0 { CD_VERT_SIDE_KERN_EM } else { 0.0 }) + right_w; + let left_part = left_w + + if left_w > 0.0 { + CD_VERT_SIDE_KERN_EM + } else { + 0.0 + }; + let right_part = (if right_w > 0.0 { + CD_VERT_SIDE_KERN_EM + } else { + 0.0 + }) + right_w; let inner_w = left_part + shaft_w + right_part; // Center shaft within the column width (pass 2) using side kerns. @@ -4450,7 +4762,9 @@ fn layout_cd_arrow( }; let mut children: Vec = Vec::new(); - if kern_left > 0.0 { children.push(LayoutBox::new_kern(kern_left)); } + if kern_left > 0.0 { + children.push(LayoutBox::new_kern(kern_left)); + } if let Some(lb) = left_box { children.push(lb); children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM)); @@ -4460,7 +4774,9 @@ fn layout_cd_arrow( children.push(LayoutBox::new_kern(CD_VERT_SIDE_KERN_EM)); children.push(rb); } - if kern_right > 0.0 { children.push(LayoutBox::new_kern(kern_right)); } + if kern_right > 0.0 { + children.push(LayoutBox::new_kern(kern_right)); + } LayoutBox { width: total_w, @@ -4508,7 +4824,12 @@ fn layout_cd(body: &[Vec], options: &LayoutOptions) -> LayoutBox { for (c, cell) in row.iter().enumerate() { let cbox = match cell { - ParseNode::CdArrow { direction, label_above, label_below, .. } => { + ParseNode::CdArrow { + direction, + label_above, + label_below, + .. + } => { layout_cd_arrow( direction, label_above.as_deref(), @@ -4522,9 +4843,9 @@ fn layout_cd(body: &[Vec], options: &LayoutOptions) -> LayoutBox { // KaTeX CD object cells are `styling` nodes; `sizingGroup` builds the body with // `buildExpression(..., false)` (see katex `functions/sizing.js`), so no inter-atom // math glue inside a cell — matching that avoids spurious Ord–Bin space (e.g. golden 0963). - ParseNode::OrdGroup { body: cell_body, .. } => { - layout_expression(cell_body, options, false) - } + ParseNode::OrdGroup { + body: cell_body, .. + } => layout_expression(cell_body, options, false), other => layout_node(other, options), }; @@ -4552,7 +4873,10 @@ fn layout_cd(body: &[Vec], options: &LayoutOptions) -> LayoutBox { for (r, row) in cell_boxes.iter().enumerate() { for (c, b) in row.iter().enumerate() { if b.width > 0.0 { - eprintln!("[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", b.width, b.height, b.depth); + eprintln!( + "[CD] cell[{r}][{c}] w={:.4} h={:.4} d={:.4}", + b.width, b.height, b.depth + ); } } } @@ -4562,7 +4886,13 @@ fn layout_cd(body: &[Vec], options: &LayoutOptions) -> LayoutBox { for (r, row) in body.iter().enumerate() { let is_arrow_row = r % 2 == 1; for (c, cell) in row.iter().enumerate() { - if let ParseNode::CdArrow { direction, label_above, label_below, .. } = cell { + if let ParseNode::CdArrow { + direction, + label_above, + label_below, + .. + } = cell + { let is_horiz = matches!(direction.as_str(), "right" | "left" | "horiz_eq"); let (new_box, col_w) = if !is_arrow_row && c % 2 == 1 && is_horiz { let b = layout_cd_arrow( @@ -4637,8 +4967,8 @@ fn layout_cd(body: &[Vec], options: &LayoutOptions) -> LayoutBox { let depth = total_height - offset; // Total width: sum of col_widths + col_gap between each - let total_width = col_widths.iter().sum::() - + col_gap * (num_cols.saturating_sub(1)) as f64; + let total_width = + col_widths.iter().sum::() + col_gap * (num_cols.saturating_sub(1)) as f64; // Build hlines_before_row (all empty for CD) let hlines_before_row: Vec> = (0..=num_rows).map(|_| vec![]).collect(); @@ -4806,8 +5136,7 @@ fn layout_proof_branch(tree: &ProofBranch, options: &LayoutOptions) -> ProofTree if tree.root_at_top { let conclusion_baseline_y = conclusion.height; - let rule_y = - conclusion.height + conclusion.depth + vertical_gap + rule_thickness / 2.0; + let rule_y = conclusion.height + conclusion.depth + vertical_gap + rule_thickness / 2.0; let premise_top_y = rule_y + rule_thickness / 2.0 + vertical_gap; let premise_baseline_y = premise_top_y + premise_height; @@ -5019,22 +5348,60 @@ fn horiz_brace_path(width: f64, height: f64, is_over: bool) -> Vec if is_over { vec![ PathCommand::MoveTo { x: 0.0, y: 0.0 }, - PathCommand::QuadTo { x1: 0.0, y1: -q, x: mid * 0.4, y: -q }, - PathCommand::LineTo { x: mid - 0.05, y: -q }, + PathCommand::QuadTo { + x1: 0.0, + y1: -q, + x: mid * 0.4, + y: -q, + }, + PathCommand::LineTo { + x: mid - 0.05, + y: -q, + }, PathCommand::LineTo { x: mid, y: -height }, - PathCommand::LineTo { x: mid + 0.05, y: -q }, - PathCommand::LineTo { x: width - mid * 0.4, y: -q }, - PathCommand::QuadTo { x1: width, y1: -q, x: width, y: 0.0 }, + PathCommand::LineTo { + x: mid + 0.05, + y: -q, + }, + PathCommand::LineTo { + x: width - mid * 0.4, + y: -q, + }, + PathCommand::QuadTo { + x1: width, + y1: -q, + x: width, + y: 0.0, + }, ] } else { vec![ PathCommand::MoveTo { x: 0.0, y: 0.0 }, - PathCommand::QuadTo { x1: 0.0, y1: q, x: mid * 0.4, y: q }, - PathCommand::LineTo { x: mid - 0.05, y: q }, + PathCommand::QuadTo { + x1: 0.0, + y1: q, + x: mid * 0.4, + y: q, + }, + PathCommand::LineTo { + x: mid - 0.05, + y: q, + }, PathCommand::LineTo { x: mid, y: height }, - PathCommand::LineTo { x: mid + 0.05, y: q }, - PathCommand::LineTo { x: width - mid * 0.4, y: q }, - PathCommand::QuadTo { x1: width, y1: q, x: width, y: 0.0 }, + PathCommand::LineTo { + x: mid + 0.05, + y: q, + }, + PathCommand::LineTo { + x: width - mid * 0.4, + y: q, + }, + PathCommand::QuadTo { + x1: width, + y1: q, + x: width, + y: 0.0, + }, ] } } diff --git a/crates/ratex-layout/src/katex_svg.rs b/crates/ratex-layout/src/katex_svg.rs index 18edc667..1d015574 100644 --- a/crates/ratex-layout/src/katex_svg.rs +++ b/crates/ratex-layout/src/katex_svg.rs @@ -54,13 +54,7 @@ pub fn katex_stretchy_arrow_path( katex_stretchy_path(label, width_em).map(|(cmds, _)| cmds) } - -fn scale_cmd_twohead_uniform( - cmd: &PathCommand, - s: f64, - vb_cy: f64, - x_shift: f64, -) -> PathCommand { +fn scale_cmd_twohead_uniform(cmd: &PathCommand, s: f64, vb_cy: f64, x_shift: f64) -> PathCommand { match *cmd { PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x: x * s + x_shift, @@ -70,7 +64,14 @@ fn scale_cmd_twohead_uniform( x: x * s + x_shift, y: (y - vb_cy) * s, }, - PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo { + PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + } => PathCommand::CubicTo { x1: x1 * s + x_shift, y1: (y1 - vb_cy) * s, x2: x2 * s + x_shift, @@ -149,13 +150,26 @@ fn flatten_path_to_contours(commands: &[PathCommand]) -> Vec> { last = (x, y); current.push(last); } - PathCommand::CubicTo { x1, y1, x2, y2, x, y } => { + PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + } => { let (x0, y0) = last; for k in 1..=N { let t = k as f64 / N as f64; let u = 1.0 - t; - let x = u * u * u * x0 + 3.0 * u * u * t * x1 + 3.0 * u * t * t * x2 + t * t * t * x; - let y = u * u * u * y0 + 3.0 * u * u * t * y1 + 3.0 * u * t * t * y2 + t * t * t * y; + let x = u * u * u * x0 + + 3.0 * u * u * t * x1 + + 3.0 * u * t * t * x2 + + t * t * t * x; + let y = u * u * u * y0 + + 3.0 * u * u * t * y1 + + 3.0 * u * t * t * y2 + + t * t * t * y; last = (x, y); current.push(last); } @@ -252,14 +266,8 @@ fn scale_svg_path_thousandths(cmds: &[PathCommand]) -> Vec { let s = 0.001; cmds.iter() .map(|c| match *c { - PathCommand::MoveTo { x, y } => PathCommand::MoveTo { - x: x * s, - y: y * s, - }, - PathCommand::LineTo { x, y } => PathCommand::LineTo { - x: x * s, - y: y * s, - }, + PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x: x * s, y: y * s }, + PathCommand::LineTo { x, y } => PathCommand::LineTo { x: x * s, y: y * s }, PathCommand::CubicTo { x1, y1, @@ -333,9 +341,7 @@ fn parse_and_fit_nonuniform( let raw = parse_svg_path(svg_path); let sx = target_width_em / vb_width; let sy = target_height_em / vb_height; - raw.iter() - .map(|c| scale_cmd_xy(c, sx, sy)) - .collect() + raw.iter().map(|c| scale_cmd_xy(c, sx, sy)).collect() } /// Build the overgroup/undergroup filled path from KaTeX leftgroup/rightgroup data. @@ -365,54 +371,90 @@ fn build_overgroup(width_em: f64, h_em: f64, is_over: bool) -> Vec if is_over { vec![ // Start at top-left, after corner - PathCommand::MoveTo { x: corner_em, y: ly(80.0) }, + PathCommand::MoveTo { + x: corner_em, + y: ly(80.0), + }, // Left outer curve down PathCommand::CubicTo { - x1: 64.0 * cx, y1: ly(80.0), - x2: 168.3 * cx, y2: ly(229.4), - x: 21.0 * cx, y: ly(260.0), + x1: 64.0 * cx, + y1: ly(80.0), + x2: 168.3 * cx, + y2: ly(229.4), + x: 21.0 * cx, + y: ly(260.0), }, PathCommand::CubicTo { - x1: 15.1 * cx, y1: ly(261.2), - x2: 3.0 * cx, y2: ly(260.0), - x: 3.0 * cx, y: ly(260.0), + x1: 15.1 * cx, + y1: ly(261.2), + x2: 3.0 * cx, + y2: ly(260.0), + x: 3.0 * cx, + y: ly(260.0), }, PathCommand::CubicTo { - x1: 1.0 * cx, y1: ly(260.0), - x2: 0.0, y2: ly(259.0), - x: 0.0, y: ly(257.0), + x1: 1.0 * cx, + y1: ly(260.0), + x2: 0.0, + y2: ly(259.0), + x: 0.0, + y: ly(257.0), + }, + PathCommand::LineTo { + x: 0.0, + y: ly(219.0), }, - PathCommand::LineTo { x: 0.0, y: ly(219.0) }, // Left inner curve up PathCommand::CubicTo { - x1: 76.0 * cx, y1: ly(61.0), - x2: 257.0 * cx, y2: ly(0.0), - x: corner_em, y: ly(0.0), + x1: 76.0 * cx, + y1: ly(61.0), + x2: 257.0 * cx, + y2: ly(0.0), + x: corner_em, + y: ly(0.0), }, // Bottom flat section - PathCommand::LineTo { x: width_em - corner_em, y: ly(0.0) }, + PathCommand::LineTo { + x: width_em - corner_em, + y: ly(0.0), + }, // Right inner curve down PathCommand::CubicTo { - x1: width_em - 257.0 * cx, y1: ly(0.0), - x2: width_em - 76.0 * cx, y2: ly(61.0), - x: width_em, y: ly(219.0), + x1: width_em - 257.0 * cx, + y1: ly(0.0), + x2: width_em - 76.0 * cx, + y2: ly(61.0), + x: width_em, + y: ly(219.0), + }, + PathCommand::LineTo { + x: width_em, + y: ly(257.0), }, - PathCommand::LineTo { x: width_em, y: ly(257.0) }, // Right outer curves up PathCommand::CubicTo { - x1: width_em, y1: ly(259.0), - x2: width_em - 1.0 * cx, y2: ly(260.0), - x: width_em - 3.0 * cx, y: ly(260.0), + x1: width_em, + y1: ly(259.0), + x2: width_em - 1.0 * cx, + y2: ly(260.0), + x: width_em - 3.0 * cx, + y: ly(260.0), }, PathCommand::CubicTo { - x1: width_em - 3.0 * cx, y1: ly(260.0), - x2: width_em - 15.1 * cx, y2: ly(261.2), - x: width_em - 21.0 * cx, y: ly(260.0), + x1: width_em - 3.0 * cx, + y1: ly(260.0), + x2: width_em - 15.1 * cx, + y2: ly(261.2), + x: width_em - 21.0 * cx, + y: ly(260.0), }, PathCommand::CubicTo { - x1: width_em - 168.3 * cx, y1: ly(229.4), - x2: width_em - 64.0 * cx, y2: ly(80.0), - x: width_em - corner_em, y: ly(80.0), + x1: width_em - 168.3 * cx, + y1: ly(229.4), + x2: width_em - 64.0 * cx, + y2: ly(80.0), + x: width_em - corner_em, + y: ly(80.0), }, PathCommand::Close, ] @@ -422,49 +464,85 @@ fn build_overgroup(width_em: f64, h_em: f64, is_over: bool) -> Vec // (435, 262) → C(64,262)(168.3,112.6)(21,82) → curves → (0,85) → V(0,123) // Inner: (0,123) → C(76,281)(257,342)(435,342) → H → close vec![ - PathCommand::MoveTo { x: corner_em, y: ly(262.0) }, + PathCommand::MoveTo { + x: corner_em, + y: ly(262.0), + }, PathCommand::CubicTo { - x1: 64.0 * cx, y1: ly(262.0), - x2: 168.3 * cx, y2: ly(112.6), - x: 21.0 * cx, y: ly(82.0), + x1: 64.0 * cx, + y1: ly(262.0), + x2: 168.3 * cx, + y2: ly(112.6), + x: 21.0 * cx, + y: ly(82.0), }, PathCommand::CubicTo { - x1: 15.1 * cx, y1: ly(80.8), - x2: 3.0 * cx, y2: ly(82.0), - x: 3.0 * cx, y: ly(82.0), + x1: 15.1 * cx, + y1: ly(80.8), + x2: 3.0 * cx, + y2: ly(82.0), + x: 3.0 * cx, + y: ly(82.0), }, PathCommand::CubicTo { - x1: 1.0 * cx, y1: ly(82.0), - x2: 0.0, y2: ly(83.0), - x: 0.0, y: ly(85.0), + x1: 1.0 * cx, + y1: ly(82.0), + x2: 0.0, + y2: ly(83.0), + x: 0.0, + y: ly(85.0), + }, + PathCommand::LineTo { + x: 0.0, + y: ly(123.0), }, - PathCommand::LineTo { x: 0.0, y: ly(123.0) }, PathCommand::CubicTo { - x1: 76.0 * cx, y1: ly(281.0), - x2: 257.0 * cx, y2: ly(342.0), - x: corner_em, y: ly(342.0), + x1: 76.0 * cx, + y1: ly(281.0), + x2: 257.0 * cx, + y2: ly(342.0), + x: corner_em, + y: ly(342.0), + }, + PathCommand::LineTo { + x: width_em - corner_em, + y: ly(342.0), }, - PathCommand::LineTo { x: width_em - corner_em, y: ly(342.0) }, PathCommand::CubicTo { - x1: width_em - 257.0 * cx, y1: ly(342.0), - x2: width_em - 76.0 * cx, y2: ly(281.0), - x: width_em, y: ly(123.0), + x1: width_em - 257.0 * cx, + y1: ly(342.0), + x2: width_em - 76.0 * cx, + y2: ly(281.0), + x: width_em, + y: ly(123.0), + }, + PathCommand::LineTo { + x: width_em, + y: ly(85.0), }, - PathCommand::LineTo { x: width_em, y: ly(85.0) }, PathCommand::CubicTo { - x1: width_em, y1: ly(83.0), - x2: width_em - 1.0 * cx, y2: ly(82.0), - x: width_em - 3.0 * cx, y: ly(82.0), + x1: width_em, + y1: ly(83.0), + x2: width_em - 1.0 * cx, + y2: ly(82.0), + x: width_em - 3.0 * cx, + y: ly(82.0), }, PathCommand::CubicTo { - x1: width_em - 3.0 * cx, y1: ly(82.0), - x2: width_em - 15.1 * cx, y2: ly(80.8), - x: width_em - 21.0 * cx, y: ly(82.0), + x1: width_em - 3.0 * cx, + y1: ly(82.0), + x2: width_em - 15.1 * cx, + y2: ly(80.8), + x: width_em - 21.0 * cx, + y: ly(82.0), }, PathCommand::CubicTo { - x1: width_em - 168.3 * cx, y1: ly(112.6), - x2: width_em - 64.0 * cx, y2: ly(262.0), - x: width_em - corner_em, y: ly(262.0), + x1: width_em - 168.3 * cx, + y1: ly(112.6), + x2: width_em - 64.0 * cx, + y2: ly(262.0), + x: width_em - corner_em, + y: ly(262.0), }, PathCommand::Close, ] @@ -473,16 +551,34 @@ fn build_overgroup(width_em: f64, h_em: f64, is_over: bool) -> Vec fn scale_cmd_xy(cmd: &PathCommand, sx: f64, sy: f64) -> PathCommand { match *cmd { - PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x: x * sx, y: y * sy }, - PathCommand::LineTo { x, y } => PathCommand::LineTo { x: x * sx, y: y * sy }, - PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo { - x1: x1 * sx, y1: y1 * sy, - x2: x2 * sx, y2: y2 * sy, - x: x * sx, y: y * sy, + PathCommand::MoveTo { x, y } => PathCommand::MoveTo { + x: x * sx, + y: y * sy, + }, + PathCommand::LineTo { x, y } => PathCommand::LineTo { + x: x * sx, + y: y * sy, + }, + PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + } => PathCommand::CubicTo { + x1: x1 * sx, + y1: y1 * sy, + x2: x2 * sx, + y2: y2 * sy, + x: x * sx, + y: y * sy, }, PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo { - x1: x1 * sx, y1: y1 * sy, - x: x * sx, y: y * sy, + x1: x1 * sx, + y1: y1 * sy, + x: x * sx, + y: y * sy, }, PathCommand::Close => PathCommand::Close, } @@ -508,34 +604,41 @@ fn parse_svg_path(d: &str) -> Vec { while i < tokens.len() { let cmd_byte = match &tokens[i] { - Token::Cmd(c) => { i += 1; *c } + Token::Cmd(c) => { + i += 1; + *c + } Token::Num(_) => last_cmd, }; match cmd_byte { b'M' => { let (x, y) = read2(&tokens, &mut i); - cx = x; cy = y; + cx = x; + cy = y; subpath_start = (cx, cy); cmds.push(PathCommand::MoveTo { x, y }); last_cmd = b'L'; } b'm' => { let (dx, dy) = read2(&tokens, &mut i); - cx += dx; cy += dy; + cx += dx; + cy += dy; subpath_start = (cx, cy); cmds.push(PathCommand::MoveTo { x: cx, y: cy }); last_cmd = b'l'; } b'L' => { let (x, y) = read2(&tokens, &mut i); - cx = x; cy = y; + cx = x; + cy = y; cmds.push(PathCommand::LineTo { x, y }); last_cmd = b'L'; } b'l' => { let (dx, dy) = read2(&tokens, &mut i); - cx += dx; cy += dy; + cx += dx; + cy += dy; cmds.push(PathCommand::LineTo { x: cx, y: cy }); last_cmd = b'l'; } @@ -567,50 +670,95 @@ fn parse_svg_path(d: &str) -> Vec { let (x1, y1) = read2(&tokens, &mut i); let (x2, y2) = read2(&tokens, &mut i); let (x, y) = read2(&tokens, &mut i); - cx = x; cy = y; - cmds.push(PathCommand::CubicTo { x1, y1, x2, y2, x, y }); + cx = x; + cy = y; + cmds.push(PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + }); last_cmd = b'C'; } b'c' => { let (dx1, dy1) = read2(&tokens, &mut i); let (dx2, dy2) = read2(&tokens, &mut i); let (dx, dy) = read2(&tokens, &mut i); - let x1 = cx + dx1; let y1 = cy + dy1; - let x2 = cx + dx2; let y2 = cy + dy2; - cx += dx; cy += dy; - cmds.push(PathCommand::CubicTo { x1, y1, x2, y2, x: cx, y: cy }); + let x1 = cx + dx1; + let y1 = cy + dy1; + let x2 = cx + dx2; + let y2 = cy + dy2; + cx += dx; + cy += dy; + cmds.push(PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x: cx, + y: cy, + }); last_cmd = b'c'; } b'S' => { let (x2, y2) = read2(&tokens, &mut i); let (x, y) = read2(&tokens, &mut i); - let x1 = cx; let y1 = cy; - cx = x; cy = y; - cmds.push(PathCommand::CubicTo { x1, y1, x2, y2, x, y }); + let x1 = cx; + let y1 = cy; + cx = x; + cy = y; + cmds.push(PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + }); last_cmd = b'S'; } b's' => { let (dx2, dy2) = read2(&tokens, &mut i); let (dx, dy) = read2(&tokens, &mut i); - let x1 = cx; let y1 = cy; - let x2 = cx + dx2; let y2 = cy + dy2; - cx += dx; cy += dy; - cmds.push(PathCommand::CubicTo { x1, y1, x2, y2, x: cx, y: cy }); + let x1 = cx; + let y1 = cy; + let x2 = cx + dx2; + let y2 = cy + dy2; + cx += dx; + cy += dy; + cmds.push(PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x: cx, + y: cy, + }); last_cmd = b's'; } b'Q' => { let (x1, y1) = read2(&tokens, &mut i); let (x, y) = read2(&tokens, &mut i); - cx = x; cy = y; + cx = x; + cy = y; cmds.push(PathCommand::QuadTo { x1, y1, x, y }); last_cmd = b'Q'; } b'q' => { let (dx1, dy1) = read2(&tokens, &mut i); let (dx, dy) = read2(&tokens, &mut i); - let x1 = cx + dx1; let y1 = cy + dy1; - cx += dx; cy += dy; - cmds.push(PathCommand::QuadTo { x1, y1, x: cx, y: cy }); + let x1 = cx + dx1; + let y1 = cy + dy1; + cx += dx; + cy += dy; + cmds.push(PathCommand::QuadTo { + x1, + y1, + x: cx, + y: cy, + }); last_cmd = b'q'; } b'Z' | b'z' => { @@ -620,7 +768,9 @@ fn parse_svg_path(d: &str) -> Vec { cy = subpath_start.1; last_cmd = b'M'; } - _ => { i += 1; } + _ => { + i += 1; + } } } cmds @@ -643,13 +793,20 @@ fn tokenize_svg(d: &str) -> Vec { i += 1; } else if b == b'-' || b == b'.' || b.is_ascii_digit() { let start = i; - if b == b'-' { i += 1; } + if b == b'-' { + i += 1; + } let mut has_dot = false; while i < bytes.len() && (bytes[i].is_ascii_digit() || (bytes[i] == b'.' && !has_dot)) { - if bytes[i] == b'.' { has_dot = true; } + if bytes[i] == b'.' { + has_dot = true; + } i += 1; } - if let Ok(n) = std::str::from_utf8(&bytes[start..i]).unwrap_or("0").parse::() { + if let Ok(n) = std::str::from_utf8(&bytes[start..i]) + .unwrap_or("0") + .parse::() + { tokens.push(Token::Num(n)); } } else { @@ -661,7 +818,10 @@ fn tokenize_svg(d: &str) -> Vec { fn read1(tokens: &[Token], i: &mut usize) -> f64 { if *i < tokens.len() { - if let Token::Num(n) = tokens[*i] { *i += 1; return n; } + if let Token::Num(n) = tokens[*i] { + *i += 1; + return n; + } } 0.0 } @@ -813,13 +973,16 @@ const BARABOVELEFTARROW: &str = "M400000 620h-399890l3-3c68.7-52.7 113.7-120 135 const RIGHTARROWABOVEBAR: &str = "M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67 151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z"; // longequal (viewBox 400000×334) -const LONGEQUAL: &str = "M0 50 h400000 v40H0z m0 194h400000v40H0z M0 50 h400000 v40H0z m0 194h400000v40H0z"; +const LONGEQUAL: &str = + "M0 50 h400000 v40H0z m0 194h400000v40H0z M0 50 h400000 v40H0z m0 194h400000v40H0z"; // leftlinesegment (viewBox 400000×522) -const LEFTLINESEGMENT: &str = "M40 281 V428 H0 V94 H40 V241 H400000 v40z M40 281 V428 H0 V94 H40 V241 H400000 v40z"; +const LEFTLINESEGMENT: &str = + "M40 281 V428 H0 V94 H40 V241 H400000 v40z M40 281 V428 H0 V94 H40 V241 H400000 v40z"; // rightlinesegment (viewBox 400000×522) -const RIGHTLINESEGMENT: &str = "M399960 241 V94 h40 V428 h-40 V281 H0 v-40z M399960 241 V94 h40 V428 h-40 V281 H0 v-40z"; +const RIGHTLINESEGMENT: &str = + "M399960 241 V94 h40 V428 h-40 V281 H0 v-40z M399960 241 V94 h40 V428 h-40 V281 H0 v-40z"; // leftgroup (for \overgroup, viewBox 400000×342) const LEFTGROUP: &str = "M400000 80 H435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0 435 0h399565z"; @@ -857,50 +1020,250 @@ struct KatexImageData { fn katex_image_data(label: &str) -> Option { let key = label.trim_start_matches('\\'); match key { - "overrightarrow" => Some(KatexImageData { paths: &["rightarrow"], min_width: 0.888, vb_height: 522.0, align: Some("xMaxYMin") }), - "overleftarrow" => Some(KatexImageData { paths: &["leftarrow"], min_width: 0.888, vb_height: 522.0, align: Some("xMinYMin") }), - "underrightarrow" => Some(KatexImageData { paths: &["rightarrow"], min_width: 0.888, vb_height: 522.0, align: Some("xMaxYMin") }), - "underleftarrow" => Some(KatexImageData { paths: &["leftarrow"], min_width: 0.888, vb_height: 522.0, align: Some("xMinYMin") }), - "xrightarrow" => Some(KatexImageData { paths: &["rightarrow"], min_width: 1.469, vb_height: 522.0, align: Some("xMaxYMin") }), + "overrightarrow" => Some(KatexImageData { + paths: &["rightarrow"], + min_width: 0.888, + vb_height: 522.0, + align: Some("xMaxYMin"), + }), + "overleftarrow" => Some(KatexImageData { + paths: &["leftarrow"], + min_width: 0.888, + vb_height: 522.0, + align: Some("xMinYMin"), + }), + "underrightarrow" => Some(KatexImageData { + paths: &["rightarrow"], + min_width: 0.888, + vb_height: 522.0, + align: Some("xMaxYMin"), + }), + "underleftarrow" => Some(KatexImageData { + paths: &["leftarrow"], + min_width: 0.888, + vb_height: 522.0, + align: Some("xMinYMin"), + }), + "xrightarrow" => Some(KatexImageData { + paths: &["rightarrow"], + min_width: 1.469, + vb_height: 522.0, + align: Some("xMaxYMin"), + }), // KaTeX `stretchy.js` `katexImagesData`: `\\cdrightarrow` / `\\cdlongequal` use minWidth 3.0em // (comment “CD minwwidth2.5pc” — amscd `\minCDarrowwidth`, ~2.5pc ≈ 3em at 10pt). - "cdrightarrow" => Some(KatexImageData { paths: &["rightarrow"], min_width: 3.0, vb_height: 522.0, align: Some("xMaxYMin") }), - "cdleftarrow" => Some(KatexImageData { paths: &["leftarrow"], min_width: 3.0, vb_height: 522.0, align: Some("xMinYMin") }), - "xleftarrow" => Some(KatexImageData { paths: &["leftarrow"], min_width: 1.469, vb_height: 522.0, align: Some("xMinYMin") }), - "Overrightarrow" => Some(KatexImageData { paths: &["doublerightarrow"], min_width: 0.888, vb_height: 560.0, align: Some("xMaxYMin") }), - "xRightarrow" => Some(KatexImageData { paths: &["doublerightarrow"], min_width: 1.526, vb_height: 560.0, align: Some("xMaxYMin") }), - "xLeftarrow" => Some(KatexImageData { paths: &["doubleleftarrow"], min_width: 1.526, vb_height: 560.0, align: Some("xMinYMin") }), - "overleftharpoon" | "xleftharpoonup" => Some(KatexImageData { paths: &["leftharpoon"], min_width: 0.888, vb_height: 522.0, align: Some("xMinYMin") }), - "xleftharpoondown" => Some(KatexImageData { paths: &["leftharpoondown"], min_width: 0.888, vb_height: 522.0, align: Some("xMinYMin") }), - "overrightharpoon" | "xrightharpoonup" => Some(KatexImageData { paths: &["rightharpoon"], min_width: 0.888, vb_height: 522.0, align: Some("xMaxYMin") }), - "xrightharpoondown" => Some(KatexImageData { paths: &["rightharpoondown"], min_width: 0.888, vb_height: 522.0, align: Some("xMaxYMin") }), - "xlongequal" => Some(KatexImageData { paths: &["longequal"], min_width: 0.888, vb_height: 334.0, align: Some("xMinYMin") }), - "cdlongequal" => Some(KatexImageData { paths: &["longequal"], min_width: 3.0, vb_height: 334.0, align: Some("xMinYMin") }), - "xtwoheadleftarrow" => Some(KatexImageData { paths: &["twoheadleftarrow"], min_width: 0.888, vb_height: 334.0, align: Some("xMinYMin") }), - "xtwoheadrightarrow" => Some(KatexImageData { paths: &["twoheadrightarrow"], min_width: 0.888, vb_height: 334.0, align: Some("xMaxYMin") }), - "overleftrightarrow" => Some(KatexImageData { paths: &["leftarrow", "rightarrow"], min_width: 0.888, vb_height: 522.0, align: None }), - "underleftrightarrow" => Some(KatexImageData { paths: &["leftarrow", "rightarrow"], min_width: 0.888, vb_height: 522.0, align: None }), - "xleftrightarrow" => Some(KatexImageData { paths: &["leftarrow", "rightarrow"], min_width: 1.75, vb_height: 522.0, align: None }), - "xLeftrightarrow" => Some(KatexImageData { paths: &["doubleleftarrow", "doublerightarrow"], min_width: 1.75, vb_height: 560.0, align: None }), - "xrightleftharpoons" => Some(KatexImageData { paths: &["leftharpoondownplus", "rightharpoonplus"], min_width: 1.75, vb_height: 716.0, align: None }), - "xleftrightharpoons" => Some(KatexImageData { paths: &["leftharpoonplus", "rightharpoondownplus"], min_width: 1.75, vb_height: 716.0, align: None }), - "xrightequilibrium" => Some(KatexImageData { paths: &["baraboveshortleftharpoon", "rightharpoonaboveshortbar"], min_width: 1.75, vb_height: 716.0, align: None }), - "xleftequilibrium" => Some(KatexImageData { paths: &["shortbaraboveleftharpoon", "shortrightharpoonabovebar"], min_width: 1.75, vb_height: 716.0, align: None }), - "xhookleftarrow" => Some(KatexImageData { paths: &["leftarrow", "righthook"], min_width: 1.08, vb_height: 522.0, align: None }), - "xhookrightarrow" => Some(KatexImageData { paths: &["lefthook", "rightarrow"], min_width: 1.08, vb_height: 522.0, align: None }), - "overlinesegment" => Some(KatexImageData { paths: &["leftlinesegment", "rightlinesegment"], min_width: 0.888, vb_height: 522.0, align: None }), - "underlinesegment" => Some(KatexImageData { paths: &["leftlinesegment", "rightlinesegment"], min_width: 0.888, vb_height: 522.0, align: None }), - "overgroup" => Some(KatexImageData { paths: &["leftgroup", "rightgroup"], min_width: 0.888, vb_height: 342.0, align: None }), - "undergroup" => Some(KatexImageData { paths: &["leftgroupunder", "rightgroupunder"], min_width: 0.888, vb_height: 342.0, align: None }), - "xmapsto" => Some(KatexImageData { paths: &["leftmapsto", "rightarrow"], min_width: 1.5, vb_height: 522.0, align: None }), - "xtofrom" => Some(KatexImageData { paths: &["leftToFrom", "rightToFrom"], min_width: 1.75, vb_height: 528.0, align: None }), - "xrightleftarrows" => Some(KatexImageData { paths: &["baraboveleftarrow", "rightarrowabovebar"], min_width: 1.75, vb_height: 901.0, align: None }), + "cdrightarrow" => Some(KatexImageData { + paths: &["rightarrow"], + min_width: 3.0, + vb_height: 522.0, + align: Some("xMaxYMin"), + }), + "cdleftarrow" => Some(KatexImageData { + paths: &["leftarrow"], + min_width: 3.0, + vb_height: 522.0, + align: Some("xMinYMin"), + }), + "xleftarrow" => Some(KatexImageData { + paths: &["leftarrow"], + min_width: 1.469, + vb_height: 522.0, + align: Some("xMinYMin"), + }), + "Overrightarrow" => Some(KatexImageData { + paths: &["doublerightarrow"], + min_width: 0.888, + vb_height: 560.0, + align: Some("xMaxYMin"), + }), + "xRightarrow" => Some(KatexImageData { + paths: &["doublerightarrow"], + min_width: 1.526, + vb_height: 560.0, + align: Some("xMaxYMin"), + }), + "xLeftarrow" => Some(KatexImageData { + paths: &["doubleleftarrow"], + min_width: 1.526, + vb_height: 560.0, + align: Some("xMinYMin"), + }), + "overleftharpoon" | "xleftharpoonup" => Some(KatexImageData { + paths: &["leftharpoon"], + min_width: 0.888, + vb_height: 522.0, + align: Some("xMinYMin"), + }), + "xleftharpoondown" => Some(KatexImageData { + paths: &["leftharpoondown"], + min_width: 0.888, + vb_height: 522.0, + align: Some("xMinYMin"), + }), + "overrightharpoon" | "xrightharpoonup" => Some(KatexImageData { + paths: &["rightharpoon"], + min_width: 0.888, + vb_height: 522.0, + align: Some("xMaxYMin"), + }), + "xrightharpoondown" => Some(KatexImageData { + paths: &["rightharpoondown"], + min_width: 0.888, + vb_height: 522.0, + align: Some("xMaxYMin"), + }), + "xlongequal" => Some(KatexImageData { + paths: &["longequal"], + min_width: 0.888, + vb_height: 334.0, + align: Some("xMinYMin"), + }), + "cdlongequal" => Some(KatexImageData { + paths: &["longequal"], + min_width: 3.0, + vb_height: 334.0, + align: Some("xMinYMin"), + }), + "xtwoheadleftarrow" => Some(KatexImageData { + paths: &["twoheadleftarrow"], + min_width: 0.888, + vb_height: 334.0, + align: Some("xMinYMin"), + }), + "xtwoheadrightarrow" => Some(KatexImageData { + paths: &["twoheadrightarrow"], + min_width: 0.888, + vb_height: 334.0, + align: Some("xMaxYMin"), + }), + "overleftrightarrow" => Some(KatexImageData { + paths: &["leftarrow", "rightarrow"], + min_width: 0.888, + vb_height: 522.0, + align: None, + }), + "underleftrightarrow" => Some(KatexImageData { + paths: &["leftarrow", "rightarrow"], + min_width: 0.888, + vb_height: 522.0, + align: None, + }), + "xleftrightarrow" => Some(KatexImageData { + paths: &["leftarrow", "rightarrow"], + min_width: 1.75, + vb_height: 522.0, + align: None, + }), + "xLeftrightarrow" => Some(KatexImageData { + paths: &["doubleleftarrow", "doublerightarrow"], + min_width: 1.75, + vb_height: 560.0, + align: None, + }), + "xrightleftharpoons" => Some(KatexImageData { + paths: &["leftharpoondownplus", "rightharpoonplus"], + min_width: 1.75, + vb_height: 716.0, + align: None, + }), + "xleftrightharpoons" => Some(KatexImageData { + paths: &["leftharpoonplus", "rightharpoondownplus"], + min_width: 1.75, + vb_height: 716.0, + align: None, + }), + "xrightequilibrium" => Some(KatexImageData { + paths: &["baraboveshortleftharpoon", "rightharpoonaboveshortbar"], + min_width: 1.75, + vb_height: 716.0, + align: None, + }), + "xleftequilibrium" => Some(KatexImageData { + paths: &["shortbaraboveleftharpoon", "shortrightharpoonabovebar"], + min_width: 1.75, + vb_height: 716.0, + align: None, + }), + "xhookleftarrow" => Some(KatexImageData { + paths: &["leftarrow", "righthook"], + min_width: 1.08, + vb_height: 522.0, + align: None, + }), + "xhookrightarrow" => Some(KatexImageData { + paths: &["lefthook", "rightarrow"], + min_width: 1.08, + vb_height: 522.0, + align: None, + }), + "overlinesegment" => Some(KatexImageData { + paths: &["leftlinesegment", "rightlinesegment"], + min_width: 0.888, + vb_height: 522.0, + align: None, + }), + "underlinesegment" => Some(KatexImageData { + paths: &["leftlinesegment", "rightlinesegment"], + min_width: 0.888, + vb_height: 522.0, + align: None, + }), + "overgroup" => Some(KatexImageData { + paths: &["leftgroup", "rightgroup"], + min_width: 0.888, + vb_height: 342.0, + align: None, + }), + "undergroup" => Some(KatexImageData { + paths: &["leftgroupunder", "rightgroupunder"], + min_width: 0.888, + vb_height: 342.0, + align: None, + }), + "xmapsto" => Some(KatexImageData { + paths: &["leftmapsto", "rightarrow"], + min_width: 1.5, + vb_height: 522.0, + align: None, + }), + "xtofrom" => Some(KatexImageData { + paths: &["leftToFrom", "rightToFrom"], + min_width: 1.75, + vb_height: 528.0, + align: None, + }), + "xrightleftarrows" => Some(KatexImageData { + paths: &["baraboveleftarrow", "rightarrowabovebar"], + min_width: 1.75, + vb_height: 901.0, + align: None, + }), // Overbrace/underbrace: KaTeX Size4 glyphs (viewBox 400000×548), same 3-piece horizontal joining as stretchy arrows. - "overbrace" => Some(KatexImageData { paths: &["leftbrace", "midbrace", "rightbrace"], min_width: 0.888, vb_height: 548.0, align: None }), - "underbrace" => Some(KatexImageData { paths: &["leftbraceunder", "midbraceunder", "rightbraceunder"], min_width: 0.888, vb_height: 548.0, align: None }), + "overbrace" => Some(KatexImageData { + paths: &["leftbrace", "midbrace", "rightbrace"], + min_width: 0.888, + vb_height: 548.0, + align: None, + }), + "underbrace" => Some(KatexImageData { + paths: &["leftbraceunder", "midbraceunder", "rightbraceunder"], + min_width: 0.888, + vb_height: 548.0, + align: None, + }), // mathtools `\overbracket` / `\underbracket`: 2-piece KaTeX SVG (same minWidth/height as KaTeX `stretchy.ts`). - "overbracket" => Some(KatexImageData { paths: &["leftbracketover", "rightbracketover"], min_width: 1.6, vb_height: 440.0, align: None }), - "underbracket" => Some(KatexImageData { paths: &["leftbracketunder", "rightbracketunder"], min_width: 1.6, vb_height: 410.0, align: None }), + "overbracket" => Some(KatexImageData { + paths: &["leftbracketover", "rightbracketover"], + min_width: 1.6, + vb_height: 440.0, + align: None, + }), + "underbracket" => Some(KatexImageData { + paths: &["leftbracketunder", "rightbracketunder"], + min_width: 1.6, + vb_height: 410.0, + align: None, + }), _ => None, } } @@ -914,48 +1277,48 @@ pub(crate) fn katex_stretchy_min_width_em(label: &str) -> Option { /// Look up the SVG path data for a named path key. fn path_for_name(name: &str) -> Option<&'static str> { match name { - "rightarrow" => Some(RIGHTARROW), - "leftarrow" => Some(LEFTARROW), - "doubleleftarrow" => Some(DOUBLELEFTARROW), - "doublerightarrow" => Some(DOUBLERIGHTARROW), - "leftharpoon" => Some(LEFTHARPOON), - "leftharpoonplus" => Some(LEFTHARPOONPLUS), - "leftharpoondown" => Some(LEFTHARPOONDOWN), - "leftharpoondownplus" => Some(LEFTHARPOONDOWNPLUS), - "rightharpoon" => Some(RIGHTHARPOON), - "rightharpoonplus" => Some(RIGHTHARPOONPLUS), - "rightharpoondown" => Some(RIGHTHARPOONDOWN), + "rightarrow" => Some(RIGHTARROW), + "leftarrow" => Some(LEFTARROW), + "doubleleftarrow" => Some(DOUBLELEFTARROW), + "doublerightarrow" => Some(DOUBLERIGHTARROW), + "leftharpoon" => Some(LEFTHARPOON), + "leftharpoonplus" => Some(LEFTHARPOONPLUS), + "leftharpoondown" => Some(LEFTHARPOONDOWN), + "leftharpoondownplus" => Some(LEFTHARPOONDOWNPLUS), + "rightharpoon" => Some(RIGHTHARPOON), + "rightharpoonplus" => Some(RIGHTHARPOONPLUS), + "rightharpoondown" => Some(RIGHTHARPOONDOWN), "rightharpoondownplus" => Some(RIGHTHARPOONDOWNPLUS), "baraboveshortleftharpoon" => Some(BARABOVESHORTLEFTHARPOON), "rightharpoonaboveshortbar" => Some(RIGHTHARPOONABOVESHORTBAR), "shortbaraboveleftharpoon" => Some(SHORTBARABOVELEFTHARPOON), "shortrightharpoonabovebar" => Some(SHORTRIGHTHARPOONABOVEBAR), - "lefthook" => Some(LEFTHOOK), - "righthook" => Some(RIGHTHOOK), - "leftbrace" => Some(LEFTBRACE), - "midbrace" => Some(MIDBRACE), - "rightbrace" => Some(RIGHTBRACE), - "leftbraceunder" => Some(LEFTBRACEUNDER), - "midbraceunder" => Some(MIDBRACEUNDER), - "rightbraceunder" => Some(RIGHTBRACEUNDER), - "leftToFrom" => Some(LEFTTOFROM), - "rightToFrom" => Some(RIGHTTOFROM), - "baraboveleftarrow" => Some(BARABOVELEFTARROW), - "rightarrowabovebar" => Some(RIGHTARROWABOVEBAR), - "longequal" => Some(LONGEQUAL), - "leftlinesegment" => Some(LEFTLINESEGMENT), - "rightlinesegment" => Some(RIGHTLINESEGMENT), - "leftmapsto" => Some(LEFTMAPSTO), - "twoheadleftarrow" => Some(TWOHEADLEFTARROW), - "twoheadrightarrow" => Some(TWOHEADRIGHTARROW), - "leftgroup" => Some(LEFTGROUP), - "leftgroupunder" => Some(LEFTGROUPUNDER), - "rightgroup" => Some(RIGHTGROUP), - "rightgroupunder" => Some(RIGHTGROUPUNDER), - "leftbracketover" => Some(LEFTBRACKETOVER), - "rightbracketover" => Some(RIGHTBRACKETOVER), - "leftbracketunder" => Some(LEFTBRACKETUNDER), - "rightbracketunder" => Some(RIGHTBRACKETUNDER), + "lefthook" => Some(LEFTHOOK), + "righthook" => Some(RIGHTHOOK), + "leftbrace" => Some(LEFTBRACE), + "midbrace" => Some(MIDBRACE), + "rightbrace" => Some(RIGHTBRACE), + "leftbraceunder" => Some(LEFTBRACEUNDER), + "midbraceunder" => Some(MIDBRACEUNDER), + "rightbraceunder" => Some(RIGHTBRACEUNDER), + "leftToFrom" => Some(LEFTTOFROM), + "rightToFrom" => Some(RIGHTTOFROM), + "baraboveleftarrow" => Some(BARABOVELEFTARROW), + "rightarrowabovebar" => Some(RIGHTARROWABOVEBAR), + "longequal" => Some(LONGEQUAL), + "leftlinesegment" => Some(LEFTLINESEGMENT), + "rightlinesegment" => Some(RIGHTLINESEGMENT), + "leftmapsto" => Some(LEFTMAPSTO), + "twoheadleftarrow" => Some(TWOHEADLEFTARROW), + "twoheadrightarrow" => Some(TWOHEADRIGHTARROW), + "leftgroup" => Some(LEFTGROUP), + "leftgroupunder" => Some(LEFTGROUPUNDER), + "rightgroup" => Some(RIGHTGROUP), + "rightgroupunder" => Some(RIGHTGROUPUNDER), + "leftbracketover" => Some(LEFTBRACKETOVER), + "rightbracketover" => Some(RIGHTBRACKETOVER), + "leftbracketunder" => Some(LEFTBRACKETUNDER), + "rightbracketunder" => Some(RIGHTBRACKETUNDER), _ => None, } } @@ -976,14 +1339,25 @@ pub fn katex_stretchy_path(label: &str, width_em: f64) -> Option<(Vec Option> { let svg_str = path_for_name(path_name)?; let raw = parse_svg_path(svg_str); - Some(raw.iter().map(|c| scale_cmd_twohead_uniform(c, s, vb_cy, x_shift)).collect()) + Some( + raw.iter() + .map(|c| scale_cmd_twohead_uniform(c, s, vb_cy, x_shift)) + .collect(), + ) }; match data.paths.len() { 1 => { - let x_shift = if data.align == Some("xMaxYMin") { width_em - 400_000.0 * s } else { 0.0 }; + let x_shift = if data.align == Some("xMaxYMin") { + width_em - 400_000.0 * s + } else { + 0.0 + }; let cmds = make_cmds(data.paths[0], x_shift)?; - Some((clip_path_to_rect(&cmds, 0.0, width_em, y_min, y_max), height_em)) + Some(( + clip_path_to_rect(&cmds, 0.0, width_em, y_min, y_max), + height_em, + )) } 2 => { let x_r = width_em - 400_000.0 * s; @@ -1005,10 +1379,22 @@ pub fn katex_stretchy_path(label: &str, width_em: f64) -> Option<(Vec shaft_x_start { - cmds.push(PathCommand::MoveTo { x: shaft_x_start, y: shaft_y_top }); - cmds.push(PathCommand::LineTo { x: shaft_x_end, y: shaft_y_top }); - cmds.push(PathCommand::LineTo { x: shaft_x_end, y: shaft_y_bot }); - cmds.push(PathCommand::LineTo { x: shaft_x_start, y: shaft_y_bot }); + cmds.push(PathCommand::MoveTo { + x: shaft_x_start, + y: shaft_y_top, + }); + cmds.push(PathCommand::LineTo { + x: shaft_x_end, + y: shaft_y_top, + }); + cmds.push(PathCommand::LineTo { + x: shaft_x_end, + y: shaft_y_bot, + }); + cmds.push(PathCommand::LineTo { + x: shaft_x_start, + y: shaft_y_bot, + }); cmds.push(PathCommand::Close); } cmds.extend(clip_path_to_rect(&rc, 0.0, width_em, y_min, y_max)); @@ -1144,7 +1530,10 @@ mod tests { #[test] fn test_parse_katex_vec_path() { let cmds = scale_svg_path_thousandths(&parse_svg_path(KATEX_VEC_PATH)); - assert!(cmds.len() >= 8, "vec path should parse to multiple segments"); + assert!( + cmds.len() >= 8, + "vec path should parse to multiple segments" + ); match cmds[0] { PathCommand::MoveTo { x, y } => { assert!((x - 0.377).abs() < 0.001); diff --git a/crates/ratex-layout/src/layout_box.rs b/crates/ratex-layout/src/layout_box.rs index 37cec17f..3bb41033 100644 --- a/crates/ratex-layout/src/layout_box.rs +++ b/crates/ratex-layout/src/layout_box.rs @@ -36,10 +36,7 @@ pub enum BoxContent { /// Filled rectangle from `\rule[]{width}{height}`. /// `thickness` is the ink height; `raise` is the distance (in em) from the baseline /// to the bottom edge of the rectangle, positive toward the top of the line. - Rule { - thickness: f64, - raise: f64, - }, + Rule { thickness: f64, raise: f64 }, /// Empty space (kern). Kern, @@ -170,10 +167,7 @@ pub enum BoxContent { /// A raised/lowered box (raisebox). /// shift > 0 moves content up, shift < 0 moves content down. - RaiseBox { - body: Box, - shift: f64, - }, + RaiseBox { body: Box, shift: f64 }, /// A scaled box (for \scriptstyle, \scriptscriptstyle in inline context). /// The child is rendered at child_scale relative to the parent. diff --git a/crates/ratex-layout/src/lib.rs b/crates/ratex-layout/src/lib.rs index b6bcaa91..a5fdce16 100644 --- a/crates/ratex-layout/src/lib.rs +++ b/crates/ratex-layout/src/lib.rs @@ -10,6 +10,6 @@ pub mod to_display; pub mod vbox; pub use engine::layout; -pub use layout_options::LayoutOptions; pub use layout_box::LayoutBox; +pub use layout_options::LayoutOptions; pub use to_display::to_display_list; diff --git a/crates/ratex-layout/src/spacing.rs b/crates/ratex-layout/src/spacing.rs index a9d724ec..5422ce1d 100644 --- a/crates/ratex-layout/src/spacing.rs +++ b/crates/ratex-layout/src/spacing.rs @@ -3,14 +3,14 @@ /// From TeXbook pp. 170-171. Also matches KaTeX's DomEnum. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MathClass { - Ord, // ordinary - Op, // large operator - Bin, // binary operation - Rel, // relation - Open, // opening delimiter - Close, // closing delimiter - Punct, // punctuation - Inner, // inner (fractions, etc.) + Ord, // ordinary + Op, // large operator + Bin, // binary operation + Rel, // relation + Open, // opening delimiter + Close, // closing delimiter + Punct, // punctuation + Inner, // inner (fractions, etc.) } /// Spacing between adjacent math atoms, in mu (1mu = 1/18 em). @@ -69,7 +69,11 @@ impl MathClass { /// `tight` should be true for script and scriptscript styles. /// Returns the spacing in mu units (1mu = 1/18 em). pub fn atom_spacing(left: MathClass, right: MathClass, tight: bool) -> f64 { - let table = if tight { &TIGHT_SPACING_TABLE } else { &SPACING_TABLE }; + let table = if tight { + &TIGHT_SPACING_TABLE + } else { + &SPACING_TABLE + }; table[left.index()][right.index()] as f64 } diff --git a/crates/ratex-layout/src/stacked_delim.rs b/crates/ratex-layout/src/stacked_delim.rs index 9723d4e3..eddc6ee4 100644 --- a/crates/ratex-layout/src/stacked_delim.rs +++ b/crates/ratex-layout/src/stacked_delim.rs @@ -4,9 +4,9 @@ use ratex_font::{get_char_metrics, CharMetrics, FontId}; use ratex_types::path_command::PathCommand; -use crate::layout_options::LayoutOptions; use crate::katex_svg::parse_svg_path_data; use crate::layout_box::{BoxContent, LayoutBox, VBoxChild, VBoxChildKind}; +use crate::layout_options::LayoutOptions; use crate::vbox::make_vbox; const LAP: f64 = 0.008; @@ -195,11 +195,26 @@ fn shift_path_y(cmds: Vec, dy: f64) -> Vec { .map(|c| match c { PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x, y: y + dy }, PathCommand::LineTo { x, y } => PathCommand::LineTo { x, y: y + dy }, - PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo { - x1, y1: y1 + dy, x2, y2: y2 + dy, x, y: y + dy, + PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + } => PathCommand::CubicTo { + x1, + y1: y1 + dy, + x2, + y2: y2 + dy, + x, + y: y + dy, }, PathCommand::QuadTo { x1, y1, x, y } => PathCommand::QuadTo { - x1, y1: y1 + dy, x, y: y + dy, + x1, + y1: y1 + dy, + x, + y: y + dy, }, PathCommand::Close => PathCommand::Close, }) @@ -210,14 +225,8 @@ fn scale_path_to_em(cmds: &[PathCommand]) -> Vec { let s = 0.001; cmds.iter() .map(|c| match *c { - PathCommand::MoveTo { x, y } => PathCommand::MoveTo { - x: x * s, - y: y * s, - }, - PathCommand::LineTo { x, y } => PathCommand::LineTo { - x: x * s, - y: y * s, - }, + PathCommand::MoveTo { x, y } => PathCommand::MoveTo { x: x * s, y: y * s }, + PathCommand::LineTo { x, y } => PathCommand::LineTo { x: x * s, y: y * s }, PathCommand::CubicTo { x1, y1, @@ -244,7 +253,11 @@ fn scale_path_to_em(cmds: &[PathCommand]) -> Vec { .collect() } -fn make_tall_svg_delim(kind: StackDelimKind, height_total: f64, options: &LayoutOptions) -> Option { +fn make_tall_svg_delim( + kind: StackDelimKind, + height_total: f64, + options: &LayoutOptions, +) -> Option { let label = svg_label_for(kind); if label.is_empty() { return None; @@ -262,8 +275,12 @@ fn make_tall_svg_delim(kind: StackDelimKind, height_total: f64, options: &Layout _ => return None, }; - let top_m = get_char_metrics(FontId::Size4Regular, top_c).map(|m| height_depth(&m)).unwrap_or(0.0); - let bot_m = get_char_metrics(FontId::Size4Regular, bot_c).map(|m| height_depth(&m)).unwrap_or(0.0); + let top_m = get_char_metrics(FontId::Size4Regular, top_c) + .map(|m| height_depth(&m)) + .unwrap_or(0.0); + let bot_m = get_char_metrics(FontId::Size4Regular, bot_c) + .map(|m| height_depth(&m)) + .unwrap_or(0.0); let min_h = top_m + bot_m; let mid_em = (height_total - min_h).max(0.0); let mid_th = (mid_em * 1000.0).round() as i64; @@ -294,7 +311,11 @@ fn make_tall_svg_delim(kind: StackDelimKind, height_total: f64, options: &Layout Some(axis_center_stacked_vbox(inner, options)) } -fn make_glyph_stack_delim(kind: StackDelimKind, height_total: f64, options: &LayoutOptions) -> Option { +fn make_glyph_stack_delim( + kind: StackDelimKind, + height_total: f64, + options: &LayoutOptions, +) -> Option { let lap_kern = |k: f64| VBoxChild { kind: VBoxChildKind::Kern(-k), shift: 0.0, diff --git a/crates/ratex-layout/src/to_display.rs b/crates/ratex-layout/src/to_display.rs index 294f1b65..03445ebd 100644 --- a/crates/ratex-layout/src/to_display.rs +++ b/crates/ratex-layout/src/to_display.rs @@ -142,7 +142,14 @@ fn emit_box( match &child.kind { VBoxChildKind::Box(b) => { cur_y += b.height * scale; - emit_box(b, x + child.shift * scale, cur_y, scale, items, font_str_cache); + emit_box( + b, + x + child.shift * scale, + cur_y, + scale, + items, + font_str_cache, + ); cur_y += b.depth * scale; } VBoxChildKind::Kern(k) => { @@ -192,10 +199,24 @@ fn emit_box( let child_denom_scale = scale * d_sc; let frac_x = x + (lbox.width * scale - numer.width * child_numer_scale) / 2.0; - emit_box(numer, frac_x, y - numer_shift * scale, child_numer_scale, items, font_str_cache); + emit_box( + numer, + frac_x, + y - numer_shift * scale, + child_numer_scale, + items, + font_str_cache, + ); let frac_x = x + (lbox.width * scale - denom.width * child_denom_scale) / 2.0; - emit_box(denom, frac_x, y + denom_shift * scale, child_denom_scale, items, font_str_cache); + emit_box( + denom, + frac_x, + y + denom_shift * scale, + child_denom_scale, + items, + font_str_cache, + ); if *bar_thickness > 0.0 { let metrics = ratex_font::get_global_metrics(0); @@ -235,7 +256,14 @@ fn emit_box( } else { base_x + (base.width + italic_correction) * scale }; - emit_box(sup_box, sup_x, y - sup_shift * scale, child_scale, items, font_str_cache); + emit_box( + sup_box, + sup_x, + y - sup_shift * scale, + child_scale, + items, + font_str_cache, + ); } if let Some(sub_box) = sub { let child_scale = scale * bs; @@ -244,7 +272,14 @@ fn emit_box( } else { base_x + base.width * scale + sub_h_kern * scale }; - emit_box(sub_box, sub_x, y + sub_shift * scale, child_scale, items, font_str_cache); + emit_box( + sub_box, + sub_x, + y + sub_shift * scale, + child_scale, + items, + font_str_cache, + ); } } @@ -314,7 +349,14 @@ fn emit_box( dashed: false, }); - emit_box(body, surd_x + radical_width * scale, y, scale, items, font_str_cache); + emit_box( + body, + surd_x + radical_width * scale, + y, + scale, + items, + font_str_cache, + ); } BoxContent::OpLimits { @@ -329,18 +371,34 @@ fn emit_box( sub_scale: bs, } => { let base_x = x + (lbox.width - base.width) * scale / 2.0; - emit_box(base, base_x, y + base_shift * scale, scale, items, font_str_cache); + emit_box( + base, + base_x, + y + base_shift * scale, + scale, + items, + font_str_cache, + ); if let Some(sup_box) = sup { let child_scale = scale * ss; - let sup_x = x + (lbox.width * scale - sup_box.width * child_scale) / 2.0 + slant * scale / 2.0; - let sup_y = y - (base.height - base_shift) * scale - sup_kern * scale - sup_box.depth * child_scale; + let sup_x = x + + (lbox.width * scale - sup_box.width * child_scale) / 2.0 + + slant * scale / 2.0; + let sup_y = y + - (base.height - base_shift) * scale + - sup_kern * scale + - sup_box.depth * child_scale; emit_box(sup_box, sup_x, sup_y, child_scale, items, font_str_cache); } if let Some(sub_box) = sub { let child_scale = scale * bs; - let sub_x = x + (lbox.width * scale - sub_box.width * child_scale) / 2.0 - slant * scale / 2.0; - let sub_y = y + (base.depth + base_shift) * scale + sub_kern * scale + sub_box.height * child_scale; + let sub_x = x + (lbox.width * scale - sub_box.width * child_scale) / 2.0 + - slant * scale / 2.0; + let sub_y = y + + (base.depth + base_shift) * scale + + sub_kern * scale + + sub_box.height * child_scale; emit_box(sub_box, sub_x, sub_y, child_scale, items, font_str_cache); } } @@ -356,8 +414,7 @@ fn emit_box( emit_box(base, x, y, scale, items, font_str_cache); if *is_below { let accent_x = x + (base.width - accent.width) * scale / 2.0; - let accent_y = - y + (base.depth + under_gap_em) * scale + accent.height * scale; + let accent_y = y + (base.depth + under_gap_em) * scale + accent.height * scale; emit_box(accent, accent_x, accent_y, scale, items, font_str_cache); } else { let accent_x = x + (base.width - accent.width) * scale / 2.0 + skew * scale; @@ -510,8 +567,7 @@ fn emit_box( if *tag_col_width > 0.0 { if let Some(tb) = row_tags.get(r).and_then(|o| o.as_ref()) { let tag_start_em = array_inner_width - content_x_offset + tag_gap_em; - let tag_x = - x + tag_start_em * scale + (tag_col_width - tb.width) * scale; + let tag_x = x + tag_start_em * scale + (tag_col_width - tb.width) * scale; emit_box(tb, tag_x, cur_y, scale, items, font_str_cache); } } @@ -607,7 +663,10 @@ fn emit_box( emit_box(body, x, y, scale * child_scale, items, font_str_cache); } - BoxContent::Angl { path_commands, body } => { + BoxContent::Angl { + path_commands, + body, + } => { let scaled: Vec = path_commands .iter() .map(|c| scale_path_command(c, scale)) @@ -622,7 +681,10 @@ fn emit_box( emit_box(body, x, y, scale, items, font_str_cache); } - BoxContent::Overline { body, rule_thickness } => { + BoxContent::Overline { + body, + rule_thickness, + } => { emit_box(body, x, y, scale, items, font_str_cache); // Rule center is at 2.5 * rule_thickness above the body's top let rule_center_y = y - (body.height + 2.5 * rule_thickness) * scale; @@ -636,7 +698,10 @@ fn emit_box( }); } - BoxContent::Underline { body, rule_thickness } => { + BoxContent::Underline { + body, + rule_thickness, + } => { emit_box(body, x, y, scale, items, font_str_cache); // Rule center is at 2.5 * rule_thickness below the body's bottom let rule_center_y = y + (body.depth + 2.5 * rule_thickness) * scale; @@ -688,7 +753,14 @@ fn scale_path_command(cmd: &PathCommand, scale: f64) -> PathCommand { x: x * scale, y: y * scale, }, - PathCommand::CubicTo { x1, y1, x2, y2, x, y } => PathCommand::CubicTo { + PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + } => PathCommand::CubicTo { x1: x1 * scale, y1: y1 * scale, x2: x2 * scale, @@ -739,13 +811,25 @@ fn compute_visual_bounds(items: &[DisplayItem]) -> (f64, f64, f64, f64) { min_y = min_y.min(y - h * scale); max_y = max_y.max(y + d * scale); } - DisplayItem::Line { x, y, width, thickness, .. } => { + DisplayItem::Line { + x, + y, + width, + thickness, + .. + } => { min_x = min_x.min(*x); max_x = max_x.max(x + width); min_y = min_y.min(y - thickness / 2.0); max_y = max_y.max(y + thickness / 2.0); } - DisplayItem::Rect { x, y, width, height, .. } => { + DisplayItem::Rect { + x, + y, + width, + height, + .. + } => { min_x = min_x.min(*x); max_x = max_x.max(x + width); min_y = min_y.min(*y); @@ -772,7 +856,8 @@ fn compute_visual_bounds(items: &[DisplayItem]) -> (f64, f64, f64, f64) { } }; match cmd { - PathCommand::MoveTo { x: cx, y: cy } | PathCommand::LineTo { x: cx, y: cy } => { + PathCommand::MoveTo { x: cx, y: cy } + | PathCommand::LineTo { x: cx, y: cy } => { consider(*cx, *cy); } PathCommand::CubicTo { diff --git a/crates/ratex-layout/tests/ce_co2_cjk_glyphs.rs b/crates/ratex-layout/tests/ce_co2_cjk_glyphs.rs index 183675b7..97978281 100644 --- a/crates/ratex-layout/tests/ce_co2_cjk_glyphs.rs +++ b/crates/ratex-layout/tests/ce_co2_cjk_glyphs.rs @@ -10,7 +10,10 @@ fn ce_co2_line_emits_cjk_regular_for_simplified_chinese() { let dl = to_display_list(&lbox); let mut rows = Vec::new(); for item in &dl.items { - if let DisplayItem::GlyphPath { font, char_code, .. } = item { + if let DisplayItem::GlyphPath { + font, char_code, .. + } = item + { if *char_code >= 0x4E00 { let ch = char::from_u32(*char_code).unwrap(); rows.push(format!("{font} U+{:04X} {ch}", char_code)); diff --git a/crates/ratex-layout/tests/layout_vs_katex.rs b/crates/ratex-layout/tests/layout_vs_katex.rs index c1eb781f..33480d36 100644 --- a/crates/ratex-layout/tests/layout_vs_katex.rs +++ b/crates/ratex-layout/tests/layout_vs_katex.rs @@ -1,3 +1,4 @@ +use ratex_layout::to_display_list; /// Integration tests comparing ratex-layout box dimensions against KaTeX. /// /// These expected values were extracted from KaTeX 0.16.38 using @@ -6,11 +7,10 @@ /// /// Tolerance: 0.001em (well within the 0.02em threshold from the plan). use ratex_layout::{layout, LayoutOptions}; -use ratex_layout::to_display_list; use ratex_parser::parser::parse; -use ratex_types::MathStyle; use ratex_types::color::Color; use ratex_types::display_item::DisplayItem; +use ratex_types::MathStyle; const TOLERANCE: f64 = 0.002; @@ -70,41 +70,45 @@ fn htmlstyle_applies_supported_css() { #[test] fn prooftree_binary_emits_inference_rule() { - let ast = parse("\\begin{prooftree}\\AxiomC{P}\\AxiomC{Q}\\BinaryInfC{R}\\end{prooftree}").unwrap(); + let ast = + parse("\\begin{prooftree}\\AxiomC{P}\\AxiomC{Q}\\BinaryInfC{R}\\end{prooftree}").unwrap(); let options = LayoutOptions::default(); let lbox = layout(&ast, &options); let display = to_display_list(&lbox); assert!(display.width > 0.0); assert!(display.height > 0.0); - assert!(display.items.iter().any(|item| matches!( - item, - DisplayItem::Line { dashed: false, .. } - ))); + assert!(display + .items + .iter() + .any(|item| matches!(item, DisplayItem::Line { dashed: false, .. }))); } #[test] fn prooftree_dashed_and_noline_rules() { - let dashed_ast = parse("\\begin{prooftree}\\AxiomC{P}\\dashedLine\\UnaryInfC{Q}\\end{prooftree}").unwrap(); + let dashed_ast = + parse("\\begin{prooftree}\\AxiomC{P}\\dashedLine\\UnaryInfC{Q}\\end{prooftree}").unwrap(); let options = LayoutOptions::default(); let dashed_display = to_display_list(&layout(&dashed_ast, &options)); - assert!(dashed_display.items.iter().any(|item| matches!( - item, - DisplayItem::Line { dashed: true, .. } - ))); + assert!(dashed_display + .items + .iter() + .any(|item| matches!(item, DisplayItem::Line { dashed: true, .. }))); - let noline_ast = parse("\\begin{prooftree}\\AxiomC{P}\\noLine\\UnaryInfC{Q}\\end{prooftree}").unwrap(); + let noline_ast = + parse("\\begin{prooftree}\\AxiomC{P}\\noLine\\UnaryInfC{Q}\\end{prooftree}").unwrap(); let noline_display = to_display_list(&layout(&noline_ast, &options)); - assert!(!noline_display.items.iter().any(|item| matches!( - item, - DisplayItem::Line { .. } - ))); + assert!(!noline_display + .items + .iter() + .any(|item| matches!(item, DisplayItem::Line { .. }))); } #[test] fn prooftree_root_at_top_flips_layout() { let normal_ast = parse("\\begin{prooftree}\\AxiomC{P}\\UIC{Q}\\end{prooftree}").unwrap(); - let root_at_top_ast = parse("\\begin{prooftree}\\AxiomC{P}\\rootAtTop\\UIC{Q}\\end{prooftree}").unwrap(); + let root_at_top_ast = + parse("\\begin{prooftree}\\AxiomC{P}\\rootAtTop\\UIC{Q}\\end{prooftree}").unwrap(); let options = LayoutOptions::default(); let normal_display = to_display_list(&layout(&normal_ast, &options)); let root_at_top_display = to_display_list(&layout(&root_at_top_ast, &options)); @@ -415,12 +419,20 @@ fn biggest_biggl() { #[test] fn pmatrix_2x2() { - check("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}", 1.45, 0.95); + check( + "\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}", + 1.45, + 0.95, + ); } #[test] fn bmatrix_identity() { - check("\\begin{bmatrix} 1 & 0 \\\\ 0 & 1 \\end{bmatrix}", 1.45, 0.95); + check( + "\\begin{bmatrix} 1 & 0 \\\\ 0 & 1 \\end{bmatrix}", + 1.45, + 0.95, + ); } #[test] diff --git a/crates/ratex-lexer/src/lexer.rs b/crates/ratex-lexer/src/lexer.rs index 0e8f576e..fda06194 100644 --- a/crates/ratex-lexer/src/lexer.rs +++ b/crates/ratex-lexer/src/lexer.rs @@ -180,7 +180,9 @@ impl<'a> Lexer<'a> { if Self::is_control_word_char(next_byte) { // Control word: \ - while self.pos < self.bytes.len() && Self::is_control_word_char(self.bytes[self.pos]) { + while self.pos < self.bytes.len() + && Self::is_control_word_char(self.bytes[self.pos]) + { self.pos += 1; } let text = &self.input[start..self.pos]; @@ -257,11 +259,7 @@ mod tests { fn lex_texts(input: &str) -> Vec { let mut lexer = Lexer::new(input); - lexer - .lex_all() - .into_iter() - .map(|t| t.text) - .collect() + lexer.lex_all().into_iter().map(|t| t.text).collect() } // === Basic character tokens === @@ -419,10 +417,7 @@ mod tests { #[test] fn test_sqrt_with_optional() { let tokens = lex_texts("\\sqrt[3]{x}"); - assert_eq!( - tokens, - vec!["\\sqrt", "[", "3", "]", "{", "x", "}", "EOF"] - ); + assert_eq!(tokens, vec!["\\sqrt", "[", "3", "]", "{", "x", "}", "EOF"]); } #[test] @@ -431,8 +426,8 @@ mod tests { assert_eq!( tokens, vec![ - "\\frac", "{", "a", " ", "+", " ", "b", "}", - "{", "c", " ", "-", " ", "d", "}", "EOF" + "\\frac", "{", "a", " ", "+", " ", "b", "}", "{", "c", " ", "-", " ", "d", "}", + "EOF" ] ); } @@ -442,9 +437,7 @@ mod tests { let tokens = lex_texts("\\sum_{i=0}^{n}"); assert_eq!( tokens, - vec![ - "\\sum", "_", "{", "i", "=", "0", "}", "^", "{", "n", "}", "EOF" - ] + vec!["\\sum", "_", "{", "i", "=", "0", "}", "^", "{", "n", "}", "EOF"] ); } @@ -463,8 +456,8 @@ mod tests { assert_eq!( tokens, vec![ - "\\frac", "{", "\\sqrt", "{", "a", "^", "2", "+", "b", "^", "2", - "}", "}", "{", "c", "}", "EOF" + "\\frac", "{", "\\sqrt", "{", "a", "^", "2", "+", "b", "^", "2", "}", "}", "{", + "c", "}", "EOF" ] ); } @@ -581,7 +574,10 @@ mod tests { // Comment and everything after % until \n is removed; \n consumed by comment assert_eq!( tokens, - vec!["a", "^", "2", " ", "+", " ", "b", "^", "2", " ", "=", " ", "c", "^", "2", " ", "EOF"] + vec![ + "a", "^", "2", " ", "+", " ", "b", "^", "2", " ", "=", " ", "c", "^", "2", " ", + "EOF" + ] ); } @@ -618,15 +614,9 @@ mod tests { // % comment\n → skipped // ^ → "^" // 2 → "2" - assert_eq!( - with_comment, - vec!["x", "_", "3", " ", "^", "2", "EOF"] - ); + assert_eq!(with_comment, vec!["x", "_", "3", " ", "^", "2", "EOF"]); // Without comment: x_3^2 - assert_eq!( - without_comment, - vec!["x", "_", "3", "^", "2", "EOF"] - ); + assert_eq!(without_comment, vec!["x", "_", "3", "^", "2", "EOF"]); // The parser would ignore the space; lexer correctly produces it. } @@ -634,10 +624,7 @@ mod tests { #[test] fn katex_comment_after_caret() { let tokens = lex_texts("x^ %comment\n{2}"); - assert_eq!( - tokens, - vec!["x", "^", " ", "{", "2", "}", "EOF"] - ); + assert_eq!(tokens, vec!["x", "^", " ", "{", "2", "}", "EOF"]); } /// KaTeX: `"x^ %comment\n\\frac{1}{2}"` parseLike `"x^\\frac{1}{2}"` @@ -655,30 +642,21 @@ mod tests { #[test] fn katex_comment_in_kern() { let tokens = lex_texts("\\kern{1 %kern\nem}"); - assert_eq!( - tokens, - vec!["\\kern", "{", "1", " ", "e", "m", "}", "EOF"] - ); + assert_eq!(tokens, vec!["\\kern", "{", "1", " ", "e", "m", "}", "EOF"]); } /// KaTeX: `"\\kern1 %kern\nem"` #[test] fn katex_comment_in_kern_no_brace() { let tokens = lex_texts("\\kern1 %kern\nem"); - assert_eq!( - tokens, - vec!["\\kern", "1", " ", "e", "m", "EOF"] - ); + assert_eq!(tokens, vec!["\\kern", "1", " ", "e", "m", "EOF"]); } /// KaTeX: `"\\color{#f00%red\n}"` #[test] fn katex_comment_in_color() { let tokens = lex_texts("\\color{#f00%red\n}"); - assert_eq!( - tokens, - vec!["\\color", "{", "#", "f", "0", "0", "}", "EOF"] - ); + assert_eq!(tokens, vec!["\\color", "{", "#", "f", "0", "0", "}", "EOF"]); } /// KaTeX: `it("should parse comments before an expression")` @@ -829,7 +807,10 @@ mod tests { #[test] fn test_verb_in_expression() { let tokens = lex_texts("x + \\verb|y| + z"); - assert_eq!(tokens, vec!["x", " ", "+", " ", "\\verb|y|", " ", "+", " ", "z", "EOF"]); + assert_eq!( + tokens, + vec!["x", " ", "+", " ", "\\verb|y|", " ", "+", " ", "z", "EOF"] + ); } /// Quadratic formula @@ -839,9 +820,8 @@ mod tests { assert_eq!( tokens, vec![ - "\\frac", "{", "-", "b", " ", "\\pm", "\\sqrt", "{", - "b", "^", "2", "-", "4", "a", "c", "}", "}", "{", - "2", "a", "}", "EOF" + "\\frac", "{", "-", "b", " ", "\\pm", "\\sqrt", "{", "b", "^", "2", "-", "4", "a", + "c", "}", "}", "{", "2", "a", "}", "EOF" ] ); } @@ -853,11 +833,9 @@ mod tests { assert_eq!( tokens, vec![ - "\\begin", "{", "p", "m", "a", "t", "r", "i", "x", "}", - " ", "a", " ", "&", " ", "b", " ", "\\\\", " ", - "c", " ", "&", " ", "d", " ", - "\\end", "{", "p", "m", "a", "t", "r", "i", "x", "}", - "EOF" + "\\begin", "{", "p", "m", "a", "t", "r", "i", "x", "}", " ", "a", " ", "&", " ", + "b", " ", "\\\\", " ", "c", " ", "&", " ", "d", " ", "\\end", "{", "p", "m", "a", + "t", "r", "i", "x", "}", "EOF" ] ); } diff --git a/crates/ratex-parser/src/environments.rs b/crates/ratex-parser/src/environments.rs index d022e832..c208c7ad 100644 --- a/crates/ratex-parser/src/environments.rs +++ b/crates/ratex-parser/src/environments.rs @@ -63,14 +63,20 @@ pub struct ArrayConfig { pub auto_number: bool, } - // ── parseArray ─────────────────────────────────────────────────────────── /// Pull a trailing `\\tag{…}` or `\\nonumber`/`\\notag` off the last cell of a row. /// Returns `Auto(true)` when the row is eligible for auto-numbering. /// The `auto_number` parameter controls the default when no marker is found. -fn extract_trailing_tag_from_last_cell(row: &mut [ParseNode], auto_number: bool) -> ParseResult { - let default_tag = if auto_number { ArrayTag::Auto(true) } else { ArrayTag::Auto(false) }; +fn extract_trailing_tag_from_last_cell( + row: &mut [ParseNode], + auto_number: bool, +) -> ParseResult { + let default_tag = if auto_number { + ArrayTag::Auto(true) + } else { + ArrayTag::Auto(false) + }; let Some(last) = row.last_mut() else { return Ok(default_tag); }; @@ -188,9 +194,7 @@ pub fn parse_array( parser.gullet.begin_group(); if !config.single_row { - parser - .gullet - .set_text_macro("\\cr", "\\\\\\relax"); + parser.gullet.set_text_macro("\\cr", "\\\\\\relax"); } let arraystretch = config.arraystretch.unwrap_or_else(|| { @@ -200,7 +204,11 @@ pub fn parse_array( MacroDefinition::Text(s) => s.clone(), MacroDefinition::Tokens { tokens, .. } => { // Tokens are stored in reverse order (stack convention for expansion) - tokens.iter().rev().map(|t| t.text.as_str()).collect::() + tokens + .iter() + .rev() + .map(|t| t.text.as_str()) + .collect::() } MacroDefinition::Function(_) => String::new(), }; @@ -257,10 +265,7 @@ pub fn parse_array( let is_empty_trailing = if let Some(s) = style { if s == StyleStr::Text || s == StyleStr::Display { if let ParseNode::Styling { body: ref sb, .. } = cell { - if let Some(ParseNode::OrdGroup { - body: ref ob, .. - }) = sb.first() - { + if let Some(ParseNode::OrdGroup { body: ref ob, .. }) = sb.first() { ob.is_empty() } else { false @@ -281,9 +286,7 @@ pub fn parse_array( row_tags.push(row_tag); body.push(row); - if is_empty_trailing - && (body.len() > 1 || !config.empty_single_row) - { + if is_empty_trailing && (body.len() > 1 || !config.empty_single_row) { body.pop(); row_tags.pop(); } @@ -370,12 +373,17 @@ pub fn parse_array( } } } - if any_visible { Some(processed) } else { None } + if any_visible { + Some(processed) + } else { + None + } } else { // Not an auto-numbering environment: keep original behavior - if row_tags.iter().any(|t| { - matches!(t, ArrayTag::Explicit(nodes) if !nodes.is_empty()) - }) { + if row_tags + .iter() + .any(|t| matches!(t, ArrayTag::Explicit(nodes) if !nodes.is_empty())) + { Some(row_tags) } else { None @@ -437,12 +445,7 @@ fn register_array(map: &mut HashMap<&'static str, EnvSpec>) { pregap: None, postgap: None, }), - _ => { - return Err(ParseError::msg(format!( - "Unknown column alignment: {}", - ca - ))) - } + _ => return Err(ParseError::msg(format!("Unknown column alignment: {}", ca))), } } @@ -506,10 +509,7 @@ fn register_matrix(map: &mut HashMap<&'static str, EnvSpec>) { ctx.parser.gullet.consume_spaces(); let close = ctx.parser.gullet.pop_token(); if close.text != "]" { - return Err(ParseError::new( - "Expected ]".to_string(), - Some(&close), - )); + return Err(ParseError::new("Expected ]".to_string(), Some(&close))); } } } @@ -561,8 +561,8 @@ fn register_matrix(map: &mut HashMap<&'static str, EnvSpec>) { } for name in &[ - "matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix", - "matrix*", "pmatrix*", "bmatrix*", "Bmatrix*", "vmatrix*", "Vmatrix*", + "matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix", "matrix*", "pmatrix*", + "bmatrix*", "Bmatrix*", "vmatrix*", "Vmatrix*", ] { map.insert( name, @@ -639,128 +639,126 @@ fn handle_aligned( args: Vec, _opt_args: Vec>, ) -> ParseResult { - let is_split = ctx.env_name == "split"; - let is_alignat = ctx.env_name.contains("at"); - let sep_type = if is_alignat { "alignat" } else { "align" }; - let auto_number = !ctx.env_name.ends_with('*') - && !is_split - && ctx.env_name != "aligned" - && ctx.env_name != "alignedat"; - - let config = ArrayConfig { - add_jot: Some(true), - empty_single_row: true, - col_separation_type: Some(sep_type.to_string()), - max_num_cols: if is_split { Some(2) } else { None }, - auto_number, - ..Default::default() - }; + let is_split = ctx.env_name == "split"; + let is_alignat = ctx.env_name.contains("at"); + let sep_type = if is_alignat { "alignat" } else { "align" }; + let auto_number = !ctx.env_name.ends_with('*') + && !is_split + && ctx.env_name != "aligned" + && ctx.env_name != "alignedat"; + + let config = ArrayConfig { + add_jot: Some(true), + empty_single_row: true, + col_separation_type: Some(sep_type.to_string()), + max_num_cols: if is_split { Some(2) } else { None }, + auto_number, + ..Default::default() + }; - let mut res = parse_array(ctx.parser, config, Some(StyleStr::Display))?; + let mut res = parse_array(ctx.parser, config, Some(StyleStr::Display))?; - // Extract explicit column count from first arg (alignat only) - let mut num_maths = 0usize; - let mut explicit_cols = 0usize; - if let Some(ParseNode::OrdGroup { body, .. }) = args.first() { - let mut arg_str = String::new(); - for node in body { - if let Some(t) = node.symbol_text() { - arg_str.push_str(t); - } - } - if let Ok(n) = arg_str.parse::() { - num_maths = n; - explicit_cols = n * 2; + // Extract explicit column count from first arg (alignat only) + let mut num_maths = 0usize; + let mut explicit_cols = 0usize; + if let Some(ParseNode::OrdGroup { body, .. }) = args.first() { + let mut arg_str = String::new(); + for node in body { + if let Some(t) = node.symbol_text() { + arg_str.push_str(t); } } - let is_aligned = explicit_cols == 0; + if let Ok(n) = arg_str.parse::() { + num_maths = n; + explicit_cols = n * 2; + } + } + let is_aligned = explicit_cols == 0; - // Determine actual number of columns - let mut num_cols = if let ParseNode::Array { ref body, .. } = res { - body.iter().map(|r| r.len()).max().unwrap_or(0) - } else { - 0 - }; + // Determine actual number of columns + let mut num_cols = if let ParseNode::Array { ref body, .. } = res { + body.iter().map(|r| r.len()).max().unwrap_or(0) + } else { + 0 + }; - if let ParseNode::Array { - body: ref mut array_body, - .. - } = res - { - for row in array_body.iter_mut() { - // Prepend empty group at every even-indexed cell (2nd, 4th, ...) - let mut i = 1; - while i < row.len() { - if let ParseNode::Styling { - body: ref mut styling_body, + if let ParseNode::Array { + body: ref mut array_body, + .. + } = res + { + for row in array_body.iter_mut() { + // Prepend empty group at every even-indexed cell (2nd, 4th, ...) + let mut i = 1; + while i < row.len() { + if let ParseNode::Styling { + body: ref mut styling_body, + .. + } = row[i] + { + if let Some(ParseNode::OrdGroup { + body: ref mut og_body, .. - } = row[i] + }) = styling_body.first_mut() { - if let Some(ParseNode::OrdGroup { - body: ref mut og_body, - .. - }) = styling_body.first_mut() - { - og_body.insert( - 0, - ParseNode::OrdGroup { - mode: ctx.mode, - body: vec![], - semisimple: None, - loc: None, - }, - ); - } + og_body.insert( + 0, + ParseNode::OrdGroup { + mode: ctx.mode, + body: vec![], + semisimple: None, + loc: None, + }, + ); } - i += 2; } + i += 2; + } - if !is_aligned { - let cur_maths = row.len() / 2; - if num_maths < cur_maths { - return Err(ParseError::msg(format!( - "Too many math in a row: expected {}, but got {}", - num_maths, cur_maths - ))); - } - } else if num_cols < row.len() { - num_cols = row.len(); + if !is_aligned { + let cur_maths = row.len() / 2; + if num_maths < cur_maths { + return Err(ParseError::msg(format!( + "Too many math in a row: expected {}, but got {}", + num_maths, cur_maths + ))); } + } else if num_cols < row.len() { + num_cols = row.len(); } } + } - if !is_aligned { - num_cols = explicit_cols; - } + if !is_aligned { + num_cols = explicit_cols; + } - let mut cols = Vec::new(); - for i in 0..num_cols { - let (align, pregap) = if i % 2 == 1 { - ("l", 0.0) - } else if i > 0 && is_aligned { - ("r", 1.0) - } else { - ("r", 0.0) - }; - cols.push(AlignSpec { - align_type: AlignType::Align, - align: Some(align.to_string()), - pregap: Some(pregap), - postgap: Some(0.0), - }); - } + let mut cols = Vec::new(); + for i in 0..num_cols { + let (align, pregap) = if i % 2 == 1 { + ("l", 0.0) + } else if i > 0 && is_aligned { + ("r", 1.0) + } else { + ("r", 0.0) + }; + cols.push(AlignSpec { + align_type: AlignType::Align, + align: Some(align.to_string()), + pregap: Some(pregap), + postgap: Some(0.0), + }); + } - if let ParseNode::Array { - cols: ref mut array_cols, - col_separation_type: ref mut array_sep_type, - .. - } = res - { - *array_cols = Some(cols); - *array_sep_type = Some( - if is_aligned { "align" } else { "alignat" }.to_string(), - ); - } + if let ParseNode::Array { + cols: ref mut array_cols, + col_separation_type: ref mut array_sep_type, + .. + } = res + { + *array_cols = Some(cols); + *array_sep_type = Some(if is_aligned { "align" } else { "alignat" }.to_string()); + } Ok(res) } @@ -1201,12 +1199,7 @@ fn register_subarray(map: &mut HashMap<&'static str, EnvSpec>) { pregap: None, postgap: None, }), - _ => { - return Err(ParseError::msg(format!( - "Unknown column alignment: {}", - ca - ))) - } + _ => return Err(ParseError::msg(format!("Unknown column alignment: {}", ca))), } } @@ -1283,9 +1276,9 @@ fn proof_command_arity(name: &str) -> Option { } fn parse_prooftree_arg(parser: &mut Parser, command: &str) -> ParseResult> { - let arg = parser.parse_argument_group(false, None)?.ok_or_else(|| { - ParseError::msg(format!("Expected argument for {}", command)) - })?; + let arg = parser + .parse_argument_group(false, None)? + .ok_or_else(|| ParseError::msg(format!("Expected argument for {}", command)))?; Ok(ParseNode::ord_argument(arg)) } diff --git a/crates/ratex-parser/src/functions/accent.rs b/crates/ratex-parser/src/functions/accent.rs index 16ca59ba..0faebeb5 100644 --- a/crates/ratex-parser/src/functions/accent.rs +++ b/crates/ratex-parser/src/functions/accent.rs @@ -5,8 +5,17 @@ use crate::functions::{define_function_full, ArgType, FunctionContext, FunctionS use crate::parse_node::{Mode, ParseNode}; static NON_STRETCHY: &[&str] = &[ - "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", - "\\check", "\\hat", "\\vec", "\\dot", "\\mathring", + "\\acute", + "\\grave", + "\\ddot", + "\\tilde", + "\\bar", + "\\breve", + "\\check", + "\\hat", + "\\vec", + "\\dot", + "\\mathring", ]; pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { @@ -14,15 +23,38 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", - "\\check", "\\hat", "\\vec", "\\dot", "\\mathring", "\\widecheck", - "\\widehat", "\\widetilde", "\\overrightarrow", "\\overleftarrow", - "\\Overrightarrow", "\\overleftrightarrow", "\\overgroup", - "\\overlinesegment", "\\overleftharpoon", "\\overrightharpoon", + "\\acute", + "\\grave", + "\\ddot", + "\\tilde", + "\\bar", + "\\breve", + "\\check", + "\\hat", + "\\vec", + "\\dot", + "\\mathring", + "\\widecheck", + "\\widehat", + "\\widetilde", + "\\overrightarrow", + "\\overleftarrow", + "\\Overrightarrow", + "\\overleftrightarrow", + "\\overgroup", + "\\overlinesegment", + "\\overleftharpoon", + "\\overrightharpoon", ], "accent", - 1, 0, None, - false, false, true, false, false, + 1, + 0, + None, + false, + false, + true, + false, + false, handle_accent, ); @@ -30,16 +62,29 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\'", "\\`", "\\^", "\\~", "\\=", "\\u", "\\.", "\\\"", - "\\c", "\\r", "\\H", "\\v", "\\textcircled", + "\\'", + "\\`", + "\\^", + "\\~", + "\\=", + "\\u", + "\\.", + "\\\"", + "\\c", + "\\r", + "\\H", + "\\v", + "\\textcircled", ], "accent", - 1, 0, + 1, + 0, Some(vec![ArgType::Primitive]), false, - true, // allowed_in_text - true, // allowed_in_math - false, false, + true, // allowed_in_text + true, // allowed_in_math + false, + false, handle_text_accent, ); @@ -47,12 +92,22 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow", - "\\undergroup", "\\underlinesegment", "\\utilde", + "\\underleftarrow", + "\\underrightarrow", + "\\underleftrightarrow", + "\\undergroup", + "\\underlinesegment", + "\\utilde", ], "accentUnder", - 1, 0, None, - false, false, true, false, false, + 1, + 0, + None, + false, + false, + true, + false, + false, handle_accent_under, ); } diff --git a/crates/ratex-parser/src/functions/arrow.rs b/crates/ratex-parser/src/functions/arrow.rs index 6321b317..35254465 100644 --- a/crates/ratex-parser/src/functions/arrow.rs +++ b/crates/ratex-parser/src/functions/arrow.rs @@ -8,17 +8,38 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow", - "\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow", - "\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown", - "\\xrightharpoonup", "\\xleftharpoondown", "\\xleftharpoonup", - "\\xrightleftharpoons", "\\xleftrightharpoons", "\\xlongequal", - "\\xtwoheadrightarrow", "\\xtwoheadleftarrow", "\\xtofrom", - "\\xrightleftarrows", "\\xrightequilibrium", "\\xleftequilibrium", + "\\xleftarrow", + "\\xrightarrow", + "\\xLeftarrow", + "\\xRightarrow", + "\\xleftrightarrow", + "\\xLeftrightarrow", + "\\xhookleftarrow", + "\\xhookrightarrow", + "\\xmapsto", + "\\xrightharpoondown", + "\\xrightharpoonup", + "\\xleftharpoondown", + "\\xleftharpoonup", + "\\xrightleftharpoons", + "\\xleftrightharpoons", + "\\xlongequal", + "\\xtwoheadrightarrow", + "\\xtwoheadleftarrow", + "\\xtofrom", + "\\xrightleftarrows", + "\\xrightequilibrium", + "\\xleftequilibrium", ], "xArrow", - 1, 1, None, - false, false, true, false, false, + 1, + 1, + None, + false, + false, + true, + false, + false, handle_arrow, ); } diff --git a/crates/ratex-parser/src/functions/char_cmd.rs b/crates/ratex-parser/src/functions/char_cmd.rs index eef01167..64b9e4db 100644 --- a/crates/ratex-parser/src/functions/char_cmd.rs +++ b/crates/ratex-parser/src/functions/char_cmd.rs @@ -9,8 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\@char"], "textord", - 1, 0, None, - false, true, true, false, false, + 1, + 0, + None, + false, + true, + true, + false, + false, handle_at_char, ); } diff --git a/crates/ratex-parser/src/functions/color.rs b/crates/ratex-parser/src/functions/color.rs index 074862c6..59d037f3 100644 --- a/crates/ratex-parser/src/functions/color.rs +++ b/crates/ratex-parser/src/functions/color.rs @@ -10,12 +10,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\textcolor"], "color", - 2, 1, + 2, + 1, Some(vec![ArgType::Raw, ArgType::Color, ArgType::Original]), false, - true, // allowed_in_text + true, // allowed_in_text true, - false, false, + false, + false, handle_textcolor, ); @@ -24,12 +26,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\color"], "color", - 1, 1, + 1, + 1, Some(vec![ArgType::Raw, ArgType::Color]), false, true, true, - false, false, + false, + false, handle_color, ); @@ -38,12 +42,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\colorbox"], "enclose", - 2, 0, + 2, + 0, Some(vec![ArgType::Color, ArgType::Text]), false, true, true, - false, false, + false, + false, handle_colorbox, ); @@ -52,12 +58,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\fcolorbox"], "enclose", - 3, 0, + 3, + 0, Some(vec![ArgType::Color, ArgType::Color, ArgType::Text]), false, true, true, - false, false, + false, + false, handle_fcolorbox, ); } @@ -85,9 +93,7 @@ fn handle_color( ) -> ParseResult { let color = encode_color(extract_color(&args[0])?, &opt_args); let break_on = ctx.break_on_token_text.clone(); - let body = ctx - .parser - .parse_expression(true, break_on.as_deref())?; + let body = ctx.parser.parse_expression(true, break_on.as_deref())?; Ok(ParseNode::Color { mode: ctx.parser.mode, diff --git a/crates/ratex-parser/src/functions/cr.rs b/crates/ratex-parser/src/functions/cr.rs index 0ba15651..47288733 100644 --- a/crates/ratex-parser/src/functions/cr.rs +++ b/crates/ratex-parser/src/functions/cr.rs @@ -9,9 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\\\", "\\newline"], "cr", - 0, 0, + 0, + 0, None, - false, false, true, false, false, + false, + false, + true, + false, + false, handle_cr, ); } diff --git a/crates/ratex-parser/src/functions/def.rs b/crates/ratex-parser/src/functions/def.rs index 8e0e8710..19cd90b0 100644 --- a/crates/ratex-parser/src/functions/def.rs +++ b/crates/ratex-parser/src/functions/def.rs @@ -34,10 +34,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\global", "\\long", "\\\\globallong"], "internal", - 0, 0, None, + 0, + 0, + None, + false, + true, + true, + false, false, - true, true, - false, false, handle_prefix, ); @@ -46,10 +50,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\def", "\\gdef", "\\edef", "\\xdef"], "internal", - 0, 0, None, + 0, + 0, + None, + false, + true, + true, false, - true, true, - false, true, + true, handle_def, ); @@ -58,10 +66,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\let", "\\\\globallet"], "internal", - 0, 0, None, + 0, + 0, + None, false, - true, true, - false, true, + true, + true, + false, + true, handle_let, ); @@ -70,10 +82,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\futurelet", "\\\\globalfuture"], "internal", - 0, 0, None, + 0, + 0, + None, + false, + true, + true, false, - true, true, - false, true, + true, handle_futurelet, ); } @@ -221,7 +237,12 @@ fn handle_futurelet( }) } -fn let_command(ctx: &mut FunctionContext, name: &str, mut tok: ratex_lexer::token::Token, global: bool) { +fn let_command( + ctx: &mut FunctionContext, + name: &str, + mut tok: ratex_lexer::token::Token, + global: bool, +) { let macro_def = ctx.parser.gullet.get_macro(&tok.text).cloned(); let def = match macro_def { Some(d) => d, diff --git a/crates/ratex-parser/src/functions/delimsizing.rs b/crates/ratex-parser/src/functions/delimsizing.rs index f8560b2c..49e97921 100644 --- a/crates/ratex-parser/src/functions/delimsizing.rs +++ b/crates/ratex-parser/src/functions/delimsizing.rs @@ -11,22 +11,70 @@ struct DelimInfo { fn delim_info(name: &str) -> Option { match name { - "\\bigl" => Some(DelimInfo { mclass: "mopen", size: 1 }), - "\\Bigl" => Some(DelimInfo { mclass: "mopen", size: 2 }), - "\\biggl" => Some(DelimInfo { mclass: "mopen", size: 3 }), - "\\Biggl" => Some(DelimInfo { mclass: "mopen", size: 4 }), - "\\bigr" => Some(DelimInfo { mclass: "mclose", size: 1 }), - "\\Bigr" => Some(DelimInfo { mclass: "mclose", size: 2 }), - "\\biggr" => Some(DelimInfo { mclass: "mclose", size: 3 }), - "\\Biggr" => Some(DelimInfo { mclass: "mclose", size: 4 }), - "\\bigm" => Some(DelimInfo { mclass: "mrel", size: 1 }), - "\\Bigm" => Some(DelimInfo { mclass: "mrel", size: 2 }), - "\\biggm" => Some(DelimInfo { mclass: "mrel", size: 3 }), - "\\Biggm" => Some(DelimInfo { mclass: "mrel", size: 4 }), - "\\big" => Some(DelimInfo { mclass: "mord", size: 1 }), - "\\Big" => Some(DelimInfo { mclass: "mord", size: 2 }), - "\\bigg" => Some(DelimInfo { mclass: "mord", size: 3 }), - "\\Bigg" => Some(DelimInfo { mclass: "mord", size: 4 }), + "\\bigl" => Some(DelimInfo { + mclass: "mopen", + size: 1, + }), + "\\Bigl" => Some(DelimInfo { + mclass: "mopen", + size: 2, + }), + "\\biggl" => Some(DelimInfo { + mclass: "mopen", + size: 3, + }), + "\\Biggl" => Some(DelimInfo { + mclass: "mopen", + size: 4, + }), + "\\bigr" => Some(DelimInfo { + mclass: "mclose", + size: 1, + }), + "\\Bigr" => Some(DelimInfo { + mclass: "mclose", + size: 2, + }), + "\\biggr" => Some(DelimInfo { + mclass: "mclose", + size: 3, + }), + "\\Biggr" => Some(DelimInfo { + mclass: "mclose", + size: 4, + }), + "\\bigm" => Some(DelimInfo { + mclass: "mrel", + size: 1, + }), + "\\Bigm" => Some(DelimInfo { + mclass: "mrel", + size: 2, + }), + "\\biggm" => Some(DelimInfo { + mclass: "mrel", + size: 3, + }), + "\\Biggm" => Some(DelimInfo { + mclass: "mrel", + size: 4, + }), + "\\big" => Some(DelimInfo { + mclass: "mord", + size: 1, + }), + "\\Big" => Some(DelimInfo { + mclass: "mord", + size: 2, + }), + "\\bigg" => Some(DelimInfo { + mclass: "mord", + size: 3, + }), + "\\Bigg" => Some(DelimInfo { + mclass: "mord", + size: 4, + }), _ => None, } } @@ -35,15 +83,18 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", - "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", - "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", - "\\big", "\\Big", "\\bigg", "\\Bigg", + "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", + "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", "\\big", "\\Big", "\\bigg", "\\Bigg", ], "delimsizing", - 1, 0, + 1, + 0, Some(vec![ArgType::Primitive]), - false, false, true, false, false, + false, + false, + true, + false, + false, handle_delimsizing, ); } @@ -85,20 +136,47 @@ fn check_delimiter(node: &ParseNode, func_name: &str) -> ParseResult { fn is_valid_delimiter(text: &str) -> bool { matches!( text, - "(" | "\\lparen" | ")" | "\\rparen" - | "[" | "\\lbrack" | "]" | "\\rbrack" - | "\\{" | "\\lbrace" | "\\}" | "\\rbrace" - | "\\lfloor" | "\\rfloor" - | "\\lceil" | "\\rceil" - | "<" | ">" | "\\langle" | "\\rangle" | "\\lt" | "\\gt" - | "\\lvert" | "\\rvert" | "\\lVert" | "\\rVert" - | "\\lgroup" | "\\rgroup" - | "\\lmoustache" | "\\rmoustache" - | "/" | "\\backslash" - | "|" | "\\vert" | "\\|" | "\\Vert" - | "\\uparrow" | "\\Uparrow" - | "\\downarrow" | "\\Downarrow" - | "\\updownarrow" | "\\Updownarrow" + "(" | "\\lparen" + | ")" + | "\\rparen" + | "[" + | "\\lbrack" + | "]" + | "\\rbrack" + | "\\{" + | "\\lbrace" + | "\\}" + | "\\rbrace" + | "\\lfloor" + | "\\rfloor" + | "\\lceil" + | "\\rceil" + | "<" + | ">" + | "\\langle" + | "\\rangle" + | "\\lt" + | "\\gt" + | "\\lvert" + | "\\rvert" + | "\\lVert" + | "\\rVert" + | "\\lgroup" + | "\\rgroup" + | "\\lmoustache" + | "\\rmoustache" + | "/" + | "\\backslash" + | "|" + | "\\vert" + | "\\|" + | "\\Vert" + | "\\uparrow" + | "\\Uparrow" + | "\\downarrow" + | "\\Downarrow" + | "\\updownarrow" + | "\\Updownarrow" | "." ) } diff --git a/crates/ratex-parser/src/functions/enclose.rs b/crates/ratex-parser/src/functions/enclose.rs index 0a187af2..92b404a4 100644 --- a/crates/ratex-parser/src/functions/enclose.rs +++ b/crates/ratex-parser/src/functions/enclose.rs @@ -9,11 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\fbox"], "enclose", - 1, 0, + 1, + 0, Some(vec![ArgType::HBox]), false, - true, true, - false, false, + true, + true, + false, + false, handle_fbox, ); @@ -21,8 +24,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\cancel", "\\bcancel", "\\xcancel", "\\phase"], "enclose", - 1, 0, None, - false, false, true, false, false, + 1, + 0, + None, + false, + false, + true, + false, + false, handle_cancel, ); @@ -34,10 +43,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\sout"], "enclose", - 1, 0, None, + 1, + 0, + None, + false, + true, + true, + false, false, - true, true, - false, false, handle_cancel, ); @@ -45,11 +58,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\angl"], "enclose", - 1, 0, + 1, + 0, Some(vec![ArgType::HBox]), false, - false, true, - false, false, + false, + true, + false, + false, handle_angl, ); } diff --git a/crates/ratex-parser/src/functions/font.rs b/crates/ratex-parser/src/functions/font.rs index de61caa4..ee704532 100644 --- a/crates/ratex-parser/src/functions/font.rs +++ b/crates/ratex-parser/src/functions/font.rs @@ -8,15 +8,30 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\mathrm", "\\mathit", "\\mathbf", "\\mathnormal", "\\mathsfit", - "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf", + "\\mathrm", + "\\mathit", + "\\mathbf", + "\\mathnormal", + "\\mathsfit", + "\\mathbb", + "\\mathcal", + "\\mathfrak", + "\\mathscr", + "\\mathsf", "\\mathtt", - "\\Bbb", "\\bold", "\\frak", + "\\Bbb", + "\\bold", + "\\frak", ], "font", - 1, 0, None, - true, // allowed_in_argument - false, true, false, false, + 1, + 0, + None, + true, // allowed_in_argument + false, + true, + false, + false, handle_font, ); @@ -25,8 +40,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\boldsymbol", "\\bm"], "mclass", - 1, 0, None, - false, false, true, false, false, + 1, + 0, + None, + false, + false, + true, + false, + false, handle_boldsymbol, ); @@ -35,11 +56,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\emph"], "text", - 1, 0, + 1, + 0, Some(vec![ArgType::Text]), false, - true, true, - false, false, + true, + true, + false, + false, handle_emph, ); @@ -48,10 +72,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\rm", "\\sf", "\\tt", "\\bf", "\\it", "\\cal"], "font", - 0, 0, None, + 0, + 0, + None, + false, + true, + true, + false, false, - true, true, - false, false, handle_old_font, ); } diff --git a/crates/ratex-parser/src/functions/genfrac.rs b/crates/ratex-parser/src/functions/genfrac.rs index 62b19dc6..3c033179 100644 --- a/crates/ratex-parser/src/functions/genfrac.rs +++ b/crates/ratex-parser/src/functions/genfrac.rs @@ -9,8 +9,16 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\cfrac", "\\dfrac", "\\frac", "\\tfrac", "\\dbinom", "\\binom", "\\tbinom", - "\\\\atopfrac", "\\\\bracefrac", "\\\\brackfrac", + "\\cfrac", + "\\dfrac", + "\\frac", + "\\tfrac", + "\\dbinom", + "\\binom", + "\\tbinom", + "\\\\atopfrac", + "\\\\bracefrac", + "\\\\brackfrac", ], "genfrac", 2, @@ -32,10 +40,18 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { 6, 0, Some(vec![ - ArgType::Math, ArgType::Math, ArgType::Size, - ArgType::Text, ArgType::Math, ArgType::Math, + ArgType::Math, + ArgType::Math, + ArgType::Size, + ArgType::Text, + ArgType::Math, + ArgType::Math, ]), - true, false, true, false, false, + true, + false, + true, + false, + false, handle_genfrac_full, ); @@ -60,9 +76,12 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\above"], "infix", - 1, 0, + 1, + 0, Some(vec![ArgType::Size]), - false, false, true, + false, + false, + true, true, // infix false, handle_above_infix, @@ -73,9 +92,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\\\abovefrac"], "genfrac", - 3, 0, + 3, + 0, Some(vec![ArgType::Math, ArgType::Size, ArgType::Math]), - false, false, true, false, false, + false, + false, + true, + false, + false, handle_abovefrac, ); } @@ -96,16 +120,8 @@ fn handle_genfrac( "\\dbinom" | "\\binom" | "\\tbinom" => { (false, Some("(".to_string()), Some(")".to_string())) } - "\\\\bracefrac" => ( - false, - Some("\\{".to_string()), - Some("\\}".to_string()), - ), - "\\\\brackfrac" => ( - false, - Some("[".to_string()), - Some("]".to_string()), - ), + "\\\\bracefrac" => (false, Some("\\{".to_string()), Some("\\}".to_string())), + "\\\\brackfrac" => (false, Some("[".to_string()), Some("]".to_string())), _ => (true, None, None), }; @@ -156,7 +172,9 @@ fn handle_genfrac_full( let right_delim = extract_delim(&args[1]); let (has_bar_line, bar_size) = match &args[2] { - ParseNode::Size { value, is_blank, .. } => { + ParseNode::Size { + value, is_blank, .. + } => { if *is_blank { (true, None) } else { @@ -167,7 +185,12 @@ fn handle_genfrac_full( _ => (true, None), }; - let style_strs = [StyleStr::Display, StyleStr::Text, StyleStr::Script, StyleStr::Scriptscript]; + let style_strs = [ + StyleStr::Display, + StyleStr::Text, + StyleStr::Script, + StyleStr::Scriptscript, + ]; let style = match &args[3] { ParseNode::OrdGroup { body, .. } if !body.is_empty() => { extract_textord_num(&body[0]).and_then(|n| style_strs.get(n).cloned()) @@ -188,7 +211,12 @@ fn handle_genfrac_full( }; match style { - Some(s) => Ok(ParseNode::Styling { mode, style: s, body: vec![frac], loc: None }), + Some(s) => Ok(ParseNode::Styling { + mode, + style: s, + body: vec![frac], + loc: None, + }), None => Ok(frac), } } @@ -196,15 +224,17 @@ fn handle_genfrac_full( fn extract_delim(node: &ParseNode) -> Option { let text = match node { ParseNode::Atom { text, .. } => text.clone(), - ParseNode::OrdGroup { body, .. } if body.len() == 1 => { - match &body[0] { - ParseNode::Atom { text, .. } => text.clone(), - _ => return None, - } - } + ParseNode::OrdGroup { body, .. } if body.len() == 1 => match &body[0] { + ParseNode::Atom { text, .. } => text.clone(), + _ => return None, + }, _ => return None, }; - if text == "." || text.is_empty() { None } else { Some(text) } + if text == "." || text.is_empty() { + None + } else { + Some(text) + } } fn extract_textord_num(node: &ParseNode) -> Option { diff --git a/crates/ratex-parser/src/functions/hbox.rs b/crates/ratex-parser/src/functions/hbox.rs index aa324bad..5423fa98 100644 --- a/crates/ratex-parser/src/functions/hbox.rs +++ b/crates/ratex-parser/src/functions/hbox.rs @@ -9,11 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\hbox"], "hbox", - 1, 0, + 1, + 0, Some(vec![ArgType::Text]), false, - true, true, - false, true, + true, + true, + false, + true, handle_hbox, ); } diff --git a/crates/ratex-parser/src/functions/horiz_brace.rs b/crates/ratex-parser/src/functions/horiz_brace.rs index 7075a340..6462288f 100644 --- a/crates/ratex-parser/src/functions/horiz_brace.rs +++ b/crates/ratex-parser/src/functions/horiz_brace.rs @@ -14,8 +14,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { "\\underbracket", ], "horizBrace", - 1, 0, None, - false, false, true, false, false, + 1, + 0, + None, + false, + false, + true, + false, + false, handle_horiz_brace, ); } diff --git a/crates/ratex-parser/src/functions/href.rs b/crates/ratex-parser/src/functions/href.rs index cc4f357d..c0e6c660 100644 --- a/crates/ratex-parser/src/functions/href.rs +++ b/crates/ratex-parser/src/functions/href.rs @@ -9,11 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\href"], "href", - 2, 0, + 2, + 0, Some(vec![ArgType::Url, ArgType::Original]), false, - true, true, - false, false, + true, + true, + false, + false, handle_href, ); @@ -21,11 +24,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\url"], "href", - 1, 0, + 1, + 0, Some(vec![ArgType::Url]), false, - true, true, - false, false, + true, + true, + false, + false, handle_url, ); } diff --git a/crates/ratex-parser/src/functions/htmlmathml.rs b/crates/ratex-parser/src/functions/htmlmathml.rs index fcef94b0..74ffd838 100644 --- a/crates/ratex-parser/src/functions/htmlmathml.rs +++ b/crates/ratex-parser/src/functions/htmlmathml.rs @@ -9,8 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\html@mathml"], "htmlmathml", - 2, 0, None, - false, true, true, false, false, + 2, + 0, + None, + false, + true, + true, + false, + false, handle_htmlmathml, ); @@ -18,9 +24,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\htmlStyle"], "html", - 2, 0, + 2, + 0, Some(vec![ArgType::Raw, ArgType::Original]), - false, true, true, false, false, + false, + true, + true, + false, + false, handle_htmlstyle, ); } diff --git a/crates/ratex-parser/src/functions/kern.rs b/crates/ratex-parser/src/functions/kern.rs index cacf4e18..3fc3a089 100644 --- a/crates/ratex-parser/src/functions/kern.rs +++ b/crates/ratex-parser/src/functions/kern.rs @@ -9,10 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\kern", "\\mkern", "\\hskip", "\\mskip"], "kern", - 1, 0, + 1, + 0, Some(vec![ArgType::Size]), - true, // allowed_in_argument - true, true, false, false, + true, // allowed_in_argument + true, + true, + false, + false, handle_kern, ); } diff --git a/crates/ratex-parser/src/functions/lap.rs b/crates/ratex-parser/src/functions/lap.rs index 7b28cb66..f7fd3edf 100644 --- a/crates/ratex-parser/src/functions/lap.rs +++ b/crates/ratex-parser/src/functions/lap.rs @@ -9,10 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\mathllap", "\\mathrlap", "\\mathclap"], "lap", - 1, 0, None, + 1, + 0, + None, + false, + true, + true, + false, false, - true, true, - false, false, handle_lap, ); } diff --git a/crates/ratex-parser/src/functions/left_right.rs b/crates/ratex-parser/src/functions/left_right.rs index 8a664f55..fc4c04f3 100644 --- a/crates/ratex-parser/src/functions/left_right.rs +++ b/crates/ratex-parser/src/functions/left_right.rs @@ -9,8 +9,13 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\left"], "leftright", - 1, 0, None, - false, false, true, false, + 1, + 0, + None, + false, + false, + true, + false, true, // primitive handle_left, ); @@ -19,8 +24,13 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\right"], "leftright-right", - 1, 0, None, - false, false, true, false, + 1, + 0, + None, + false, + false, + true, + false, true, // primitive handle_right, ); @@ -29,8 +39,13 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\middle"], "middle", - 1, 0, None, - false, false, true, false, + 1, + 0, + None, + false, + false, + true, + false, true, handle_middle, ); diff --git a/crates/ratex-parser/src/functions/math.rs b/crates/ratex-parser/src/functions/math.rs index e2232242..3b46e82a 100644 --- a/crates/ratex-parser/src/functions/math.rs +++ b/crates/ratex-parser/src/functions/math.rs @@ -9,8 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\(", "$"], "styling", - 0, 0, None, - false, true, false, false, false, + 0, + 0, + None, + false, + true, + false, + false, + false, handle_math_switch, ); @@ -18,8 +24,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\)", "\\]"], "text", - 0, 0, None, - false, true, false, false, false, + 0, + 0, + None, + false, + true, + false, + false, + false, handle_math_close, ); } diff --git a/crates/ratex-parser/src/functions/mathchoice.rs b/crates/ratex-parser/src/functions/mathchoice.rs index d31e1e34..474243e8 100644 --- a/crates/ratex-parser/src/functions/mathchoice.rs +++ b/crates/ratex-parser/src/functions/mathchoice.rs @@ -9,8 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\mathchoice"], "mathchoice", - 4, 0, None, - false, false, true, false, true, + 4, + 0, + None, + false, + false, + true, + false, + true, handle_mathchoice, ); } diff --git a/crates/ratex-parser/src/functions/mclass.rs b/crates/ratex-parser/src/functions/mclass.rs index 57f5486b..3c998b2e 100644 --- a/crates/ratex-parser/src/functions/mclass.rs +++ b/crates/ratex-parser/src/functions/mclass.rs @@ -8,12 +8,23 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\mathord", "\\mathbin", "\\mathrel", "\\mathopen", - "\\mathclose", "\\mathpunct", "\\mathinner", + "\\mathord", + "\\mathbin", + "\\mathrel", + "\\mathopen", + "\\mathclose", + "\\mathpunct", + "\\mathinner", ], "mclass", - 1, 0, None, - false, false, true, false, true, + 1, + 0, + None, + false, + false, + true, + false, + true, handle_mclass, ); @@ -21,8 +32,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\stackrel", "\\overset", "\\underset"], "mclass", - 2, 0, None, - false, false, true, false, false, + 2, + 0, + None, + false, + false, + true, + false, + false, handle_stackrel, ); @@ -31,8 +48,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\@binrel"], "mclass", - 2, 0, None, - false, false, true, false, false, + 2, + 0, + None, + false, + false, + true, + false, + false, handle_binrel, ); } diff --git a/crates/ratex-parser/src/functions/mod.rs b/crates/ratex-parser/src/functions/mod.rs index fcc9a799..557f7659 100644 --- a/crates/ratex-parser/src/functions/mod.rs +++ b/crates/ratex-parser/src/functions/mod.rs @@ -1,47 +1,47 @@ -pub mod genfrac; -pub mod sqrt; -pub mod op; pub mod accent; -pub mod font; +pub mod arrow; +pub mod bussproofs; +pub mod char_cmd; pub mod color; -pub mod sizing; -pub mod delimsizing; -pub mod left_right; -pub mod spacing; -pub mod styling; -pub mod overline; -pub mod kern; -pub mod phantom; -pub mod text; pub mod cr; -pub mod relax; -pub mod verb; -pub mod symbols_cmd; +pub mod def; +pub mod delimsizing; +pub mod enclose; pub mod environment; -pub mod mclass; -pub mod operatorname; +pub mod font; +pub mod genfrac; +pub mod hbox; pub mod horiz_brace; -pub mod arrow; -pub mod enclose; -pub mod rule; pub mod href; -pub mod hbox; -pub mod lap; -pub mod raisebox; -pub mod vcenter; -pub mod pmb; -pub mod mathchoice; -pub mod def; pub mod htmlmathml; -pub mod char_cmd; +pub mod kern; +pub mod lap; +pub mod left_right; pub mod math; -pub mod tag; +pub mod mathchoice; +pub mod mclass; pub mod nonumber; -pub mod bussproofs; +pub mod op; +pub mod operatorname; +pub mod overline; +pub mod phantom; +pub mod pmb; +pub mod raisebox; +pub mod relax; +pub mod rule; +pub mod sizing; +pub mod spacing; +pub mod sqrt; +pub mod styling; +pub mod symbols_cmd; +pub mod tag; +pub mod text; +pub mod vcenter; +pub mod verb; -use std::collections::HashMap; use crate::error::ParseResult; use crate::parse_node::{Mode, ParseNode}; +use std::collections::HashMap; use ratex_lexer::token::Token; @@ -54,8 +54,11 @@ pub struct FunctionContext<'a, 'b> { } /// Handler function signature. -pub type FunctionHandler = - fn(ctx: &mut FunctionContext, args: Vec, opt_args: Vec>) -> ParseResult; +pub type FunctionHandler = fn( + ctx: &mut FunctionContext, + args: Vec, + opt_args: Vec>, +) -> ParseResult; /// Argument types for function parameters. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/ratex-parser/src/functions/op.rs b/crates/ratex-parser/src/functions/op.rs index 623cc30d..0954f724 100644 --- a/crates/ratex-parser/src/functions/op.rs +++ b/crates/ratex-parser/src/functions/op.rs @@ -9,16 +9,42 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap", - "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes", - "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint", - "\u{220F}", "\u{2210}", "\u{2211}", "\u{22C0}", "\u{22C1}", - "\u{22C2}", "\u{22C3}", "\u{2A00}", "\u{2A01}", "\u{2A02}", - "\u{2A04}", "\u{2A06}", + "\\coprod", + "\\bigvee", + "\\bigwedge", + "\\biguplus", + "\\bigcap", + "\\bigcup", + "\\intop", + "\\prod", + "\\sum", + "\\bigotimes", + "\\bigoplus", + "\\bigodot", + "\\bigsqcup", + "\\smallint", + "\u{220F}", + "\u{2210}", + "\u{2211}", + "\u{22C0}", + "\u{22C1}", + "\u{22C2}", + "\u{22C3}", + "\u{2A00}", + "\u{2A01}", + "\u{2A02}", + "\u{2A04}", + "\u{2A06}", ], "op", - 0, 0, None, - false, false, true, false, false, + 0, + 0, + None, + false, + false, + true, + false, + false, handle_op_symbol_limits, ); @@ -26,15 +52,20 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg", - "\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg", - "\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp", - "\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", + "\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg", "\\arg", "\\ch", "\\cos", + "\\cosec", "\\cosh", "\\cot", "\\cotg", "\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", + "\\dim", "\\exp", "\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh", "\\sh", "\\tan", "\\tanh", "\\tg", "\\th", ], "op", - 0, 0, None, - false, false, true, false, false, + 0, + 0, + None, + false, + false, + true, + false, + false, handle_op_text_nolimits, ); @@ -45,8 +76,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { "\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup", ], "op", - 0, 0, None, - false, false, true, false, false, + 0, + 0, + None, + false, + false, + true, + false, + false, handle_op_text_limits, ); @@ -54,13 +91,18 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\int", "\\iint", "\\iiint", "\\oint", "\\oiint", "\\oiiint", - "\u{222B}", "\u{222C}", "\u{222D}", "\u{222E}", "\u{222F}", "\u{2230}", + "\\int", "\\iint", "\\iiint", "\\oint", "\\oiint", "\\oiiint", "\u{222B}", "\u{222C}", + "\u{222D}", "\u{222E}", "\u{222F}", "\u{2230}", ], "op", - 0, 0, None, + 0, + 0, + None, true, // allowed_in_argument - false, true, false, false, + false, + true, + false, + false, handle_op_symbol_nolimits, ); @@ -69,8 +111,13 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\mathop"], "op", - 1, 0, None, - false, false, true, false, + 1, + 0, + None, + false, + false, + true, + false, true, // primitive handle_mathop, ); diff --git a/crates/ratex-parser/src/functions/operatorname.rs b/crates/ratex-parser/src/functions/operatorname.rs index 33b8cc33..413b7ac0 100644 --- a/crates/ratex-parser/src/functions/operatorname.rs +++ b/crates/ratex-parser/src/functions/operatorname.rs @@ -9,8 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\operatorname@", "\\operatornamewithlimits"], "operatorname", - 1, 0, None, - false, false, true, false, false, + 1, + 0, + None, + false, + false, + true, + false, + false, handle_operatorname, ); } diff --git a/crates/ratex-parser/src/functions/overline.rs b/crates/ratex-parser/src/functions/overline.rs index 73d29582..ff88ce38 100644 --- a/crates/ratex-parser/src/functions/overline.rs +++ b/crates/ratex-parser/src/functions/overline.rs @@ -9,8 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\overline"], "overline", - 1, 0, None, - false, false, true, false, false, + 1, + 0, + None, + false, + false, + true, + false, + false, handle_overline, ); @@ -18,11 +24,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\underline"], "underline", - 1, 0, None, + 1, + 0, + None, false, - true, // allowed_in_text + true, // allowed_in_text true, - false, false, + false, + false, handle_underline, ); } diff --git a/crates/ratex-parser/src/functions/phantom.rs b/crates/ratex-parser/src/functions/phantom.rs index 6a86be97..8c82dc45 100644 --- a/crates/ratex-parser/src/functions/phantom.rs +++ b/crates/ratex-parser/src/functions/phantom.rs @@ -9,8 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\phantom"], "phantom", - 1, 0, None, - true, true, true, false, false, + 1, + 0, + None, + true, + true, + true, + false, + false, handle_phantom, ); @@ -18,8 +24,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\vphantom"], "vphantom", - 1, 0, None, - true, true, true, false, false, + 1, + 0, + None, + true, + true, + true, + false, + false, handle_vphantom, ); @@ -30,8 +42,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\smash"], "smash", - 1, 1, None, - true, true, true, false, false, + 1, + 1, + None, + true, + true, + true, + false, + false, handle_smash, ); } diff --git a/crates/ratex-parser/src/functions/pmb.rs b/crates/ratex-parser/src/functions/pmb.rs index 819f1aa9..b8a8d374 100644 --- a/crates/ratex-parser/src/functions/pmb.rs +++ b/crates/ratex-parser/src/functions/pmb.rs @@ -9,17 +9,25 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\pmb"], "pmb", - 1, 0, None, + 1, + 0, + None, + false, + true, + true, + false, false, - true, true, - false, false, handle_pmb, ); } fn binrel_class(arg: &ParseNode) -> String { let atom = if let ParseNode::OrdGroup { body, .. } = arg { - if !body.is_empty() { &body[0] } else { arg } + if !body.is_empty() { + &body[0] + } else { + arg + } } else { arg }; diff --git a/crates/ratex-parser/src/functions/raisebox.rs b/crates/ratex-parser/src/functions/raisebox.rs index 095a20b5..caf2030d 100644 --- a/crates/ratex-parser/src/functions/raisebox.rs +++ b/crates/ratex-parser/src/functions/raisebox.rs @@ -9,11 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\raisebox"], "raisebox", - 2, 0, + 2, + 0, Some(vec![ArgType::Size, ArgType::HBox]), false, - true, true, - false, false, + true, + true, + false, + false, handle_raisebox, ); } diff --git a/crates/ratex-parser/src/functions/relax.rs b/crates/ratex-parser/src/functions/relax.rs index 11b525a6..aabcb6c5 100644 --- a/crates/ratex-parser/src/functions/relax.rs +++ b/crates/ratex-parser/src/functions/relax.rs @@ -9,11 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\relax"], "internal", - 0, 0, None, - true, // allowed_in_argument - true, // allowed_in_text + 0, + 0, + None, + true, // allowed_in_argument + true, // allowed_in_text true, - false, false, + false, + false, handle_relax, ); } diff --git a/crates/ratex-parser/src/functions/rule.rs b/crates/ratex-parser/src/functions/rule.rs index 3365fe20..85616884 100644 --- a/crates/ratex-parser/src/functions/rule.rs +++ b/crates/ratex-parser/src/functions/rule.rs @@ -9,11 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\rule"], "rule", - 2, 1, + 2, + 1, Some(vec![ArgType::Size, ArgType::Size, ArgType::Size]), false, - true, true, - false, false, + true, + true, + false, + false, handle_rule, ); } diff --git a/crates/ratex-parser/src/functions/sizing.rs b/crates/ratex-parser/src/functions/sizing.rs index 6d1d4ea8..400a5413 100644 --- a/crates/ratex-parser/src/functions/sizing.rs +++ b/crates/ratex-parser/src/functions/sizing.rs @@ -5,8 +5,17 @@ use crate::functions::{define_function_full, FunctionContext, FunctionSpec}; use crate::parse_node::ParseNode; static SIZE_FUNCS: &[&str] = &[ - "\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small", - "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", + "\\tiny", + "\\sixptsize", + "\\scriptsize", + "\\footnotesize", + "\\small", + "\\normalsize", + "\\large", + "\\Large", + "\\LARGE", + "\\huge", + "\\Huge", ]; pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { @@ -14,11 +23,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, SIZE_FUNCS, "sizing", - 0, 0, None, + 0, + 0, + None, false, true, // allowed_in_text true, - false, false, + false, + false, handle_sizing, ); } diff --git a/crates/ratex-parser/src/functions/spacing.rs b/crates/ratex-parser/src/functions/spacing.rs index e533bcc7..41c3b3cd 100644 --- a/crates/ratex-parser/src/functions/spacing.rs +++ b/crates/ratex-parser/src/functions/spacing.rs @@ -8,16 +8,26 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, &[ - "\\quad", "\\qquad", "\\enspace", "\\thinspace", "\\medspace", - "\\thickspace", "\\negthinspace", "\\negmedspace", "\\negthickspace", + "\\quad", + "\\qquad", + "\\enspace", + "\\thinspace", + "\\medspace", + "\\thickspace", + "\\negthinspace", + "\\negmedspace", + "\\negthickspace", "\\nobreakspace", ], "spacing", - 0, 0, None, - true, // allowed_in_argument - true, // allowed_in_text + 0, + 0, + None, + true, // allowed_in_argument + true, // allowed_in_text true, - false, false, + false, + false, handle_spacing, ); @@ -25,12 +35,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\hspace"], "kern", - 1, 0, + 1, + 0, Some(vec![crate::functions::ArgType::Size]), false, true, true, - false, false, + false, + false, handle_hspace, ); @@ -39,8 +51,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\hfill"], "spacing", - 0, 0, None, - false, true, true, false, false, + 0, + 0, + None, + false, + true, + true, + false, + false, handle_spacing, ); } diff --git a/crates/ratex-parser/src/functions/sqrt.rs b/crates/ratex-parser/src/functions/sqrt.rs index cc828a5e..2f8dc551 100644 --- a/crates/ratex-parser/src/functions/sqrt.rs +++ b/crates/ratex-parser/src/functions/sqrt.rs @@ -10,7 +10,7 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { &["\\sqrt"], "sqrt", 1, - 1, // one optional arg [n] + 1, // one optional arg [n] None, false, false, diff --git a/crates/ratex-parser/src/functions/styling.rs b/crates/ratex-parser/src/functions/styling.rs index 12819433..beef828c 100644 --- a/crates/ratex-parser/src/functions/styling.rs +++ b/crates/ratex-parser/src/functions/styling.rs @@ -14,11 +14,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { "\\scriptscriptstyle", ], "styling", - 0, 0, None, - true, // allowed_in_argument + 0, + 0, + None, + true, // allowed_in_argument true, true, - false, false, + false, + false, handle_styling, ); } diff --git a/crates/ratex-parser/src/functions/text.rs b/crates/ratex-parser/src/functions/text.rs index b54d5185..f29133f7 100644 --- a/crates/ratex-parser/src/functions/text.rs +++ b/crates/ratex-parser/src/functions/text.rs @@ -7,15 +7,28 @@ use crate::parse_node::ParseNode; pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { define_function_full( map, - &["\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal", - "\\textbf", "\\textit", "\\textmd", "\\textup", "\\textsl", "\\textsc"], + &[ + "\\text", + "\\textrm", + "\\textsf", + "\\texttt", + "\\textnormal", + "\\textbf", + "\\textit", + "\\textmd", + "\\textup", + "\\textsl", + "\\textsc", + ], "text", - 1, 0, + 1, + 0, Some(vec![ArgType::Text]), - true, // allowed_in_argument - true, // allowed_in_text + true, // allowed_in_argument + true, // allowed_in_text true, - false, false, + false, + false, handle_text, ); } diff --git a/crates/ratex-parser/src/functions/vcenter.rs b/crates/ratex-parser/src/functions/vcenter.rs index 0c12f8e0..51f0223e 100644 --- a/crates/ratex-parser/src/functions/vcenter.rs +++ b/crates/ratex-parser/src/functions/vcenter.rs @@ -9,11 +9,14 @@ pub fn register(map: &mut HashMap<&'static str, FunctionSpec>) { map, &["\\vcenter"], "vcenter", - 1, 0, + 1, + 0, Some(vec![ArgType::Original]), false, - false, true, - false, false, + false, + true, + false, + false, handle_vcenter, ); } diff --git a/crates/ratex-parser/src/lib.rs b/crates/ratex-parser/src/lib.rs index 6f01c315..88546d03 100644 --- a/crates/ratex-parser/src/lib.rs +++ b/crates/ratex-parser/src/lib.rs @@ -1,8 +1,8 @@ pub mod environments; pub mod error; pub mod functions; -pub mod mhchem; pub mod macro_expander; +pub mod mhchem; pub mod parse_node; pub mod parser; pub mod unicode_sup_sub; diff --git a/crates/ratex-parser/src/macro_expander.rs b/crates/ratex-parser/src/macro_expander.rs index 9cc9d31d..8d04736b 100644 --- a/crates/ratex-parser/src/macro_expander.rs +++ b/crates/ratex-parser/src/macro_expander.rs @@ -21,10 +21,7 @@ pub enum MacroDefinition { /// Simple string expansion (e.g., `\def\foo{bar}` → "bar") Text(String), /// Pre-tokenized expansion with argument count - Tokens { - tokens: Vec, - num_args: usize, - }, + Tokens { tokens: Vec, num_args: usize }, /// Function-based macro (consumes tokens directly, returns expansion) Function(FnMacroHandler), } @@ -84,7 +81,8 @@ impl MacroNamespace { fn set(&mut self, name: String, def: MacroDefinition) { if let Some(undo) = self.group_stack.last_mut() { - undo.entry(name.clone()).or_insert_with(|| self.current.get(&name).cloned()); + undo.entry(name.clone()) + .or_insert_with(|| self.current.get(&name).cloned()); } self.current.insert(name, def); } @@ -105,8 +103,12 @@ impl MacroNamespace { if let Some(undo) = self.group_stack.pop() { for (name, old_val) in undo { match old_val { - Some(def) => { self.current.insert(name, def); } - None => { self.current.remove(&name); } + Some(def) => { + self.current.insert(name, def); + } + None => { + self.current.remove(&name); + } } } } @@ -693,9 +695,7 @@ impl<'a> MacroExpander<'a> { // \operatorname: \@ifstar\operatornamewithlimits\operatorname@ self.macros.set( "\\operatorname".to_string(), - MacroDefinition::Text( - "\\@ifstar\\operatornamewithlimits\\operatorname@".to_string(), - ), + MacroDefinition::Text("\\@ifstar\\operatornamewithlimits\\operatorname@".to_string()), ); // \message{...}: consume argument and discard (no-op) @@ -815,9 +815,11 @@ impl<'a> MacroExpander<'a> { // Tokens are reversed (last token first in vec), scan in logical order for i in (0..content.len()).rev() { let t = &content[i]; - if t.text == "{" { depth += 1; } - else if t.text == "}" { depth -= 1; } - else if depth == 0 && t.text == "|" { + if t.text == "{" { + depth += 1; + } else if t.text == "}" { + depth -= 1; + } else if depth == 0 && t.text == "|" { // Check for || (double pipe) → middleDouble if !middle_double.is_empty() && i > 0 && content[i - 1].text == "|" { _first_pipe_idx = Some(i); @@ -885,10 +887,8 @@ impl<'a> MacroExpander<'a> { } pub fn set_text_macro(&mut self, name: &str, text: &str) { - self.macros.set( - name.to_string(), - MacroDefinition::Text(text.to_string()), - ); + self.macros + .set(name.to_string(), MacroDefinition::Text(text.to_string())); } pub fn get_macro(&self, name: &str) -> Option<&MacroDefinition> { @@ -1278,7 +1278,10 @@ fn handle_newcommand( ) -> ParseResult> { let name_arg = me.consume_arg(None)?; // name_arg.tokens is reversed (stack order); last element = first token in original - let name = name_arg.tokens.last().map_or_else(String::new, |t| t.text.clone()); + let name = name_arg + .tokens + .last() + .map_or_else(String::new, |t| t.text.clone()); let exists = me.is_defined(&name); if exists && !exists_ok { diff --git a/crates/ratex-parser/src/mhchem/actions.rs b/crates/ratex-parser/src/mhchem/actions.rs index ab37633f..c9c95ef7 100644 --- a/crates/ratex-parser/src/mhchem/actions.rs +++ b/crates/ratex-parser/src/mhchem/actions.rs @@ -10,8 +10,7 @@ use regex::Regex; use serde_json::{json, Value}; use std::sync::LazyLock; -static RE_DIGITS_ONLY: LazyLock = - LazyLock::new(|| Regex::new(r"^[0-9]+$").unwrap()); +static RE_DIGITS_ONLY: LazyLock = LazyLock::new(|| Regex::new(r"^[0-9]+$").unwrap()); static RE_CELSIUS_C: LazyLock = LazyLock::new(|| Regex::new(r"\u{00B0}C|\^oC|\^\{o\}C").unwrap()); static RE_CELSIUS_F: LazyLock = @@ -255,7 +254,11 @@ fn ce_o_after_d(ctx: &ParserCtx, buffer: &mut Buffer, m: &MatchToken) -> MhchemR Ok(ret) } -fn ce_charge_or_bond(ctx: &ParserCtx, buffer: &mut Buffer, m: &MatchToken) -> MhchemResult> { +fn ce_charge_or_bond( + ctx: &ParserCtx, + buffer: &mut Buffer, + m: &MatchToken, +) -> MhchemResult> { if buffer.begins_with_bond { let mut ret = ce_output(ctx, buffer, None)?; ret.push(json!({"type_": "bond", "kind_": "-"})); @@ -275,12 +278,17 @@ fn ce_after_od( let dash = token_string(m); let o = buffer.o.as_deref().unwrap_or(""); let c1 = match_pattern(ctx.data, "orbital", o).ok().flatten(); - let c2 = match_pattern(ctx.data, "one lowercase greek letter $", o).ok().flatten(); - let c3 = match_pattern(ctx.data, "one lowercase latin letter $", o).ok().flatten(); - let c4 = match_pattern(ctx.data, "$one lowercase latin letter$ $", o).ok().flatten(); + let c2 = match_pattern(ctx.data, "one lowercase greek letter $", o) + .ok() + .flatten(); + let c3 = match_pattern(ctx.data, "one lowercase latin letter $", o) + .ok() + .flatten(); + let c4 = match_pattern(ctx.data, "$one lowercase latin letter$ $", o) + .ok() + .flatten(); let h_orb = c1.as_ref().map(|h| h.remainder.is_empty()).unwrap_or(false); - let hyphen_follows = - dash == "-" && (h_orb || c2.is_some() || c3.is_some() || c4.is_some()); + let hyphen_follows = dash == "-" && (h_orb || c2.is_some() || c3.is_some() || c4.is_some()); if hyphen_follows && Buffer::is_slot_empty(&buffer.a) @@ -302,8 +310,9 @@ fn ce_after_od( return Ok(ret); } - let digits_d = - match_pattern(ctx.data, "digits", buffer.d.as_deref().unwrap_or("")).ok().flatten(); + let digits_d = match_pattern(ctx.data, "digits", buffer.d.as_deref().unwrap_or("")) + .ok() + .flatten(); let d_only = digits_d.map(|h| h.remainder.is_empty()).unwrap_or(false); if is_after_d && d_only { cat(&mut buffer.d, m); @@ -315,7 +324,11 @@ fn ce_after_od( Ok(ret) } -fn ce_output(ctx: &ParserCtx, buffer: &mut Buffer, entity: Option<&Value>) -> MhchemResult> { +fn ce_output( + ctx: &ParserCtx, + buffer: &mut Buffer, + entity: Option<&Value>, +) -> MhchemResult> { let entity_follows = entity.and_then(|v| { if let Some(n) = v.as_u64() { Some(n as i32) @@ -351,15 +364,25 @@ fn ce_output(ctx: &ParserCtx, buffer: &mut Buffer, entity: Option<&Value>) -> Mh .is_some() { d_type = Some("oxidation".into()); - } else if !Buffer::is_slot_empty(&buffer.o) && d_type.as_deref() == Some("kv") && Buffer::is_slot_empty(&buffer.q) { + } else if !Buffer::is_slot_empty(&buffer.o) + && d_type.as_deref() == Some("kv") + && Buffer::is_slot_empty(&buffer.q) + { d_type = None; } - if Buffer::is_slot_empty(&buffer.o) && Buffer::is_slot_empty(&buffer.q) && Buffer::is_slot_empty(&buffer.d) && Buffer::is_slot_empty(&buffer.b) && Buffer::is_slot_empty(&buffer.p) + if Buffer::is_slot_empty(&buffer.o) + && Buffer::is_slot_empty(&buffer.q) + && Buffer::is_slot_empty(&buffer.d) + && Buffer::is_slot_empty(&buffer.b) + && Buffer::is_slot_empty(&buffer.p) && entity_follows != Some(2) { buffer.o = buffer.a.take(); - } else if Buffer::is_slot_empty(&buffer.o) && Buffer::is_slot_empty(&buffer.q) && Buffer::is_slot_empty(&buffer.d) && (!Buffer::is_slot_empty(&buffer.b) || !Buffer::is_slot_empty(&buffer.p)) + } else if Buffer::is_slot_empty(&buffer.o) + && Buffer::is_slot_empty(&buffer.q) + && Buffer::is_slot_empty(&buffer.d) + && (!Buffer::is_slot_empty(&buffer.b) || !Buffer::is_slot_empty(&buffer.p)) { buffer.o = buffer.a.take(); buffer.d = buffer.b.take(); @@ -371,7 +394,11 @@ fn ce_output(ctx: &ParserCtx, buffer: &mut Buffer, entity: Option<&Value>) -> Mh let p = engine::go_machine(ctx, buffer.p.take().unwrap_or_default().as_str(), "pq")?; let o = engine::go_machine(ctx, buffer.o.take().unwrap_or_default().as_str(), "o")?; let q = engine::go_machine(ctx, buffer.q.take().unwrap_or_default().as_str(), "pq")?; - let d_sm = if d_type.as_deref() == Some("oxidation") { "oxidation" } else { "bd" }; + let d_sm = if d_type.as_deref() == Some("oxidation") { + "oxidation" + } else { + "bd" + }; let d = engine::go_machine(ctx, buffer.d.take().unwrap_or_default().as_str(), d_sm)?; let mut chem = serde_json::Map::new(); @@ -654,11 +681,7 @@ fn global_action( Ok(vec![]) } "insert" => { - let opt = spec - .option - .as_ref() - .and_then(|v| v.as_str()) - .unwrap_or(""); + let opt = spec.option.as_ref().and_then(|v| v.as_str()).unwrap_or(""); Ok(vec![json!({"type_": opt})]) } "insert+p1" => Ok(vec![json!({ @@ -679,7 +702,9 @@ fn global_action( })]) } "copy" => Ok(vec![Value::String(token_string(m))]), - "rm" => Ok(vec![json!({"type_": "rm", "p1": match_str(m).unwrap_or_default()})]), + "rm" => Ok(vec![ + json!({"type_": "rm", "p1": match_str(m).unwrap_or_default()}), + ]), "text" => Ok(engine::go_machine(ctx, &token_string(m), "text")?), "{text}" => { let mut v = vec![Value::String("{".into())]; @@ -688,11 +713,7 @@ fn global_action( Ok(v) } "tex-math" => Ok(engine::go_machine(ctx, &token_string(m), "tex-math")?), - "tex-math tight" => Ok(engine::go_machine( - ctx, - &token_string(m), - "tex-math tight", - )?), + "tex-math tight" => Ok(engine::go_machine(ctx, &token_string(m), "tex-math tight")?), "bond" => { let kind = spec .option @@ -707,7 +728,9 @@ fn global_action( let MatchToken::A(v) = m else { return Err(MhchemError::msg("color0-output")); }; - Ok(vec![json!({"type_": "color0", "color": v.first().cloned().unwrap_or_default()})]) + Ok(vec![ + json!({"type_": "color0", "color": v.first().cloned().unwrap_or_default()}), + ]) } "ce" => Ok(engine::go_machine(ctx, &token_string(m), "ce")?), "1/2" => half_action(m), diff --git a/crates/ratex-parser/src/mhchem/data.rs b/crates/ratex-parser/src/mhchem/data.rs index fd64113d..23cc8ebd 100644 --- a/crates/ratex-parser/src/mhchem/data.rs +++ b/crates/ratex-parser/src/mhchem/data.rs @@ -37,9 +37,8 @@ impl MhchemData { let src = src_val .as_str() .ok_or_else(|| MhchemError::msg("pattern not string"))?; - let re = Regex::new(src).map_err(|e| { - MhchemError::msg(format!("regex compile {:?}: {}", k, e)) - })?; + let re = Regex::new(src) + .map_err(|e| MhchemError::msg(format!("regex compile {:?}: {}", k, e)))?; map.insert(k.clone(), re); } diff --git a/crates/ratex-parser/src/mhchem/engine.rs b/crates/ratex-parser/src/mhchem/engine.rs index c444d8dc..4e915823 100644 --- a/crates/ratex-parser/src/mhchem/engine.rs +++ b/crates/ratex-parser/src/mhchem/engine.rs @@ -20,7 +20,11 @@ fn normalize_input(s: &str) -> String { out } -pub(crate) fn go_machine(ctx: &ParserCtx<'_>, input: &str, machine: &str) -> MhchemResult> { +pub(crate) fn go_machine( + ctx: &ParserCtx<'_>, + input: &str, + machine: &str, +) -> MhchemResult> { let mut input = normalize_input(input); if input.is_empty() { return Ok(vec![]); diff --git a/crates/ratex-parser/src/mhchem/mod.rs b/crates/ratex-parser/src/mhchem/mod.rs index 9cd6ab2e..e404da9c 100644 --- a/crates/ratex-parser/src/mhchem/mod.rs +++ b/crates/ratex-parser/src/mhchem/mod.rs @@ -79,11 +79,7 @@ mod tests { #[test] fn reaction_arrow() { let t = chem_parse_str("2H + O -> H2O", "ce").expect("mhchem"); - assert!( - t.contains("rightarrow") || t.contains("->"), - "{}", - t - ); + assert!(t.contains("rightarrow") || t.contains("->"), "{}", t); } #[test] @@ -116,5 +112,4 @@ mod tests { let tex = chem_parse_str(inner, "ce").expect("mhchem"); crate::parser::parse(&tex).expect("mhchem TeX should parse"); } - } diff --git a/crates/ratex-parser/src/mhchem/patterns.rs b/crates/ratex-parser/src/mhchem/patterns.rs index db4e89c6..36b6231f 100644 --- a/crates/ratex-parser/src/mhchem/patterns.rs +++ b/crates/ratex-parser/src/mhchem/patterns.rs @@ -168,9 +168,9 @@ fn regex_match_token(re: &Regex, input: &str) -> Option<(MatchToken, usize)> { Some((MatchToken::S(m0.as_str().to_string()), end)) } -static RE_AGG_OPEN: LazyLock = LazyLock::new(|| Regex::new(r"^\([a-z]{1,3}(?=[\),])").unwrap()); -static RE_CMD_BRACE: LazyLock = - LazyLock::new(|| Regex::new(r"^\\[a-zA-Z]+\{").unwrap()); +static RE_AGG_OPEN: LazyLock = + LazyLock::new(|| Regex::new(r"^\([a-z]{1,3}(?=[\),])").unwrap()); +static RE_CMD_BRACE: LazyLock = LazyLock::new(|| Regex::new(r"^\\[a-zA-Z]+\{").unwrap()); static RE_BEFORE_BRACE: LazyLock = LazyLock::new(|| Regex::new(r"^(?=\{)").unwrap()); static RE_NEG_POW: LazyLock = LazyLock::new(|| { Regex::new( @@ -194,7 +194,8 @@ static RE_AMOUNT_MAIN: LazyLock = LazyLock::new(|| { static RE_AMOUNT_DOLLAR: LazyLock = LazyLock::new(|| { Regex::new( r"^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$", - ).unwrap() + ) + .unwrap() }); static RE_FORMULA_PAREN_ONLY: LazyLock = LazyLock::new(|| Regex::new(r"^\([a-z]+\)$").unwrap()); @@ -247,7 +248,8 @@ fn pattern_state_of_agg(input: &str) -> MhchemResult> { None, false, )? { - if RE_SOA_REMAINDER.find(&h.remainder) + if RE_SOA_REMAINDER + .find(&h.remainder) .ok() .flatten() .filter(|m| m.start() == 0) @@ -273,7 +275,8 @@ fn pattern_amount(input: &str) -> MhchemResult> { remainder: input[m.end()..].to_string(), })); } - let Some(h) = find_observe_groups(input, "", Beg::Str("$"), "$", End::Str(""), None, false)? else { + let Some(h) = find_observe_groups(input, "", Beg::Str("$"), "$", End::Str(""), None, false)? + else { return Ok(None); }; let MatchToken::S(ref s) = h.token else { @@ -293,21 +296,33 @@ fn pattern_formula(input: &str) -> MhchemResult> { if RE_FORMULA_PAREN_ONLY.is_match(input).unwrap_or(false) { return Ok(None); } - Ok(RE_FORMULA_MAIN.find(input).ok().flatten().map(|m| PatternHit { - token: MatchToken::S(m.as_str().to_string()), - remainder: input[m.end()..].to_string(), - })) + Ok(RE_FORMULA_MAIN + .find(input) + .ok() + .flatten() + .map(|m| PatternHit { + token: MatchToken::S(m.as_str().to_string()), + remainder: input[m.end()..].to_string(), + })) } -pub fn match_pattern(data: &MhchemData, pattern_name: &str, input: &str) -> MhchemResult> { +pub fn match_pattern( + data: &MhchemData, + pattern_name: &str, + input: &str, +) -> MhchemResult> { match pattern_name { "(-)(9)^(-9)" => pattern_neg_pow(input), "(-)(9.,9)(e)(99)" => pattern_sci(input), "state of aggregation $" => pattern_state_of_agg(input), "amount" | "amount2" => pattern_amount(input), "formula$" => pattern_formula(input), - "^{(...)}" => find_observe_groups(input, "^{", Beg::Str(""), "", End::Str("}"), None, false), - "^($...$)" => find_observe_groups(input, "^", Beg::Str("$"), "$", End::Str(""), None, false), + "^{(...)}" => { + find_observe_groups(input, "^{", Beg::Str(""), "", End::Str("}"), None, false) + } + "^($...$)" => { + find_observe_groups(input, "^", Beg::Str("$"), "$", End::Str(""), None, false) + } "^\\x{}{}" => find_observe_groups( input, "^", @@ -326,8 +341,12 @@ pub fn match_pattern(data: &MhchemData, pattern_name: &str, input: &str) -> Mhch None, false, ), - "_{(...)}" => find_observe_groups(input, "_{", Beg::Str(""), "", End::Str("}"), None, false), - "_($...$)" => find_observe_groups(input, "_", Beg::Str("$"), "$", End::Str(""), None, false), + "_{(...)}" => { + find_observe_groups(input, "_{", Beg::Str(""), "", End::Str("}"), None, false) + } + "_($...$)" => { + find_observe_groups(input, "_", Beg::Str("$"), "$", End::Str(""), None, false) + } "_\\x{}{}" => find_observe_groups( input, "_", @@ -349,9 +368,19 @@ pub fn match_pattern(data: &MhchemData, pattern_name: &str, input: &str) -> Mhch "{...}" => find_observe_groups(input, "", Beg::Str("{"), "}", End::Str(""), None, false), "{(...)}" => find_observe_groups(input, "{", Beg::Str(""), "", End::Str("}"), None, false), "$...$" => find_observe_groups(input, "", Beg::Str("$"), "$", End::Str(""), None, false), - "${(...)}$" => find_observe_groups(input, "${", Beg::Str(""), "", End::Str("}$"), None, false), + "${(...)}$" => { + find_observe_groups(input, "${", Beg::Str(""), "", End::Str("}$"), None, false) + } "$(...)$" => find_observe_groups(input, "$", Beg::Str(""), "", End::Str("$"), None, false), - "\\bond{(...)}" => find_observe_groups(input, "\\bond{", Beg::Str(""), "", End::Str("}"), None, false), + "\\bond{(...)}" => find_observe_groups( + input, + "\\bond{", + Beg::Str(""), + "", + End::Str("}"), + None, + false, + ), "[(...)]" => find_observe_groups(input, "[", Beg::Str(""), "", End::Str("]"), None, false), "\\x{}{}" => find_observe_groups( input, @@ -434,10 +463,14 @@ pub fn match_pattern(data: &MhchemData, pattern_name: &str, input: &str) -> Mhch Some(("{", Beg::Str(""), "", End::Str("}"))), false, ), - "\\ce{(...)}" => find_observe_groups(input, "\\ce{", Beg::Str(""), "", End::Str("}"), None, false), + "\\ce{(...)}" => { + find_observe_groups(input, "\\ce{", Beg::Str(""), "", End::Str("}"), None, false) + } _ => { let Some(re) = data.regexes.map.get(pattern_name) else { - return Err(MhchemError::msg(format!("mhchem bug P: unknown pattern ({pattern_name})"))); + return Err(MhchemError::msg(format!( + "mhchem bug P: unknown pattern ({pattern_name})" + ))); }; Ok(regex_match_token(re, input).map(|(t, n)| PatternHit { token: t, @@ -455,9 +488,7 @@ mod fog_tests { fn x_double_brace_second_group_includes_nested_ce_close() { use crate::mhchem::data::data; let input = r"\underset{\mathrm{red}}{\ce{HgI2}}"; - let hit = match_pattern(data(), r"\x{}{}", input) - .unwrap() - .unwrap(); + let hit = match_pattern(data(), r"\x{}{}", input).unwrap().unwrap(); let MatchToken::S(s) = hit.token else { panic!("expected combined S"); }; diff --git a/crates/ratex-parser/src/mhchem/texify.rs b/crates/ratex-parser/src/mhchem/texify.rs index 9adb5223..e7e8d662 100644 --- a/crates/ratex-parser/src/mhchem/texify.rs +++ b/crates/ratex-parser/src/mhchem/texify.rs @@ -117,7 +117,11 @@ fn go2(buf: &Value) -> MhchemResult { format!("\\mathrm{{{p1}}}") } "text" => { - let mut p1 = buf.get("p1").and_then(|x| x.as_str()).unwrap_or("").to_string(); + let mut p1 = buf + .get("p1") + .and_then(|x| x.as_str()) + .unwrap_or("") + .to_string(); if p1.contains('^') || p1.contains('_') { p1 = p1.replace(' ', "~").replace('-', "\\text{-}"); format!("\\mathrm{{{p1}}}") @@ -270,8 +274,12 @@ fn get_bond(a: &str) -> Option<&'static str> { "~" => "{\\tripledash}", "~-" => "{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}", "~=" => "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}", - "~--" => "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}", - "-~-" => "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}", + "~--" => { + "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}" + } + "-~-" => { + "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}" + } "..." => "{{\\cdot}{\\cdot}{\\cdot}}", "...." => "{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}", "->" => "{\\rightarrow}", diff --git a/crates/ratex-parser/src/parse_node.rs b/crates/ratex-parser/src/parse_node.rs index 8b473356..8ee60744 100644 --- a/crates/ratex-parser/src/parse_node.rs +++ b/crates/ratex-parser/src/parse_node.rs @@ -942,13 +942,11 @@ mod tests { fn test_serialize_ordgroup() { let node = ParseNode::OrdGroup { mode: Mode::Math, - body: vec![ - ParseNode::MathOrd { - mode: Mode::Math, - text: "a".to_string(), - loc: None, - }, - ], + body: vec![ParseNode::MathOrd { + mode: Mode::Math, + text: "a".to_string(), + loc: None, + }], semisimple: None, loc: None, }; diff --git a/crates/ratex-parser/src/parser.rs b/crates/ratex-parser/src/parser.rs index 39b2710e..1c8c28d3 100644 --- a/crates/ratex-parser/src/parser.rs +++ b/crates/ratex-parser/src/parser.rs @@ -558,9 +558,9 @@ impl<'a> Parser<'a> { token: Option, break_on_token_text: Option<&str>, ) -> ParseResult { - let func = FUNCTIONS.get(name).ok_or_else(|| { - ParseError::msg(format!("No function handler for {}", name)) - })?; + let func = FUNCTIONS + .get(name) + .ok_or_else(|| ParseError::msg(format!("No function handler for {}", name)))?; let mut ctx = FunctionContext { func_name: name.to_string(), @@ -594,8 +594,11 @@ impl<'a> Parser<'a> { let is_optional = i < func_data.num_optional_args; let effective_type = if (func_data.primitive && arg_type.is_none()) - || (func_data.node_type == "sqrt" && i == 1 - && opt_args.first().is_some_and(|o: &Option| o.is_none())) + || (func_data.node_type == "sqrt" + && i == 1 + && opt_args + .first() + .is_some_and(|o: &Option| o.is_none())) { Some(ArgType::Primitive) } else { @@ -613,7 +616,9 @@ impl<'a> Parser<'a> { } else if let Some(a) = arg { args.push(a); } else { - return Err(ParseError::msg("Null argument, please report this as a bug")); + return Err(ParseError::msg( + "Null argument, please report this as a bug", + )); } } @@ -637,10 +642,7 @@ impl<'a> Parser<'a> { let group = self.parse_group(name, None)?; match group { Some(g) => Ok(Some(g)), - None => Err(ParseError::new( - format!("Expected group as {}", name), - None, - )), + None => Err(ParseError::new(format!("Expected group as {}", name), None)), } } Some(ArgType::Math) | Some(ArgType::Text) => { @@ -720,11 +722,13 @@ impl<'a> Parser<'a> { self.gullet.consume_spaces(); let res = if !optional && self.gullet.future().text != "{" { - Some(self.parse_regex_group( - ®ex_lite::Regex::new(r"^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$") - .unwrap(), - "size", - )?) + Some( + self.parse_regex_group( + ®ex_lite::Regex::new(r"^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$") + .unwrap(), + "size", + )?, + ) } else { self.parse_string_group("size", optional)? }; @@ -742,9 +746,9 @@ impl<'a> Parser<'a> { let size_re = regex_lite::Regex::new(r"([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})").unwrap(); - let m = size_re.captures(&text).ok_or_else(|| { - ParseError::new(format!("Invalid size: '{}'", text), Some(&res)) - })?; + let m = size_re + .captures(&text) + .ok_or_else(|| ParseError::new(format!("Invalid size: '{}'", text), Some(&res)))?; let sign = m.get(1).map_or("", |m| m.as_str()); let magnitude = m.get(2).map_or("", |m| m.as_str()); @@ -1096,15 +1100,17 @@ impl<'a> Parser<'a> { } } } else { - return self.try_parse_unicode_accent(&base_str, nucleus).map(|opt| { - opt.or_else(|| { - Some(ParseNode::TextOrd { - mode: Mode::Text, - text: base_str.clone(), - loc: loc.clone(), + return self + .try_parse_unicode_accent(&base_str, nucleus) + .map(|opt| { + opt.or_else(|| { + Some(ParseNode::TextOrd { + mode: Mode::Text, + text: base_str.clone(), + loc: loc.clone(), + }) }) - }) - }); + }); }; // Wrap in accent nodes from innermost to outermost @@ -1127,8 +1133,7 @@ impl<'a> Parser<'a> { pub fn subparse(&mut self, tokens: Vec) -> ParseResult> { let old_token = self.next_token.take(); - self.gullet - .push_token(Token::new("}", 0, 0)); + self.gullet.push_token(Token::new("}", 0, 0)); self.gullet.push_tokens(tokens); let parse = self.parse_expression(false, None)?; self.expect("}", true)?; @@ -1158,9 +1163,18 @@ fn is_latin_base_char(ch: char) -> bool { fn is_supported_combining_accent(ch: char) -> bool { matches!( ch, - '\u{0300}' | '\u{0301}' | '\u{0302}' | '\u{0303}' | '\u{0304}' - | '\u{0306}' | '\u{0307}' | '\u{0308}' | '\u{030A}' | '\u{030B}' | '\u{030C}' - | '\u{0327}' + '\u{0300}' + | '\u{0301}' + | '\u{0302}' + | '\u{0303}' + | '\u{0304}' + | '\u{0306}' + | '\u{0307}' + | '\u{0308}' + | '\u{030A}' + | '\u{030B}' + | '\u{030C}' + | '\u{0327}' ) } @@ -1202,8 +1216,20 @@ fn combining_to_accent_label(ch: char, mode: Mode) -> String { fn is_valid_unit(unit: &str) -> bool { matches!( unit, - "pt" | "mm" | "cm" | "in" | "bp" | "pc" | "dd" | "cc" | "nd" | "nc" | "sp" | "px" - | "ex" | "em" | "mu" + "pt" | "mm" + | "cm" + | "in" + | "bp" + | "pc" + | "dd" + | "cc" + | "nd" + | "nc" + | "sp" + | "px" + | "ex" + | "em" + | "mu" ) } diff --git a/crates/ratex-parser/src/tests.rs b/crates/ratex-parser/src/tests.rs index 00794234..919cc461 100644 --- a/crates/ratex-parser/src/tests.rs +++ b/crates/ratex-parser/src/tests.rs @@ -2,8 +2,8 @@ /// Tests are designed to validate against KaTeX's parsing behavior. #[cfg(test)] mod core_parsing { - use crate::parser::parse; use crate::parse_node::ParseNode; + use crate::parser::parse; // ── Basic characters ───────────────────────────────── @@ -177,15 +177,21 @@ mod core_parsing { #[cfg(test)] mod fractions_and_radicals { - use crate::parser::parse; use crate::parse_node::ParseNode; + use crate::parser::parse; #[test] fn simple_frac() { let ast = parse("\\frac{a}{b}").unwrap(); assert_eq!(ast.len(), 1); assert_eq!(ast[0].type_name(), "genfrac"); - if let ParseNode::GenFrac { has_bar_line, numer, denom, .. } = &ast[0] { + if let ParseNode::GenFrac { + has_bar_line, + numer, + denom, + .. + } = &ast[0] + { assert!(has_bar_line); // numer and denom are wrapped in OrdGroup if let ParseNode::OrdGroup { body, .. } = numer.as_ref() { @@ -231,7 +237,13 @@ mod fractions_and_radicals { assert_eq!(ast.len(), 1); // binom has delimiters and no bar line let check = |node: &ParseNode| { - if let ParseNode::GenFrac { has_bar_line, left_delim, right_delim, .. } = node { + if let ParseNode::GenFrac { + has_bar_line, + left_delim, + right_delim, + .. + } = node + { assert!(!has_bar_line); assert_eq!(left_delim.as_deref(), Some("(")); assert_eq!(right_delim.as_deref(), Some(")")); @@ -281,15 +293,21 @@ mod fractions_and_radicals { #[cfg(test)] mod operators { - use crate::parser::parse; use crate::parse_node::ParseNode; + use crate::parser::parse; #[test] fn sum_symbol() { let ast = parse("\\sum").unwrap(); assert_eq!(ast.len(), 1); assert_eq!(ast[0].type_name(), "op"); - if let ParseNode::Op { symbol, limits, name, .. } = &ast[0] { + if let ParseNode::Op { + symbol, + limits, + name, + .. + } = &ast[0] + { assert!(symbol); assert!(limits); assert_eq!(name.as_deref(), Some("\\sum")); @@ -310,7 +328,13 @@ mod operators { fn lim_text_op() { let ast = parse("\\lim").unwrap(); assert_eq!(ast.len(), 1); - if let ParseNode::Op { symbol, limits, name, .. } = &ast[0] { + if let ParseNode::Op { + symbol, + limits, + name, + .. + } = &ast[0] + { assert!(!symbol); assert!(limits); assert_eq!(name.as_deref(), Some("\\lim")); @@ -384,15 +408,18 @@ mod operators { #[cfg(test)] mod accents_and_fonts { - use crate::parser::parse; use crate::parse_node::ParseNode; + use crate::parser::parse; #[test] fn hat_accent() { let ast = parse("\\hat{x}").unwrap(); assert_eq!(ast.len(), 1); assert_eq!(ast[0].type_name(), "accent"); - if let ParseNode::Accent { label, is_stretchy, .. } = &ast[0] { + if let ParseNode::Accent { + label, is_stretchy, .. + } = &ast[0] + { assert_eq!(label, "\\hat"); assert_eq!(*is_stretchy, Some(false)); } @@ -402,7 +429,10 @@ mod accents_and_fonts { fn widehat_accent() { let ast = parse("\\widehat{ABC}").unwrap(); assert_eq!(ast.len(), 1); - if let ParseNode::Accent { label, is_stretchy, .. } = &ast[0] { + if let ParseNode::Accent { + label, is_stretchy, .. + } = &ast[0] + { assert_eq!(label, "\\widehat"); assert_eq!(*is_stretchy, Some(true)); } @@ -440,15 +470,18 @@ mod accents_and_fonts { #[cfg(test)] mod delimiters { - use crate::parser::parse; use crate::parse_node::ParseNode; + use crate::parser::parse; #[test] fn left_right_parens() { let ast = parse("\\left( x \\right)").unwrap(); assert_eq!(ast.len(), 1); assert_eq!(ast[0].type_name(), "leftright"); - if let ParseNode::LeftRight { left, right, body, .. } = &ast[0] { + if let ParseNode::LeftRight { + left, right, body, .. + } = &ast[0] + { assert_eq!(left, "("); assert_eq!(right, ")"); assert!(!body.is_empty()); @@ -472,8 +505,8 @@ mod delimiters { #[cfg(test)] mod colors_and_sizing { - use crate::parser::parse; use crate::parse_node::ParseNode; + use crate::parser::parse; #[test] fn textcolor() { @@ -505,8 +538,14 @@ mod colors_and_sizing { let ast = parse("\\htmlStyle{color: blue; font-size: 20px;}{x^2}").unwrap(); assert_eq!(ast.len(), 1); assert_eq!(ast[0].type_name(), "html"); - if let ParseNode::Html { attributes, body, .. } = &ast[0] { - assert_eq!(attributes.get("style").unwrap(), "color: blue; font-size: 20px;"); + if let ParseNode::Html { + attributes, body, .. + } = &ast[0] + { + assert_eq!( + attributes.get("style").unwrap(), + "color: blue; font-size: 20px;" + ); assert!(!body.is_empty()); } else { panic!("Expected html node"); @@ -657,8 +696,7 @@ mod environments { #[test] fn pmatrix_wraps_in_leftright() { - let ast = - parse("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}").unwrap(); + let ast = parse("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}").unwrap(); assert_eq!(ast.len(), 1); assert_eq!(ast[0].type_name(), "leftright"); if let ParseNode::LeftRight { @@ -676,13 +714,9 @@ mod environments { #[test] fn bmatrix_wraps_in_leftright() { - let ast = - parse("\\begin{bmatrix} 1 & 2 \\\\ 3 & 4 \\end{bmatrix}").unwrap(); + let ast = parse("\\begin{bmatrix} 1 & 2 \\\\ 3 & 4 \\end{bmatrix}").unwrap(); assert_eq!(ast.len(), 1); - if let ParseNode::LeftRight { - left, right, .. - } = &ast[0] - { + if let ParseNode::LeftRight { left, right, .. } = &ast[0] { assert_eq!(left, "["); assert_eq!(right, "]"); } else { @@ -692,12 +726,8 @@ mod environments { #[test] fn vmatrix_wraps_in_leftright() { - let ast = - parse("\\begin{vmatrix} a & b \\\\ c & d \\end{vmatrix}").unwrap(); - if let ParseNode::LeftRight { - left, right, .. - } = &ast[0] - { + let ast = parse("\\begin{vmatrix} a & b \\\\ c & d \\end{vmatrix}").unwrap(); + if let ParseNode::LeftRight { left, right, .. } = &ast[0] { assert_eq!(left, "|"); assert_eq!(right, "|"); } else { @@ -707,12 +737,8 @@ mod environments { #[test] fn big_bmatrix_wraps_in_leftright() { - let ast = - parse("\\begin{Bmatrix} a \\\\ b \\end{Bmatrix}").unwrap(); - if let ParseNode::LeftRight { - left, right, .. - } = &ast[0] - { + let ast = parse("\\begin{Bmatrix} a \\\\ b \\end{Bmatrix}").unwrap(); + if let ParseNode::LeftRight { left, right, .. } = &ast[0] { assert_eq!(left, "\\{"); assert_eq!(right, "\\}"); } else { @@ -722,10 +748,9 @@ mod environments { #[test] fn cases_environment() { - let ast = parse( - "\\begin{cases} x & \\text{if } x > 0 \\\\ -x & \\text{otherwise} \\end{cases}", - ) - .unwrap(); + let ast = + parse("\\begin{cases} x & \\text{if } x > 0 \\\\ -x & \\text{otherwise} \\end{cases}") + .unwrap(); assert_eq!(ast.len(), 1); assert_eq!(ast[0].type_name(), "leftright"); if let ParseNode::LeftRight { @@ -761,10 +786,7 @@ mod environments { { assert_eq!(body.len(), 2); assert!(add_jot.unwrap_or(false)); - assert_eq!( - col_separation_type.as_deref(), - Some("align") - ); + assert_eq!(col_separation_type.as_deref(), Some("align")); } else { panic!("Expected Array node for aligned"); } @@ -780,8 +802,7 @@ mod environments { #[test] fn align_with_tag_strips_tag_and_sets_array_tags() { use crate::parse_node::ArrayTag; - let ast = - parse("\\begin{aligned} x &= 1 \\tag{1} \\\\ y &= 2 \\end{aligned}").unwrap(); + let ast = parse("\\begin{aligned} x &= 1 \\tag{1} \\\\ y &= 2 \\end{aligned}").unwrap(); if let ParseNode::Array { body, tags, .. } = &ast[0] { assert_eq!(body.len(), 2); let row0_last = &body[0][body[0].len() - 1]; @@ -816,10 +837,8 @@ mod environments { #[test] fn matrix_3x3() { - let ast = parse( - "\\begin{matrix} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\\\ 7 & 8 & 9 \\end{matrix}", - ) - .unwrap(); + let ast = + parse("\\begin{matrix} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\\\ 7 & 8 & 9 \\end{matrix}").unwrap(); if let ParseNode::Array { body, cols, .. } = &ast[0] { assert_eq!(body.len(), 3); for row in body { @@ -894,8 +913,7 @@ mod environments { #[test] fn pmatrix_json_structure() { - let ast = - parse("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}").unwrap(); + let ast = parse("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}").unwrap(); let json = serde_json::to_value(&ast).unwrap(); let node = &json[0]; assert_eq!(node["type"], "leftright"); @@ -917,29 +935,21 @@ mod environments { #[test] fn nested_frac_in_matrix() { - let ast = parse( - "\\begin{pmatrix} \\frac{1}{2} & 0 \\\\ 0 & \\frac{3}{4} \\end{pmatrix}", - ) - .unwrap(); + let ast = parse("\\begin{pmatrix} \\frac{1}{2} & 0 \\\\ 0 & \\frac{3}{4} \\end{pmatrix}") + .unwrap(); assert_eq!(ast[0].type_name(), "leftright"); } #[test] fn matrix_with_expressions() { - let ast = parse( - "\\begin{bmatrix} a+b & c^2 \\\\ \\sqrt{d} & e_i \\end{bmatrix}", - ) - .unwrap(); + let ast = parse("\\begin{bmatrix} a+b & c^2 \\\\ \\sqrt{d} & e_i \\end{bmatrix}").unwrap(); assert_eq!(ast[0].type_name(), "leftright"); } #[test] fn rcases_environment() { let ast = parse("\\begin{rcases} a \\\\ b \\end{rcases}").unwrap(); - if let ParseNode::LeftRight { - left, right, .. - } = &ast[0] - { + if let ParseNode::LeftRight { left, right, .. } = &ast[0] { assert_eq!(left, "."); assert_eq!(right, "\\}"); } else { @@ -964,10 +974,16 @@ mod environments { #[test] fn prooftree_binary_dashed_line() { - let ast = parse("\\begin{prooftree}\\AxiomC{P}\\AxiomC{Q}\\dashedLine\\BinaryInfC{R}\\end{prooftree}").unwrap(); + let ast = parse( + "\\begin{prooftree}\\AxiomC{P}\\AxiomC{Q}\\dashedLine\\BinaryInfC{R}\\end{prooftree}", + ) + .unwrap(); if let ParseNode::ProofTree { tree, .. } = &ast[0] { assert_eq!(tree.premises.len(), 2); - assert!(matches!(tree.line_style, crate::parse_node::ProofLineStyle::Dashed)); + assert!(matches!( + tree.line_style, + crate::parse_node::ProofLineStyle::Dashed + )); } else { panic!("Expected ProofTree node"); } @@ -982,8 +998,7 @@ mod environments { #[test] fn prooftree_fcenter_renders_visible_symbol() { let ast = - parse("\\begin{prooftree}\\AxiomC{A \\fCenter B}\\UIC{C}\\end{prooftree}") - .unwrap(); + parse("\\begin{prooftree}\\AxiomC{A \\fCenter B}\\UIC{C}\\end{prooftree}").unwrap(); if let ParseNode::ProofTree { tree, .. } = &ast[0] { // The axiom's conclusion contains A \fCenter B, stored in premises[0].conclusion let axiom = &tree.premises[0]; @@ -1005,8 +1020,7 @@ mod environments { #[test] fn prooftree_root_at_top_flag() { let ast = - parse("\\begin{prooftree}\\AxiomC{P}\\rootAtTop\\UIC{Q}\\end{prooftree}") - .unwrap(); + parse("\\begin{prooftree}\\AxiomC{P}\\rootAtTop\\UIC{Q}\\end{prooftree}").unwrap(); if let ParseNode::ProofTree { tree, .. } = &ast[0] { assert!(tree.root_at_top, "\\rootAtTop should set root_at_top flag"); } else { @@ -1016,8 +1030,7 @@ mod environments { #[test] fn prooftree_root_at_bottom_is_default() { - let ast = - parse("\\begin{prooftree}\\AxiomC{P}\\UIC{Q}\\end{prooftree}").unwrap(); + let ast = parse("\\begin{prooftree}\\AxiomC{P}\\UIC{Q}\\end{prooftree}").unwrap(); if let ParseNode::ProofTree { tree, .. } = &ast[0] { assert!(!tree.root_at_top, "root_at_top should default to false"); } else { @@ -1027,19 +1040,17 @@ mod environments { #[test] fn prooftree_orphan_label_errors() { - let result = - parse("\\begin{prooftree}\\AxiomC{P}\\LeftLabel{L}\\end{prooftree}"); - assert!(result.is_err(), "orphan \\LeftLabel should produce an error"); + let result = parse("\\begin{prooftree}\\AxiomC{P}\\LeftLabel{L}\\end{prooftree}"); + assert!( + result.is_err(), + "orphan \\LeftLabel should produce an error" + ); } #[test] fn vmatrix_double_wraps() { - let ast = - parse("\\begin{Vmatrix} a & b \\\\ c & d \\end{Vmatrix}").unwrap(); - if let ParseNode::LeftRight { - left, right, .. - } = &ast[0] - { + let ast = parse("\\begin{Vmatrix} a & b \\\\ c & d \\end{Vmatrix}").unwrap(); + if let ParseNode::LeftRight { left, right, .. } = &ast[0] { assert_eq!(left, "\\Vert"); assert_eq!(right, "\\Vert"); } else { diff --git a/crates/ratex-pdf/src/bin/render_pdf.rs b/crates/ratex-pdf/src/bin/render_pdf.rs index ce571126..d41566d1 100644 --- a/crates/ratex-pdf/src/bin/render_pdf.rs +++ b/crates/ratex-pdf/src/bin/render_pdf.rs @@ -12,7 +12,10 @@ use ratex_types::math_style::MathStyle; fn main() { let args: Vec = std::env::args().collect(); if args.iter().any(|a| a == "-h" || a == "--help") { - print!("{}", help_text(args.first().map(String::as_str).unwrap_or("render-pdf"))); + print!( + "{}", + help_text(args.first().map(String::as_str).unwrap_or("render-pdf")) + ); return; } @@ -71,9 +74,11 @@ fn main() { let mut idx = 0; let mut ok_count = 0; let reader: Box = match input_file { - Some(path) => Box::new(io::BufReader::new( - File::open(&path).unwrap_or_else(|e| panic!("Failed to open input file '{}': {}", path, e)), - )), + Some(path) => { + Box::new(io::BufReader::new(File::open(&path).unwrap_or_else(|e| { + panic!("Failed to open input file '{}': {}", path, e) + }))) + } None => Box::new(io::BufReader::new(io::stdin())), }; for line in reader.lines() { @@ -116,12 +121,7 @@ fn pdf_formula( fn default_font_dir() -> String { const MARKER: &str = "KaTeX_Main-Regular.ttf"; - let candidates = [ - "fonts", - "../fonts", - "../../fonts", - "../../../fonts", - ]; + let candidates = ["fonts", "../fonts", "../../fonts", "../../../fonts"]; for c in &candidates { let p = std::path::Path::new(c); if p.join(MARKER).is_file() { diff --git a/crates/ratex-pdf/src/fonts.rs b/crates/ratex-pdf/src/fonts.rs index c4f9346f..33a5b696 100644 --- a/crates/ratex-pdf/src/fonts.rs +++ b/crates/ratex-pdf/src/fonts.rs @@ -48,11 +48,13 @@ fn variable_weight(font: &SfFontRef) -> Option { let axes = font.axes(); let wght_axis = axes.get_by_tag(Tag::new(b"wght"))?; - Some(if wght_axis.min_value() <= 400.0 && 400.0 <= wght_axis.max_value() { - 400.0 - } else { - wght_axis.default_value() - }) + Some( + if wght_axis.min_value() <= 400.0 && 400.0 <= wght_axis.max_value() { + 400.0 + } else { + wght_axis.default_value() + }, + ) } /// If the font has a `wght` variation axis, return a `Location` targeting the selected weight. @@ -452,14 +454,14 @@ pub(crate) fn embed_fonts( // Subset the font. let index = skrifa_collection_index(usage.font_id); - let sf = SfFontRef::from_index(raw, index) - .map_err(|e| format!("skrifa error: {e}"))?; + let sf = SfFontRef::from_index(raw, index).map_err(|e| format!("skrifa error: {e}"))?; let subsetted = if let Some(target_weight) = variable_weight(&sf) { let coords = [(subsetter::Tag::new(b"wght"), target_weight)]; subsetter::subset_with_variations(raw, index, &coords, &remapper) } else { subsetter::subset(raw, index, &remapper) - }.map_err(|e| format!("Subset error for {:?}: {e}", usage.font_id))?; + } + .map_err(|e| format!("Subset error for {:?}: {e}", usage.font_id))?; // Compress the subset. let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&subsetted, 6); diff --git a/crates/ratex-render/src/main.rs b/crates/ratex-render/src/main.rs index 301386f2..b9fae7ae 100644 --- a/crates/ratex-render/src/main.rs +++ b/crates/ratex-render/src/main.rs @@ -2,8 +2,8 @@ use std::fs::File; use std::io::{self, BufRead}; use std::path::PathBuf; -use ratex_layout::{layout, LayoutOptions}; use ratex_layout::to_display_list; +use ratex_layout::{layout, LayoutOptions}; use ratex_parser::parser::parse; use ratex_render::{render_to_png, RenderOptions}; use ratex_types::color::Color; @@ -12,7 +12,10 @@ use ratex_types::math_style::MathStyle; fn main() { let args: Vec = std::env::args().collect(); if args.iter().any(|a| a == "-h" || a == "--help") { - print!("{}", help_text(args.first().map(String::as_str).unwrap_or("render"))); + print!( + "{}", + help_text(args.first().map(String::as_str).unwrap_or("render")) + ); return; } @@ -85,14 +88,20 @@ fn main() { }; let inline = args.contains(&"--inline".to_string()); - let style = if inline { MathStyle::Text } else { MathStyle::Display }; + let style = if inline { + MathStyle::Text + } else { + MathStyle::Display + }; let layout_opts = LayoutOptions::default().with_style(style).with_color(color); let mut idx = 0; let reader: Box = match input_file { - Some(path) => Box::new(io::BufReader::new( - File::open(&path).unwrap_or_else(|e| panic!("Failed to open input file '{}': {}", path, e)), - )), + Some(path) => { + Box::new(io::BufReader::new(File::open(&path).unwrap_or_else(|e| { + panic!("Failed to open input file '{}': {}", path, e) + }))) + } None => Box::new(io::BufReader::new(io::stdin())), }; for line in reader.lines() { diff --git a/crates/ratex-render/tests/bench_render.rs b/crates/ratex-render/tests/bench_render.rs index b9038af1..d94dd5ed 100644 --- a/crates/ratex-render/tests/bench_render.rs +++ b/crates/ratex-render/tests/bench_render.rs @@ -82,12 +82,7 @@ fn bench_formula( glyph_count = dl .items .iter() - .filter(|i| { - matches!( - i, - ratex_types::display_item::DisplayItem::GlyphPath { .. } - ) - }) + .filter(|i| matches!(i, ratex_types::display_item::DisplayItem::GlyphPath { .. })) .count(); let t1 = Instant::now(); @@ -133,25 +128,51 @@ fn build_formulas() -> Vec<(&'static str, &'static str, &'static str)> { ("a^2-b^2=(a+b)(a-b)", r"a^2 - b^2 = (a+b)(a-b)"), ("\\sqrt{a^2+b^2}", r"\sqrt{a^2 + b^2}"), ("\\frac{dy}{dx}", r"\frac{dy}{dx}"), - ("x_{1,2} formula", r"x_{1,2} = \frac{-b \pm \sqrt{b^2-4ac}}{2a}"), + ( + "x_{1,2} formula", + r"x_{1,2} = \frac{-b \pm \sqrt{b^2-4ac}}{2a}", + ), ("F=ma", r"F = ma"), - ("a\\cdot b = |a||b|\\cos\\theta", r"a \cdot b = |a||b|\cos\theta"), - ("\\lim_{x\\to 0}\\frac{\\sin x}{x}", r"\lim_{x\to 0}\frac{\sin x}{x}"), + ( + "a\\cdot b = |a||b|\\cos\\theta", + r"a \cdot b = |a||b|\cos\theta", + ), + ( + "\\lim_{x\\to 0}\\frac{\\sin x}{x}", + r"\lim_{x\to 0}\frac{\sin x}{x}", + ), ("\\int_a^b f(x)dx", r"\int_a^b f(x)\,dx"), ("\\sum_{i=1}^{n} i", r"\sum_{i=1}^{n} i = \frac{n(n+1)}{2}"), ("\\prod_{i=1}^{n} a_i", r"\prod_{i=1}^{n} a_i"), ("\\binom{n}{k}", r"\binom{n}{k} = \frac{n!}{k!(n-k)!}"), - ("\\vec{F} = q(\\vec{E} + \\vec{v}\\times\\vec{B})", r"\vec{F} = q(\vec{E} + \vec{v}\times\vec{B})"), - ("\\nabla\\cdot\\vec{E}=\\rho/\\varepsilon_0", r"\nabla\cdot\vec{E} = \frac{\rho}{\varepsilon_0}"), + ( + "\\vec{F} = q(\\vec{E} + \\vec{v}\\times\\vec{B})", + r"\vec{F} = q(\vec{E} + \vec{v}\times\vec{B})", + ), + ( + "\\nabla\\cdot\\vec{E}=\\rho/\\varepsilon_0", + r"\nabla\cdot\vec{E} = \frac{\rho}{\varepsilon_0}", + ), ("\\infty+1=\\infty", r"\infty + 1 = \infty"), ("\\partial f/\\partial x", r"\frac{\partial f}{\partial x}"), - ("\\oint_C \\vec{F}\\cdot d\\vec{r}", r"\oint_C \vec{F}\cdot d\vec{r}"), - ("\\int_{-\\infty}^{\\infty}e^{-x^2}dx", r"\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}"), + ( + "\\oint_C \\vec{F}\\cdot d\\vec{r}", + r"\oint_C \vec{F}\cdot d\vec{r}", + ), + ( + "\\int_{-\\infty}^{\\infty}e^{-x^2}dx", + r"\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}", + ), ("\\hat{H}\\psi=E\\psi", r"\hat{H}\psi = E\psi"), ("A\\subseteq B", r"A \subseteq B \implies P(A)\leq P(B)"), - ("\\lfloor x\\rfloor\\le x<\\lceil x\\rceil", r"\lfloor x\rfloor \leq x < \lceil x\rceil"), + ( + "\\lfloor x\\rfloor\\le x<\\lceil x\\rceil", + r"\lfloor x\rfloor \leq x < \lceil x\rceil", + ), ]; - for &(l, e) in simple { v.push(("math", l, e)); } + for &(l, e) in simple { + v.push(("math", l, e)); + } // ── Category 2: Complex math (20) ── let complex: &[(&str, &str)] = &[ @@ -196,66 +217,177 @@ fn build_formulas() -> Vec<(&'static str, &'static str, &'static str)> { ("\\int_0^{2\\pi}\\sin^2 x\\,dx=\\pi", r"\int_0^{2\pi} \sin^2 x\,dx = \pi"), ]; - for &(l, e) in complex { v.push(("complex", l, e)); } + for &(l, e) in complex { + v.push(("complex", l, e)); + } // ── Category 3: Matrix / array (15) ── let matrix: &[(&str, &str)] = &[ - ("pmatrix 2x2", r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}"), - ("pmatrix 3x3", r"\begin{pmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{pmatrix}"), - ("bmatrix 2x2", r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}"), - ("vmatrix 2x2", r"\begin{vmatrix} a & b \\ c & d \end{vmatrix}"), - ("cases defn", r"|x| = \begin{cases} x & x\geq 0 \\ -x & x<0 \end{cases}"), - ("aligned 3eq", r"\begin{aligned} a &= b + c \\ d &= e + f + g \\ h &= i - j \end{aligned}"), - ("gathered 2eq", r"\begin{gathered} x = a + b \\ y = c - d \end{gathered}"), - ("Bmatrix 2x2", r"\begin{Bmatrix} x & y \\ z & w \end{Bmatrix}"), - ("array 3col", r"\begin{array}{ccc} 1 & 2 & 3 \\ 4 & 5 & 6 \end{array}"), - ("array w/lines",r"\begin{array}{|c|c|} \hline a & b \\ \hline c & d \\ \hline \end{array}"), - ("smallmatrix 2x2", r"\begin{smallmatrix} 1 & 2 \\ 3 & 4 \end{smallmatrix}"), + ( + "pmatrix 2x2", + r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", + ), + ( + "pmatrix 3x3", + r"\begin{pmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{pmatrix}", + ), + ( + "bmatrix 2x2", + r"\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}", + ), + ( + "vmatrix 2x2", + r"\begin{vmatrix} a & b \\ c & d \end{vmatrix}", + ), + ( + "cases defn", + r"|x| = \begin{cases} x & x\geq 0 \\ -x & x<0 \end{cases}", + ), + ( + "aligned 3eq", + r"\begin{aligned} a &= b + c \\ d &= e + f + g \\ h &= i - j \end{aligned}", + ), + ( + "gathered 2eq", + r"\begin{gathered} x = a + b \\ y = c - d \end{gathered}", + ), + ( + "Bmatrix 2x2", + r"\begin{Bmatrix} x & y \\ z & w \end{Bmatrix}", + ), + ( + "array 3col", + r"\begin{array}{ccc} 1 & 2 & 3 \\ 4 & 5 & 6 \end{array}", + ), + ( + "array w/lines", + r"\begin{array}{|c|c|} \hline a & b \\ \hline c & d \\ \hline \end{array}", + ), + ( + "smallmatrix 2x2", + r"\begin{smallmatrix} 1 & 2 \\ 3 & 4 \end{smallmatrix}", + ), ("\\substack sum", r"\sum_{\substack{i=1\\ j=2}}^{n} a_{ij}"), - ("\\binom big", r"\binom{n+1}{k} = \binom{n}{k} + \binom{n}{k-1}"), - ("\\cfrac nested", r"\cfrac{1}{\sqrt{2} + \cfrac{1}{\sqrt{2} + \cfrac{1}{\sqrt{2}}}}"), - ("\\overbrace", r"\overbrace{a + b + c + d}^{n\text{ terms}}"), + ( + "\\binom big", + r"\binom{n+1}{k} = \binom{n}{k} + \binom{n}{k-1}", + ), + ( + "\\cfrac nested", + r"\cfrac{1}{\sqrt{2} + \cfrac{1}{\sqrt{2} + \cfrac{1}{\sqrt{2}}}}", + ), + ("\\overbrace", r"\overbrace{a + b + c + d}^{n\text{ terms}}"), ]; - for &(l, e) in matrix { v.push(("matrix", l, e)); } + for &(l, e) in matrix { + v.push(("matrix", l, e)); + } // ── Category 4: CJK text + math (15) ── let cjk: &[(&str, &str)] = &[ - ("你好世界", r"\text{你好世界}\quad x^2 + y^2 = z^2"), - ("数学公式", r"\text{数学公式}\quad \frac{a}{b} + \frac{c}{d}"), - ("微积分", r"\text{微积分}\quad \int_0^\infty f(x)dx"), - ("定义域值域", r"\text{定义域}\; D_f = \{x \in \mathbb{R} \mid x \geq 0\}"), - ("三角函数", r"\text{三角函数}\quad \sin\theta + \cos\theta = 1\text{(单位圆)}"), - ("方程求解", r"\text{方程}\; ax^2 + bx + c = 0 \;\text{的解为}\; x = \frac{-b\pm\sqrt{b^2-4ac}}{2a}"), - ("概率论", r"\text{概率论}\quad P(A \cup B) = P(A) + P(B) - P(A \cap B)"), - ("线性代数", r"\text{线性代数}\quad \begin{pmatrix} a & b \\ c & d \end{pmatrix}\text{矩阵乘法}"), - ("物理学", r"\text{物理学}\quad E_k = \frac{1}{2}mv^2\quad\text{动能公式}"), - ("统计学", r"\text{统计学}\quad \bar{x} = \frac{1}{n}\sum_{i=1}^{n} x_i\quad\text{样本均值}"), - ("数论基础", r"\text{数论基础}\quad a \equiv b \pmod{n}\quad\text{同余关系}"), - ("集合论", r"\text{集合论}\quad A \subset B \implies |A| \leq |B|"), - ("逻辑推理", r"\text{逻辑推理}\quad p \implies q \;\text{等价于}\; \neg p \lor q"), - ("拓扑学", r"\text{拓扑学}\quad \forall x \in X,\; \exists U \subset X \text{ 是开集}"), - ("几何学", r"\text{几何学}\quad \angle ABC = 180^\circ - \angle BAC - \angle BCA"), + ("你好世界", r"\text{你好世界}\quad x^2 + y^2 = z^2"), + ( + "数学公式", + r"\text{数学公式}\quad \frac{a}{b} + \frac{c}{d}", + ), + ("微积分", r"\text{微积分}\quad \int_0^\infty f(x)dx"), + ( + "定义域值域", + r"\text{定义域}\; D_f = \{x \in \mathbb{R} \mid x \geq 0\}", + ), + ( + "三角函数", + r"\text{三角函数}\quad \sin\theta + \cos\theta = 1\text{(单位圆)}", + ), + ( + "方程求解", + r"\text{方程}\; ax^2 + bx + c = 0 \;\text{的解为}\; x = \frac{-b\pm\sqrt{b^2-4ac}}{2a}", + ), + ( + "概率论", + r"\text{概率论}\quad P(A \cup B) = P(A) + P(B) - P(A \cap B)", + ), + ( + "线性代数", + r"\text{线性代数}\quad \begin{pmatrix} a & b \\ c & d \end{pmatrix}\text{矩阵乘法}", + ), + ( + "物理学", + r"\text{物理学}\quad E_k = \frac{1}{2}mv^2\quad\text{动能公式}", + ), + ( + "统计学", + r"\text{统计学}\quad \bar{x} = \frac{1}{n}\sum_{i=1}^{n} x_i\quad\text{样本均值}", + ), + ( + "数论基础", + r"\text{数论基础}\quad a \equiv b \pmod{n}\quad\text{同余关系}", + ), + ( + "集合论", + r"\text{集合论}\quad A \subset B \implies |A| \leq |B|", + ), + ( + "逻辑推理", + r"\text{逻辑推理}\quad p \implies q \;\text{等价于}\; \neg p \lor q", + ), + ( + "拓扑学", + r"\text{拓扑学}\quad \forall x \in X,\; \exists U \subset X \text{ 是开集}", + ), + ( + "几何学", + r"\text{几何学}\quad \angle ABC = 180^\circ - \angle BAC - \angle BCA", + ), ]; - for &(l, e) in cjk { v.push(("cjk", l, e)); } + for &(l, e) in cjk { + v.push(("cjk", l, e)); + } // ── Category 5: Emoji + math (10) ── let emoji: &[(&str, &str)] = &[ - ("😊+math", r"\text{😊}\quad x^2 + y^2 = z^2 \quad \text{✅}"), + ("😊+math", r"\text{😊}\quad x^2 + y^2 = z^2 \quad \text{✅}"), ("⭐+formula", r"\text{⭐} \quad E = mc^2 \quad \text{🔥}"), - ("🎉+integral", r"\text{🎉}\quad \int_0^1 f(x)dx \quad \text{💯}"), + ( + "🎉+integral", + r"\text{🎉}\quad \int_0^1 f(x)dx \quad \text{💯}", + ), ("❤️+equation", r"\text{❤️}\quad a + b = c \quad \text{👍}"), - ("🚀+frac", r"\text{🚀}\quad \frac{a}{b} + \frac{c}{d} \quad \text{🎯}"), - ("💡+sum", r"\text{💡}\quad \sum_{i=1}^n i = \frac{n(n+1)}{2} \quad \text{📊}"), - ("🎵+matrix", r"\text{🎵}\quad \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \quad \text{🎶}"), - ("🏆+limit", r"\text{🏆}\quad \lim_{n\to\infty} \left(1+\frac{1}{n}\right)^n \quad \text{🥇}"), - ("📐+sqrt", r"\text{📐}\quad \sqrt{a^2 + b^2} = c \quad \text{📏}"), - ("🔬+chem", r"\text{🔬}\quad \ce{H2O + CO2 -> H2CO3} \quad \text{🧪}"), + ( + "🚀+frac", + r"\text{🚀}\quad \frac{a}{b} + \frac{c}{d} \quad \text{🎯}", + ), + ( + "💡+sum", + r"\text{💡}\quad \sum_{i=1}^n i = \frac{n(n+1)}{2} \quad \text{📊}", + ), + ( + "🎵+matrix", + r"\text{🎵}\quad \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \quad \text{🎶}", + ), + ( + "🏆+limit", + r"\text{🏆}\quad \lim_{n\to\infty} \left(1+\frac{1}{n}\right)^n \quad \text{🥇}", + ), + ( + "📐+sqrt", + r"\text{📐}\quad \sqrt{a^2 + b^2} = c \quad \text{📏}", + ), + ( + "🔬+chem", + r"\text{🔬}\quad \ce{H2O + CO2 -> H2CO3} \quad \text{🧪}", + ), ]; - for &(l, e) in emoji { v.push(("emoji", l, e)); } + for &(l, e) in emoji { + v.push(("emoji", l, e)); + } // Remove the last one (chem inside emoji category) to keep emoji pure. // We'll add a replacement pure-emoji formula. v.pop(); - v.push(("emoji", "🎨+binom", r"\text{🎨}\quad \binom{n}{k} = \frac{n!}{k!(n-k)!} \quad \text{🎭}")); + v.push(( + "emoji", + "🎨+binom", + r"\text{🎨}\quad \binom{n}{k} = \frac{n!}{k!(n-k)!} \quad \text{🎭}", + )); // ── Category 6: Chemistry (mhchem) + misc (15) ── let misc: &[(&str, &str)] = &[ @@ -266,16 +398,33 @@ fn build_formulas() -> Vec<(&'static str, &'static str, &'static str)> { ("CH4 combustion", r"\ce{CH4 + 2O2 -> CO2 + 2H2O}"), ("Fe2O3 reduction", r"\ce{Fe2O3 + 3CO -> 2Fe + 3CO2}"), ("CaCO3 decomp", r"\ce{CaCO3 ->[\Delta] CaO + CO2 ^}"), - ("NaCl dissolve", r"\ce{NaCl_{(s)} ->[\ce{H2O}] Na+_{(aq)} + Cl-_{(aq)}}"), - ("KMnO4 titration", r"\ce{5Fe^{2+} + MnO4- + 8H+ -> 5Fe^{3+} + Mn^{2+} + 4H2O}"), + ( + "NaCl dissolve", + r"\ce{NaCl_{(s)} ->[\ce{H2O}] Na+_{(aq)} + Cl-_{(aq)}}", + ), + ( + "KMnO4 titration", + r"\ce{5Fe^{2+} + MnO4- + 8H+ -> 5Fe^{3+} + Mn^{2+} + 4H2O}", + ), ("HCl + NaOH", r"\ce{HCl + NaOH -> NaCl + H2O}"), ("AgCl precipitate", r"\ce{Ag+ + Cl- -> AgCl v}"), - ("buffer eq", r"\text{pH} = \text{p}K_a + \log\frac{[\ce{A-}]}{[\ce{HA}]}"), - ("SO2 oxidation", r"\ce{2SO2 + O2 <=>[V2O5][450^\circ C] 2SO3}"), + ( + "buffer eq", + r"\text{pH} = \text{p}K_a + \log\frac{[\ce{A-}]}{[\ce{HA}]}", + ), + ( + "SO2 oxidation", + r"\ce{2SO2 + O2 <=>[V2O5][450^\circ C] 2SO3}", + ), (r"Zn + CuSO4", r"\ce{Zn + CuSO4 -> ZnSO4 + Cu v}"), - ("ethanol+H2SO4", r"\ce{C2H5OH ->[\ce{H2SO4}][170^\circ C] C2H4 ^ + H2O}"), + ( + "ethanol+H2SO4", + r"\ce{C2H5OH ->[\ce{H2SO4}][170^\circ C] C2H4 ^ + H2O}", + ), ]; - for &(l, e) in misc { v.push(("chem", l, e)); } + for &(l, e) in misc { + v.push(("chem", l, e)); + } assert_eq!(v.len(), 100, "must have exactly 100 formulas"); v @@ -299,9 +448,13 @@ fn bench_render_100() { let formulas = build_formulas(); // ── Header ── - println!("\n╔══════════════════════════════════════════════════════════════════════════════════╗"); + println!( + "\n╔══════════════════════════════════════════════════════════════════════════════════╗" + ); println!("║ RaTeX 100-Formula Render Benchmark (release, warmup={WARMUP}, iters={ITERS}) ║"); - println!("╠══════════════════════════════════════════════════════════════════════════════════╣"); + println!( + "╠══════════════════════════════════════════════════════════════════════════════════╣" + ); let mut results: Vec = Vec::with_capacity(100); let start = Instant::now(); @@ -316,13 +469,17 @@ fn bench_render_100() { // ── Per-category summary ── println!("║ Per-category averages ║"); println!("╠══════════════════════════════╦══════╦══════════╦════════╦══════════════╦════════╦════════╣"); - println!("║ {:<28} ║ {:>4} ║ {:>8} ║ {:>6} ║ {:>12} ║ {:>6} ║ {:>6} ║", - "Category", "Cnt", "P+L(μs)", "PNG(μs)", "SVG-sa(μs)", "SVG(μs)", "PDF(μs)"); + println!( + "║ {:<28} ║ {:>4} ║ {:>8} ║ {:>6} ║ {:>12} ║ {:>6} ║ {:>6} ║", + "Category", "Cnt", "P+L(μs)", "PNG(μs)", "SVG-sa(μs)", "SVG(μs)", "PDF(μs)" + ); println!("╠══════════════════════════════╬══════╬══════════╬════════╬══════════════╬════════╬════════╣"); for cat in &["math", "complex", "matrix", "cjk", "emoji", "chem"] { let group: Vec<_> = results.iter().filter(|r| r.category == *cat).collect(); - if group.is_empty() { continue; } + if group.is_empty() { + continue; + } let n = group.len() as u128; let pl = group.iter().map(|r| r.parse_layout_us).sum::() / n; let png = group.iter().map(|r| r.png_us).sum::() / n; @@ -330,8 +487,16 @@ fn bench_render_100() { let svg_sa = group.iter().map(|r| r.svg_standalone_us).sum::() / n; let pdf = group.iter().map(|r| r.pdf_us).sum::() / n; let _total_glyphs: usize = group.iter().map(|r| r.glyph_count).sum(); - println!("║ {:<28} ║ {:>4} ║ {:>7} ║ {:>5} ║ {:>10} ║ {:>5} ║ {:>5} ║", - cat, group.len(), pl, png, svg_sa, svg, pdf); + println!( + "║ {:<28} ║ {:>4} ║ {:>7} ║ {:>5} ║ {:>10} ║ {:>5} ║ {:>5} ║", + cat, + group.len(), + pl, + png, + svg_sa, + svg, + pdf + ); } // ── Overall summary ── diff --git a/crates/ratex-render/tests/font_cache_timing.rs b/crates/ratex-render/tests/font_cache_timing.rs index 0683cfcb..f5634b4f 100644 --- a/crates/ratex-render/tests/font_cache_timing.rs +++ b/crates/ratex-render/tests/font_cache_timing.rs @@ -16,8 +16,7 @@ fn font_cache_speedup() { r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", ]; - let font_dir = - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fonts"); + let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fonts"); let font_dir = font_dir.to_string_lossy().to_string(); let layout_opts = LayoutOptions::default(); diff --git a/crates/ratex-render/tests/golden_test.rs b/crates/ratex-render/tests/golden_test.rs index 4c42041b..2c51585f 100644 --- a/crates/ratex-render/tests/golden_test.rs +++ b/crates/ratex-render/tests/golden_test.rs @@ -14,13 +14,18 @@ const SCORE_THRESHOLD: f64 = 0.30; fn project_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap().parent().unwrap().to_path_buf() + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf() } fn font_dir() -> String { project_root() .join("tools/lexer_compare/node_modules/katex/dist/fonts") - .to_string_lossy().to_string() + .to_string_lossy() + .to_string() } fn load_png(path: &std::path::Path) -> Option<(Vec, u32, u32)> { @@ -34,8 +39,12 @@ fn load_png(path: &std::path::Path) -> Option<(Vec, u32, u32)> { } fn is_ink_pixel(data: &[u8], offset: usize) -> bool { - if offset + 2 >= data.len() { return false; } - data[offset] < INK_THRESHOLD || data[offset+1] < INK_THRESHOLD || data[offset+2] < INK_THRESHOLD + if offset + 2 >= data.len() { + return false; + } + data[offset] < INK_THRESHOLD + || data[offset + 1] < INK_THRESHOLD + || data[offset + 2] < INK_THRESHOLD } /// Find bounding box of ink pixels, return (x_min, y_min, x_max, y_max) @@ -95,7 +104,11 @@ fn crop_image(data: &[u8], w: u32, h: u32, channels: u32) -> (Vec, u32, u32) /// Nearest-neighbor resize to target height, preserving aspect ratio. fn resize_image(data: &[u8], w: u32, h: u32, channels: u32, target_h: u32) -> (Vec, u32, u32) { if h == 0 || w == 0 { - return (vec![255u8; (target_h * target_h * channels) as usize], target_h, target_h); + return ( + vec![255u8; (target_h * target_h * channels) as usize], + target_h, + target_h, + ); } let scale = target_h as f64 / h as f64; let new_w = (w as f64 * scale).round().max(1.0) as u32; @@ -121,8 +134,14 @@ fn resize_image(data: &[u8], w: u32, h: u32, channels: u32, target_h: u32) -> (V /// Compute ink-based comparison score after crop+normalize. #[allow(clippy::too_many_arguments)] fn ink_compare( - ref_data: &[u8], ref_w: u32, ref_h: u32, ref_ch: u32, - test_data: &[u8], test_w: u32, test_h: u32, test_ch: u32, + ref_data: &[u8], + ref_w: u32, + ref_h: u32, + ref_ch: u32, + test_data: &[u8], + test_w: u32, + test_h: u32, + test_ch: u32, ) -> f64 { let (rc, rcw, rch) = crop_image(ref_data, ref_w, ref_h, ref_ch); let (tc, tcw, tch) = crop_image(test_data, test_w, test_h, test_ch); @@ -134,7 +153,9 @@ fn ink_compare( let h = rnh.max(tnh) as usize; let get_ink = |data: &[u8], dw: u32, _dh: u32, ch: u32, x: usize, y: usize| -> bool { - if x >= dw as usize || y >= NORM_HEIGHT as usize { return false; } + if x >= dw as usize || y >= NORM_HEIGHT as usize { + return false; + } let off = (y * dw as usize + x) * ch as usize; is_ink_pixel(data, off) }; @@ -147,14 +168,28 @@ fn ink_compare( for x in 0..w { let r = get_ink(&rn, rnw, rnh, ref_ch, x, y); let t = get_ink(&tn, tnw, tnh, test_ch, x, y); - if r { ref_ink += 1; } - if r && t { both_ink += 1; } - if r || t { either_ink += 1; } + if r { + ref_ink += 1; + } + if r && t { + both_ink += 1; + } + if r || t { + either_ink += 1; + } } } - let iou = if either_ink > 0 { both_ink as f64 / either_ink as f64 } else { 1.0 }; - let recall = if ref_ink > 0 { both_ink as f64 / ref_ink as f64 } else { 0.0 }; + let iou = if either_ink > 0 { + both_ink as f64 / either_ink as f64 + } else { + 1.0 + }; + let recall = if ref_ink > 0 { + both_ink as f64 / ref_ink as f64 + } else { + 0.0 + }; let ref_aspect = rcw as f64 / rch.max(1) as f64; let test_aspect = tcw as f64 / tch.max(1) as f64; @@ -187,7 +222,8 @@ fn run_golden_suite( }; let layout_opts = LayoutOptions::default(); - let lines: Vec = std::fs::read_to_string(tc_path).unwrap() + let lines: Vec = std::fs::read_to_string(tc_path) + .unwrap() .lines() .filter(|l| !l.trim().is_empty() && !l.trim().starts_with('#')) .map(|l| l.to_string()) @@ -200,26 +236,61 @@ fn run_golden_suite( for (i, expr) in lines.iter().enumerate() { let idx = format!("{:04}", i + 1); let ref_path = fixtures_dir.join(format!("{idx}.png")); - if !ref_path.exists() { skipped += 1; continue; } + if !ref_path.exists() { + skipped += 1; + continue; + } - let ast = match parse(expr) { Ok(a) => a, Err(_) => { skipped += 1; continue; } }; + let ast = match parse(expr) { + Ok(a) => a, + Err(_) => { + skipped += 1; + continue; + } + }; let lbox = layout(&ast, &layout_opts); let dl = to_display_list(&lbox); - let png_bytes = match render_to_png(&dl, &render_opts) { Ok(d) => d, Err(_) => { skipped += 1; continue; } }; + let png_bytes = match render_to_png(&dl, &render_opts) { + Ok(d) => d, + Err(_) => { + skipped += 1; + continue; + } + }; - let (ref_data, rw, rh) = match load_png(&ref_path) { Some(v) => v, None => { skipped += 1; continue; } }; - let ref_ch = if ref_data.len() == (rw * rh * 4) as usize { 4 } else { 3 }; + let (ref_data, rw, rh) = match load_png(&ref_path) { + Some(v) => v, + None => { + skipped += 1; + continue; + } + }; + let ref_ch = if ref_data.len() == (rw * rh * 4) as usize { + 4 + } else { + 3 + }; let decoder = png::Decoder::new(std::io::Cursor::new(&png_bytes)); let mut reader = decoder.read_info().unwrap(); let mut test_buf = vec![0u8; reader.output_buffer_size()]; let info = reader.next_frame(&mut test_buf).unwrap(); test_buf.truncate(info.buffer_size()); - let test_ch = if test_buf.len() == (info.width * info.height * 4) as usize { 4 } else { 3 }; + let test_ch = if test_buf.len() == (info.width * info.height * 4) as usize { + 4 + } else { + 3 + }; let score = ink_compare( - &ref_data, rw, rh, ref_ch, - &test_buf, info.width, info.height, test_ch, + &ref_data, + rw, + rh, + ref_ch, + &test_buf, + info.width, + info.height, + test_ch, ); if score >= SCORE_THRESHOLD { @@ -227,13 +298,20 @@ fn run_golden_suite( } else { failed += 1; if failed <= 10 { - eprintln!("FAIL {label} {idx}: score={score:.3} | {}", &expr[..expr.len().min(60)]); + eprintln!( + "FAIL {label} {idx}: score={score:.3} | {}", + &expr[..expr.len().min(60)] + ); } } } let total = passed + failed; - let rate = if total > 0 { passed as f64 / total as f64 * 100.0 } else { 100.0 }; + let rate = if total > 0 { + passed as f64 / total as f64 * 100.0 + } else { + 100.0 + }; eprintln!("\n{label} (ink-based): {passed}/{total} passed ({rate:.1}%), {skipped} skipped"); if total == 0 { @@ -241,7 +319,8 @@ fn run_golden_suite( return; } - assert!(rate >= min_pass_rate, + assert!( + rate >= min_pass_rate, "{label} pass rate {rate:.1}% below {min_pass_rate:.0}% ({passed}/{total})." ); } @@ -311,7 +390,10 @@ fn cjk_smoke_non_blank_rendering() { "CJK smoke '你好世界': only {:.3}% non-white pixels (expected > 0.5%)", cjk_ratio * 100.0 ); - eprintln!("CJK smoke '你好世界': {:.1}% ink coverage", cjk_ratio * 100.0); + eprintln!( + "CJK smoke '你好世界': {:.1}% ink coverage", + cjk_ratio * 100.0 + ); // Emoji: test individual characters that may or may not be in the discovered font. // Fonts like Arial Unicode have CJK but limited emoji; Android/Noto Emoji have emoji. @@ -333,7 +415,10 @@ fn cjk_smoke_non_blank_rendering() { let (ratio, _, _) = count_ink_coverage(&png_bytes); if ratio > 0.001 { any_emoji_rendered = true; - eprintln!("CJK smoke '\\text{{{ch}}}': {:.1}% ink coverage", ratio * 100.0); + eprintln!( + "CJK smoke '\\text{{{ch}}}': {:.1}% ink coverage", + ratio * 100.0 + ); } else { eprintln!("CJK smoke '\\text{{{ch}}}': glyph not in fallback font (0% ink)"); } @@ -365,8 +450,8 @@ mod macos_font_cjk_cmap { #[test] fn apple_gothic_missing_hanzi_is_glyph_zero() { - let bytes = - std::fs::read("/System/Library/Fonts/Supplemental/AppleGothic.ttf").expect("AppleGothic"); + let bytes = std::fs::read("/System/Library/Fonts/Supplemental/AppleGothic.ttf") + .expect("AppleGothic"); let font = ab_glyph::FontRef::try_from_slice(&bytes).expect("parse"); for ch in ['氧', '碳'] { let gid = font.glyph_id(ch); diff --git a/crates/ratex-render/tests/phase_breakdown.rs b/crates/ratex-render/tests/phase_breakdown.rs index 3b62cb8a..1d64f63b 100644 --- a/crates/ratex-render/tests/phase_breakdown.rs +++ b/crates/ratex-render/tests/phase_breakdown.rs @@ -13,16 +13,21 @@ fn phase_breakdown() { let formulas: &[(&str, bool)] = &[ ("x^2 + y^2 = z^2", false), ("a+b=c", false), - (r"\frac{a}{b} + \int_0^\infty e^{-x} dx + \sum_{i=1}^n i \cdot \sqrt{x}", false), - (r"\begin{pmatrix} a & b \\ c & d \end{pmatrix} \cdot \begin{pmatrix} e & f \\ g & h \end{pmatrix}", false), + ( + r"\frac{a}{b} + \int_0^\infty e^{-x} dx + \sum_{i=1}^n i \cdot \sqrt{x}", + false, + ), + ( + r"\begin{pmatrix} a & b \\ c & d \end{pmatrix} \cdot \begin{pmatrix} e & f \\ g & h \end{pmatrix}", + false, + ), // CJK (r"\text{你好世界}\quad x^2 + y^2 = z^2", true), // emoji (r"\text{😊}\quad x^2 + y^2 = z^2 \quad \text{✅}", true), ]; - let font_dir = - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fonts"); + let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fonts"); let font_dir = font_dir.to_string_lossy().to_string(); let layout_opts = LayoutOptions::default(); @@ -36,7 +41,10 @@ fn phase_breakdown() { }; println!("\n============ Render Phase Breakdown (release) ============\n"); - println!("{:<40} {:>6} {:>12} {:>10} {:>10}", "Formula", "Glyphs", "Parse+Layout", "Render", "Total"); + println!( + "{:<40} {:>6} {:>12} {:>10} {:>10}", + "Formula", "Glyphs", "Parse+Layout", "Render", "Total" + ); println!("{:-<40} {:-<6} {:-<12} {:-<10} {:-<10}", "", "", "", "", ""); for &(expr, _has_cjk_emoji) in formulas { @@ -62,7 +70,9 @@ fn phase_breakdown() { let dl = to_display_list(&l); parse_us += t0.elapsed().as_micros(); - glyph_count = dl.items.iter() + glyph_count = dl + .items + .iter() .filter(|i| matches!(i, ratex_types::display_item::DisplayItem::GlyphPath { .. })) .count(); @@ -76,7 +86,11 @@ fn phase_breakdown() { let total_avg = parse_avg + render_avg; // Truncate long formulas for display - let label = if expr.len() > 38 { format!("{}…", &expr[..37]) } else { expr.to_string() }; + let label = if expr.len() > 38 { + format!("{}…", &expr[..37]) + } else { + expr.to_string() + }; println!( "{:<40} {:>6} {:>8} μs {:>6} μs {:>6} μs", diff --git a/crates/ratex-svg/src/bin/render_svg.rs b/crates/ratex-svg/src/bin/render_svg.rs index 45ac84b2..b9977f3d 100644 --- a/crates/ratex-svg/src/bin/render_svg.rs +++ b/crates/ratex-svg/src/bin/render_svg.rs @@ -78,14 +78,20 @@ fn main() { }; let inline = args.contains(&"--inline".to_string()); - let style = if inline { MathStyle::Text } else { MathStyle::Display }; + let style = if inline { + MathStyle::Text + } else { + MathStyle::Display + }; let layout_opts = LayoutOptions::default().with_style(style).with_color(color); let mut idx = 0; let reader: Box = match input_file { - Some(path) => Box::new(io::BufReader::new( - File::open(&path).unwrap_or_else(|e| panic!("Failed to open input file '{}': {}", path, e)), - )), + Some(path) => { + Box::new(io::BufReader::new(File::open(&path).unwrap_or_else(|e| { + panic!("Failed to open input file '{}': {}", path, e) + }))) + } None => Box::new(io::BufReader::new(io::stdin())), }; for line in reader.lines() { @@ -124,12 +130,7 @@ fn svg_formula( fn default_font_dir() -> String { const MARKER: &str = "KaTeX_Main-Regular.ttf"; - let candidates = [ - "fonts", - "../fonts", - "../../fonts", - "../../../fonts", - ]; + let candidates = ["fonts", "../fonts", "../../fonts", "../../../fonts"]; for c in &candidates { let p = std::path::Path::new(c); if p.join(MARKER).is_file() { diff --git a/crates/ratex-svg/src/standalone.rs b/crates/ratex-svg/src/standalone.rs index 870c7577..d9008ea0 100644 --- a/crates/ratex-svg/src/standalone.rs +++ b/crates/ratex-svg/src/standalone.rs @@ -78,7 +78,8 @@ pub(crate) fn standalone_glyph( if let Some(fb) = font_cache.get(&FontId::CjkFallback) { let fid = fb.glyph_id(ch); if fid.0 != 0 { - return outline_to_d(px, py, glyph_em, FontId::CjkFallback, fb, fid).map(StandaloneGlyph::Path); + return outline_to_d(px, py, glyph_em, FontId::CjkFallback, fb, fid) + .map(StandaloneGlyph::Path); } } return None; @@ -193,7 +194,8 @@ fn try_system_unicode_fallback_svg( if let Some(fb) = font_cache.get(&FontId::CjkFallback) { let fid = fb.glyph_id(ch); if fid.0 != 0 { - return outline_to_d(px, py, em, FontId::CjkFallback, fb, fid).map(StandaloneGlyph::Path); + return outline_to_d(px, py, em, FontId::CjkFallback, fb, fid) + .map(StandaloneGlyph::Path); } } None @@ -207,9 +209,7 @@ fn outline_to_d( font: &FontRef<'_>, glyph_id: ab_glyph::GlyphId, ) -> Option { - let curves = ratex_font_loader::outline_cache::get_or_compute_outline( - font_id, font, glyph_id, - )?; + let curves = ratex_font_loader::outline_cache::get_or_compute_outline(font_id, font, glyph_id)?; let units_per_em = font.units_per_em().unwrap_or(1000.0); let mut scale = em / units_per_em; diff --git a/crates/ratex-svg/tests/golden_svg.rs b/crates/ratex-svg/tests/golden_svg.rs index c19648f1..21b5c9bd 100644 --- a/crates/ratex-svg/tests/golden_svg.rs +++ b/crates/ratex-svg/tests/golden_svg.rs @@ -22,10 +22,7 @@ fn project_root() -> PathBuf { } fn font_dir() -> String { - project_root() - .join("fonts") - .to_string_lossy() - .to_string() + project_root().join("fonts").to_string_lossy().to_string() } fn load_png(path: &std::path::Path) -> Option<(Vec, u32, u32)> { diff --git a/crates/ratex-types/src/color.rs b/crates/ratex-types/src/color.rs index fa71fe1d..bc6f2f0b 100644 --- a/crates/ratex-types/src/color.rs +++ b/crates/ratex-types/src/color.rs @@ -92,7 +92,11 @@ impl Color { "cmyk" => { let p = parse_csv_f32(value, 4)?; let (c, m, y, k) = (p[0], p[1], p[2], p[3]); - Some(Self::rgb((1.0 - c) * (1.0 - k), (1.0 - m) * (1.0 - k), (1.0 - y) * (1.0 - k))) + Some(Self::rgb( + (1.0 - c) * (1.0 - k), + (1.0 - m) * (1.0 - k), + (1.0 - y) * (1.0 - k), + )) } _ => None, } diff --git a/crates/ratex-types/src/lib.rs b/crates/ratex-types/src/lib.rs index 466d900d..1f7e5139 100644 --- a/crates/ratex-types/src/lib.rs +++ b/crates/ratex-types/src/lib.rs @@ -8,4 +8,4 @@ pub use color::Color; pub use display_item::{DisplayItem, DisplayList}; pub use math_style::MathStyle; pub use path_command::PathCommand; -pub use unicode_scripts::{UnicodeScript, script_from_codepoint, supported_codepoint}; +pub use unicode_scripts::{script_from_codepoint, supported_codepoint, UnicodeScript}; diff --git a/crates/ratex-types/src/math_style.rs b/crates/ratex-types/src/math_style.rs index 73953738..6bf2b810 100644 --- a/crates/ratex-types/src/math_style.rs +++ b/crates/ratex-types/src/math_style.rs @@ -66,10 +66,9 @@ impl MathStyle { Self::Display | Self::DisplayCramped | Self::Text | Self::TextCramped => { Self::ScriptCramped } - Self::Script - | Self::ScriptCramped - | Self::ScriptScript - | Self::ScriptScriptCramped => Self::ScriptScriptCramped, + Self::Script | Self::ScriptCramped | Self::ScriptScript | Self::ScriptScriptCramped => { + Self::ScriptScriptCramped + } } } @@ -115,10 +114,7 @@ impl MathStyle { pub fn is_tight(self) -> bool { matches!( self, - Self::Script - | Self::ScriptCramped - | Self::ScriptScript - | Self::ScriptScriptCramped + Self::Script | Self::ScriptCramped | Self::ScriptScript | Self::ScriptScriptCramped ) } diff --git a/crates/ratex-types/src/path_command.rs b/crates/ratex-types/src/path_command.rs index 9ff7a970..44a4e7a2 100644 --- a/crates/ratex-types/src/path_command.rs +++ b/crates/ratex-types/src/path_command.rs @@ -5,10 +5,28 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum PathCommand { - MoveTo { x: f64, y: f64 }, - LineTo { x: f64, y: f64 }, - CubicTo { x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64 }, - QuadTo { x1: f64, y1: f64, x: f64, y: f64 }, + MoveTo { + x: f64, + y: f64, + }, + LineTo { + x: f64, + y: f64, + }, + CubicTo { + x1: f64, + y1: f64, + x2: f64, + y2: f64, + x: f64, + y: f64, + }, + QuadTo { + x1: f64, + y1: f64, + x: f64, + y: f64, + }, Close, } @@ -22,13 +40,18 @@ mod tests { PathCommand::MoveTo { x: 0.0, y: 0.0 }, PathCommand::LineTo { x: 10.0, y: 0.0 }, PathCommand::CubicTo { - x1: 10.0, y1: 5.0, - x2: 5.0, y2: 10.0, - x: 0.0, y: 10.0, + x1: 10.0, + y1: 5.0, + x2: 5.0, + y2: 10.0, + x: 0.0, + y: 10.0, }, PathCommand::QuadTo { - x1: 5.0, y1: 5.0, - x: 0.0, y: 0.0, + x1: 5.0, + y1: 5.0, + x: 0.0, + y: 0.0, }, PathCommand::Close, ]; diff --git a/crates/ratex-types/src/unicode_scripts.rs b/crates/ratex-types/src/unicode_scripts.rs index 0872018e..6917f23d 100644 --- a/crates/ratex-types/src/unicode_scripts.rs +++ b/crates/ratex-types/src/unicode_scripts.rs @@ -129,7 +129,10 @@ mod tests { fn test_cyrillic() { assert!(supported_codepoint('А' as u32)); // Cyrillic А = 0x0410 assert!(supported_codepoint('я' as u32)); // Cyrillic я = 0x044F - assert_eq!(script_from_codepoint('А' as u32), Some(UnicodeScript::Cyrillic)); + assert_eq!( + script_from_codepoint('А' as u32), + Some(UnicodeScript::Cyrillic) + ); } #[test] diff --git a/crates/ratex-unicode-font/src/lib.rs b/crates/ratex-unicode-font/src/lib.rs index 78b1af9b..e9d521e7 100644 --- a/crates/ratex-unicode-font/src/lib.rs +++ b/crates/ratex-unicode-font/src/lib.rs @@ -111,7 +111,10 @@ fn load_unicode_fallback_font() -> Option<(Arc>, u32)> { // 1. User-specified font via RATEX_UNICODE_FONT if let Ok(spec) = std::env::var("RATEX_UNICODE_FONT") { if let Some(font) = load_font_spec(&spec) { - eprintln!("[ratex-unicode-font] loaded from RATEX_UNICODE_FONT: {}", spec); + eprintln!( + "[ratex-unicode-font] loaded from RATEX_UNICODE_FONT: {}", + spec + ); return Some(font); } } @@ -302,18 +305,29 @@ mod tests { let ttc = "/System/Library/Fonts/PingFang.ttc"; if std::path::Path::new(ttc).exists() { let result_family = load_font_spec(&format!("{ttc}#PingFang SC")); - assert!(result_family.is_some(), "Should load PingFang.ttc with family name"); + assert!( + result_family.is_some(), + "Should load PingFang.ttc with family name" + ); let result_default = load_font_spec(ttc); - assert!(result_default.is_some(), "Should load PingFang.ttc without selector"); + assert!( + result_default.is_some(), + "Should load PingFang.ttc without selector" + ); if let Some((_, face_index)) = result_default { - assert_eq!(face_index, 0, "TTC without selector should default to face 0"); + assert_eq!( + face_index, 0, + "TTC without selector should default to face 0" + ); } if let Some((_, face_index_family)) = result_family { - let result_index = - load_font_spec(&format!("{ttc}#{face_index_family}")); - assert!(result_index.is_some(), "Should load PingFang.ttc with index"); + let result_index = load_font_spec(&format!("{ttc}#{face_index_family}")); + assert!( + result_index.is_some(), + "Should load PingFang.ttc with index" + ); if let Some((_, face_index_idx)) = result_index { assert_eq!( face_index_family, face_index_idx, diff --git a/crates/ratex-wasm/src/lib.rs b/crates/ratex-wasm/src/lib.rs index ca8843b8..2a346a6e 100644 --- a/crates/ratex-wasm/src/lib.rs +++ b/crates/ratex-wasm/src/lib.rs @@ -67,13 +67,25 @@ fn sanitize_item(item: &mut DisplayItem) { sanitize_f64(y); sanitize_f64(scale); } - DisplayItem::Line { x, y, width, thickness, .. } => { + DisplayItem::Line { + x, + y, + width, + thickness, + .. + } => { sanitize_f64(x); sanitize_f64(y); sanitize_f64(width); sanitize_f64(thickness); } - DisplayItem::Rect { x, y, width, height, .. } => { + DisplayItem::Rect { + x, + y, + width, + height, + .. + } => { sanitize_f64(x); sanitize_f64(y); sanitize_f64(width); @@ -95,7 +107,14 @@ fn sanitize_path_command(cmd: &mut PathCommand) { sanitize_f64(x); sanitize_f64(y); } - PathCommand::CubicTo { x1, y1, x2, y2, x, y } => { + PathCommand::CubicTo { + x1, + y1, + x2, + y2, + x, + y, + } => { sanitize_f64(x1); sanitize_f64(y1); sanitize_f64(x2);