rapiddoc_native/
script_geometry.rs1use 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}
163pub 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}