Files
wordcraft/crates/docx/src/units.rs
T
47edcd2ff2 chore: resolve SonarCloud maintainability issues (#4083)
* chore: resolve SonarCloud maintainability issues

* fix: restore compiling refactors, revert behaviour changes, run cargo fmt

- fontdb::norm: keep the closure for filter (char::is_alphanumeric takes
  self, not &self, so it does not fit Iterator::filter).
- story.rs: write_mark_rpr needs &mut self (rev_attrs, rpr_change).
- Revert the viewport meta change and the return 0 added to
  workspace_version/sha256, which changed behaviour or masked failures.
- cargo fmt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Import Document in odt page_parts after dropping glob re-export

* fix: apply_kind_properties binds ContentControl; cell_ref keeps char closures

* fix(engine): pass case helpers by value (clippy needless borrows)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-24 03:54:19 -04:00

161 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Lenient number parsing for OOXML attribute values.
use crate::xml::El;
/// Largest absolute length we accept, points (beyond any real page: 200 inches).
pub const MAX_LEN_PT: f32 = 14_400.0;
/// Parse a number, accepting decimals and ISO 29500 universal measures (`1in`, `2.5cm`, `12pt`,
/// `3mm`, `1pc`, `1pi`). `per_pt` is how many file units make a point when there's no unit
/// suffix (20 for twips, 2 for half-points, 8 for eighths, 12700 for EMU). Returns points.
pub fn measure(s: &str, per_pt: f32) -> Option<f32> {
let t = s.trim();
if t.is_empty() || t.len() > 40 {
return None;
}
let bytes = t.as_bytes();
let mut split = t.len();
let mut in_exp = false;
for (i, &b) in bytes.iter().enumerate() {
if b.is_ascii_digit() || b == b'.' {
in_exp = false;
continue;
}
if (b == b'-' || b == b'+') && (i == 0 || in_exp) {
in_exp = false;
continue;
}
if b == b'e' || b == b'E' {
let rest = &bytes[i + 1..];
let after_sign = match rest.first() {
Some(&s) if s == b'+' || s == b'-' => &rest[1..],
_ => rest,
};
if after_sign.first().is_some_and(u8::is_ascii_digit) {
in_exp = true;
continue;
}
}
split = i;
break;
}
let (num, unit) = t.split_at(split);
let v: f64 = num.parse().ok()?;
if !v.is_finite() {
return None;
}
let unit_lower = unit.trim().to_ascii_lowercase();
let pt = match unit_lower.as_str() {
"" => v / per_pt as f64,
"pt" => v,
"in" => v * 72.0,
"cm" => v * 72.0 / 2.54,
"mm" => v * 72.0 / 25.4,
"pc" | "pi" => v * 12.0,
"px" => v * 0.75,
"emu" => v / 12_700.0,
_ => return None,
};
let pt = pt as f32;
pt.is_finite().then_some(pt.clamp(-MAX_LEN_PT, MAX_LEN_PT))
}
/// `w:line` under `lineRule="auto"`: the multiple of single line spacing, in 240ths (240 =
/// single). Without a unit the value already is that — the transitional form. A universal
/// measure is a height (strict OOXML writes `w:line="13.90pt"`, ISO 29500
/// `ST_SignedTwipsMeasure`), so it goes through twips as Word reads it: × 20 ÷ 240 (13.90 pt =
/// 278 twips, the 1.1583 multiple Word renders).
pub fn line_multiple(s: &str) -> Option<f32> {
let t = s.trim();
let measured = t.ends_with(|c: char| c.is_ascii_alphabetic());
measure(t, 240.0).map(|v| if measured { v * 20.0 / 240.0 } else { v })
}
/// Plain integer (accepts decimals, rounds; clamps into i64 range).
pub fn int(s: &str) -> Option<i64> {
let t = s.trim();
if t.is_empty() || t.len() > 40 {
return None;
}
if let Ok(v) = t.parse::<i64>() {
return Some(v);
}
let v: f64 = t.parse().ok()?;
v.is_finite().then(|| v.round().clamp(i64::MIN as f64, i64::MAX as f64) as i64)
}
pub fn u32_of(s: &str) -> Option<u32> {
int(s).map(|v| v.clamp(0, u32::MAX as i64) as u32)
}
/// Twips attribute → points.
pub fn tw(e: &El, attr: &str) -> Option<f32> {
e.attr(attr).and_then(|v| measure(v, 20.0))
}
/// ST_OnOff: absent `w:val` = on; `0`, `false`, `off` = off.
pub fn on_off(e: &El) -> bool {
match e.attr("w:val") {
None => true,
Some(v) => !matches!(v.trim(), "0" | "false" | "off" | "False" | "FALSE"),
}
}
/// An on/off child of `parent`, if present.
pub fn flag(parent: &El, name: &str) -> Option<bool> {
parent.child(name).map(on_off)
}
/// Format a number for an attribute: integers without a fraction.
pub fn n(v: i64) -> String {
v.to_string()
}
/// Points → twips string.
pub fn twips(pt: f32) -> String {
wordcraft_geom::to_twips(pt.clamp(-MAX_LEN_PT, MAX_LEN_PT)).to_string()
}
/// Points → EMU string (never negative for sizes).
pub fn emu(pt: f32) -> String {
wordcraft_geom::to_emu(pt.clamp(-MAX_LEN_PT, MAX_LEN_PT)).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn measures() {
assert_eq!(measure("1440", 20.0), Some(72.0));
assert_eq!(measure("1in", 20.0), Some(72.0));
assert_eq!(measure("12pt", 2.0), Some(12.0));
assert_eq!(measure("24.0", 2.0), Some(12.0));
assert_eq!(measure("nan", 20.0), None);
assert_eq!(measure("1e400", 20.0), None);
assert_eq!(measure("99999999999999", 20.0), Some(MAX_LEN_PT));
assert_eq!(measure("x", 20.0), None);
// `w:line` auto: the multiple in 240ths without a unit, through twips with one (#2329).
assert_eq!(line_multiple("240"), Some(1.0));
assert_eq!(line_multiple("276"), Some(1.15));
assert_eq!(line_multiple("0"), Some(0.0));
let strict = line_multiple("13.90pt").expect("strict line height");
assert!((strict - 278.0 / 240.0).abs() < 1e-5, "{strict}");
let inches = line_multiple("1.2in").expect("inches");
assert!((inches - 7.2).abs() < 1e-5, "{inches}");
assert_eq!(line_multiple("junk"), None);
assert_eq!(line_multiple(""), None);
assert_eq!(measure("12700emu", 20.0), Some(1.0));
assert_eq!(measure("25400EMU", 20.0), Some(2.0));
assert_eq!(measure("1IN", 20.0), Some(72.0));
assert_eq!(measure("12PT", 2.0), Some(12.0));
assert_eq!(measure("2.54CM", 20.0), Some(72.0));
assert_eq!(measure("25.4MM", 20.0), Some(72.0));
assert_eq!(measure("10PX", 20.0), Some(7.5));
assert_eq!(measure("1e2pt", 20.0), Some(100.0));
assert_eq!(measure("1e+2pt", 20.0), Some(100.0));
assert_eq!(int("12.6"), Some(13));
assert_eq!(u32_of("-5"), Some(0));
}
}