Skip to main content

rapiddoc_native/
script_geometry.rs

1//! 旧_script_geometry的普通JSON适配;不执行inline token精炼或整表语义HTML。
2use crate::{
3    error, python_decimal,
4    text_boundary::{python_alnum, python_space},
5};
6use rapiddoc_core::{Error, ErrorCode};
7use serde_json::{json, Value};
8use std::collections::{HashMap, HashSet};
9mod kernel;
10mod math_identifiers;
11pub mod refine;
12fn invalid() -> Error {
13    error(ErrorCode::InvalidInput, "上下标几何输入无效")
14}
15fn limit() -> Error {
16    error(ErrorCode::ResourceLimit, "上下标几何输入超过预算")
17}
18fn number(v: &Value) -> Option<f64> {
19    let n = if let Some(b) = v.as_bool() {
20        f64::from(u8::from(b))
21    } else if let Some(n) = v.as_f64() {
22        n
23    } else {
24        v.as_str()?.trim().parse().ok()?
25    };
26    n.is_finite().then_some(n)
27}
28fn bbox(v: &Value) -> Option<[f64; 4]> {
29    let a = v.as_array()?;
30    if a.len() != 4 {
31        return None;
32    }
33    let b = [
34        number(&a[0])?,
35        number(&a[1])?,
36        number(&a[2])?,
37        number(&a[3])?,
38    ];
39    (b[2] > b[0] && b[3] > b[1]).then_some(b)
40}
41fn source(c: &Value) -> Option<i64> {
42    c.get("char_idx")?.as_i64()
43}
44fn scalar(v: Option<&Value>) -> Result<String, Error> {
45    Ok(match v {
46        None => String::new(),
47        Some(Value::String(s)) => s.clone(),
48        Some(Value::Null) => "None".into(),
49        Some(Value::Bool(b)) => if *b { "True" } else { "False" }.into(),
50        Some(n) if n.is_number() => n.to_string(),
51        _ => return Err(invalid()),
52    })
53}
54pub fn text_flags(s: &str) -> u32 {
55    let nonempty = !s.is_empty();
56    let mut chars = s.chars();
57    let first = chars.next();
58    let single = first.is_some() && chars.next().is_none();
59    let local = single
60        && first.is_some_and(|c| {
61            c.is_ascii_alphanumeric()
62                || matches!(c as u32,0xff10..=0xff19|0xff21..=0xff3a|0xff41..=0xff5a)
63        });
64    u32::from(nonempty && s.chars().all(python_space))
65        | (u32::from(matches!(s, "\r" | "\n")) << 1)
66        | (u32::from(nonempty && s.chars().all(python_alnum)) << 2)
67        | (u32::from(nonempty && s.chars().all(|c| python_decimal(c).is_some())) << 3)
68        | (u32::from(local) << 4)
69        | (u32::from(matches!(s, "/" | "⁄")) << 5)
70        | (u32::from(matches!(s, "," | "," | "-" | "–" | "—")) << 6)
71}
72fn records(v: &Value) -> Result<Vec<kernel::Record>, Error> {
73    let chars = v["chars"].as_array().ok_or_else(invalid)?;
74    if chars.len() > rapiddoc_core::resources::limit(1500) {
75        return Err(limit());
76    }
77    for name in ["tight_bboxes", "origins"] {
78        if let Some(map) = v.get(name) {
79            if !map.is_object() {
80                return Err(invalid());
81            }
82            if map.as_object().unwrap().len() > rapiddoc_core::resources::limit(20000) {
83                return Err(limit());
84            }
85        }
86    }
87    let protected: HashSet<usize> = match v.get("protected_body_indices") {
88        None => HashSet::new(),
89        Some(a) => {
90            let a = a
91                .as_array()
92                .filter(|a| a.len() <= rapiddoc_core::resources::limit(1500))
93                .ok_or_else(invalid)?;
94            a.iter()
95                .map(|i| {
96                    i.as_u64()
97                        .and_then(|n| usize::try_from(n).ok())
98                        .ok_or_else(invalid)
99                })
100                .collect::<Result<_, _>>()?
101        }
102    };
103    let mut fonts = HashMap::new();
104    let mut output = Vec::with_capacity(chars.len());
105    let mut bytes = 0usize;
106    for (i, c) in chars.iter().enumerate() {
107        if !c.is_object() {
108            return Err(invalid());
109        }
110        let text = scalar(c.get("char"))?;
111        bytes += text.len();
112        if text.len() > rapiddoc_core::resources::limit(256)
113            || bytes > rapiddoc_core::resources::limit(1_000_000)
114        {
115            return Err(limit());
116        }
117        let loose = bbox(&c["bbox"]).unwrap_or([0.; 4]);
118        let index = source(c);
119        let key = index.map(|n| n.to_string());
120        let tight = key.as_ref().and_then(|k| bbox(&v["tight_bboxes"][k]));
121        let origin = key
122            .as_ref()
123            .and_then(|k| v["origins"][k].as_array())
124            .and_then(|a| Some([number(a.first()?)?, number(a.get(1)?)?]));
125        if loose
126            .iter()
127            .chain(tight.iter().flatten())
128            .chain(origin.iter().flatten())
129            .any(|n| n.abs() > 1e9)
130        {
131            return Err(limit());
132        }
133        let mut flags = text_flags(&text) | (u32::from(protected.contains(&i)) << 7);
134        if flags & 3 == 0 && tight.is_some() && origin.is_some() {
135            flags |= 256;
136        }
137        let font = if let Some(name) = c["font"]["name"].as_str().filter(|s| !s.is_empty()) {
138            bytes += name.len();
139            if bytes > rapiddoc_core::resources::limit(1_000_000) {
140                return Err(limit());
141            }
142            let key = (
143                name,
144                c["font"]["flags"].as_i64(),
145                c["font"]["weight"].as_i64(),
146            );
147            let next = fonts.len() as i64;
148            *fonts.entry(key).or_insert(next)
149        } else {
150            -1
151        };
152        output.push((loose, tight, origin, flags, font));
153    }
154    Ok(output)
155}
156pub fn classify(v: &Value) -> Result<Value, Error> {
157    let result = kernel::classify(records(v)?).ok_or_else(invalid)?;
158    Ok(json!(result
159        .into_iter()
160        .map(|r| ["body", "sup", "sub"][r as usize])
161        .collect::<Vec<_>>()))
162}
163/// 给后续token精炼复用的数值引用补充;roles必须与chars一一对应。
164pub fn numeric_indices(v: &Value) -> Result<Value, Error> {
165    let records = records(v)?;
166    let roles = match v.get("roles") {
167        None => vec![0; records.len()],
168        Some(a) => {
169            let a = a
170                .as_array()
171                .filter(|a| a.len() == records.len())
172                .ok_or_else(invalid)?;
173            a.iter()
174                .map(|r| match r.as_str() {
175                    Some("body") => Ok(0),
176                    Some("sup") => Ok(1),
177                    Some("sub") => Ok(2),
178                    _ => Err(invalid()),
179                })
180                .collect::<Result<Vec<_>, _>>()?
181        }
182    };
183    let mut indices = kernel::numeric_indices(records, &roles);
184    indices.sort_unstable();
185    Ok(json!(indices))
186}
187#[cfg(test)]
188mod tests {
189    use super::*;
190    #[test]
191    fn old_script_geometry_and_unicode_flags() {
192        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
193            .join("../../build/migration/script-geometry.json");
194        let f: Value = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
195        for c in f["cases"].as_array().unwrap() {
196            assert_eq!(
197                classify(&c["request"]).unwrap(),
198                c["expected"]["roles"],
199                "{}",
200                c["label"]
201            );
202            assert_eq!(
203                numeric_indices(&c["request"]).unwrap(),
204                c["expected"]["numeric_indices"],
205                "{}",
206                c["label"]
207            );
208        }
209        let extra: Value = serde_json::from_slice(
210            &std::fs::read(
211                std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
212                    .join("../../build/migration/script-numeric-extra.json"),
213            )
214            .unwrap(),
215        )
216        .unwrap();
217        for c in extra["cases"].as_array().unwrap() {
218            assert_eq!(
219                numeric_indices(&c["request"]).unwrap(),
220                c["expected"],
221                "{}",
222                c["label"]
223            );
224        }
225        let mut facts = f["flags"].as_array().unwrap().iter().peekable();
226        for code in 0..=0x10ffff {
227            let Some(c) = char::from_u32(code) else {
228                continue;
229            };
230            let mut buffer = [0; 4];
231            let s = c.encode_utf8(&mut buffer);
232            let expected = if facts.peek().is_some_and(|f| f["text"].as_str() == Some(s)) {
233                facts.next().unwrap()["expected"].as_u64().unwrap()
234            } else {
235                0
236            };
237            assert_eq!(u64::from(text_flags(s)), expected, "U+{code:04X}");
238        }
239        assert!(facts.next().is_none());
240    }
241}