1pub const THIN: f64 = 0.167; pub const MED: f64 = 0.222; pub const THICK: f64 = 0.278; #[derive(Debug, Clone, Default)]
14pub struct OpDictProperties {
15 pub lspace: f64,
16 pub rspace: f64,
17 pub stretchy: bool,
18 pub symmetric: bool,
19 pub largeop: bool,
20 pub movablelimits: bool,
21 pub fence: bool,
22 pub separator: bool,
23}
24
25pub fn role_to_form(role: &str) -> &'static str {
29 match role {
30 "OPEN" => "prefix",
31 "CLOSE" | "VERTBAR" | "PERIOD" | "POSTFIX" => "postfix",
32 "OPFUNCTION" | "TRIGFUNCTION" | "BIGOP" | "SUMOP" | "INTOP" | "LIMITOP" | "OPERATOR"
33 | "DIFFOP" => "prefix",
34 _ => "infix",
35 }
36}
37
38pub fn opdict_lookup(content: &str, role: &str) -> OpDictProperties {
43 let form = role_to_form(role);
44 let category = lookup_category(content, form);
45
46 let mut props = category_to_properties(category);
47
48 if is_fence_operator(content) {
50 props.fence = true;
51 }
52 if is_separator_operator(content) {
53 props.separator = true;
54 }
55
56 props
57}
58
59fn lookup_category(content: &str, form: &str) -> &'static str {
63 let chars: Vec<char> = content.chars().collect();
64 let len = chars.len();
65
66 if len > 2 {
68 return "Default";
69 }
70 if len == 0 {
71 return "Default";
72 }
73
74 let code1 = chars[0] as u32;
75
76 if len == 1 && (0x0320..=0x03FF).contains(&code1) {
78 return "Default";
79 }
80
81 let effective_code = if len == 2 {
83 let code2 = chars[1] as u32;
84 if code2 == 0x0338 || code2 == 0x20D2 {
86 code1
87 }
88 else if let Some(idx) = two_ascii_char_index(content) {
90 0x0320 + idx as u32
91 } else {
92 return "Default";
93 }
94 } else {
95 code1
96 };
97
98 if form == "infix" && (effective_code == 0x007C || effective_code == 0x223C) {
100 return "ForceDefault";
101 }
102
103 lookup_content_form(effective_code, form).unwrap_or("Default")
105}
106
107fn two_ascii_char_index(content: &str) -> Option<usize> {
109 const OPS: &[&str] = &[
110 "!!", "!=", "&&", "**", "*=", "++", "+=", "--", "-=", "->", "//", "/=", ":=", "<=", "<>", "==",
111 ">=", "||",
112 ];
113 OPS.iter().position(|&op| op == content)
114}
115
116fn lookup_content_form(code: u32, form: &str) -> Option<&'static str> {
120 match form {
121 "infix" => lookup_infix(code),
122 "prefix" => lookup_prefix(code),
123 "postfix" => lookup_postfix(code),
124 _ => None,
125 }
126}
127
128const INFIX_TABLE: &[(u32, u32, &str)] = &[
130 (0x0025, 0x0025, "C"),
131 (0x002A, 0x002A, "C"),
132 (0x002B, 0x002B, "B"),
133 (0x002C, 0x002C, "M"),
134 (0x002D, 0x002D, "B"),
135 (0x002E, 0x002E, "C"),
136 (0x002F, 0x002F, "B"),
137 (0x003A, 0x003A, "M"),
138 (0x003B, 0x003B, "M"),
139 (0x003F, 0x0040, "C"),
140 (0x005C, 0x005C, "K"),
141 (0x005E, 0x005E, "C"),
142 (0x005F, 0x005F, "K"),
143 (0x00B1, 0x00B1, "B"),
144 (0x00B7, 0x00B7, "C"),
145 (0x00D7, 0x00D7, "C"),
146 (0x00F7, 0x00F7, "B"),
147 (0x0322, 0x0322, "B"),
148 (0x0323, 0x0323, "C"),
149 (0x032E, 0x032E, "C"),
150 (0x2022, 0x2022, "C"),
151 (0x2043, 0x2043, "C"),
152 (0x2044, 0x2044, "B"),
153 (0x2061, 0x2064, "K"),
154 (0x2190, 0x2195, "A"),
155 (0x219A, 0x21AE, "A"),
156 (0x21B0, 0x21B5, "A"),
157 (0x21B9, 0x21B9, "A"),
158 (0x21BC, 0x21D5, "A"),
159 (0x21DA, 0x21F0, "A"),
160 (0x21F3, 0x21FF, "A"),
161 (0x2206, 0x2206, "K"),
162 (0x2212, 0x2216, "B"),
163 (0x2217, 0x2219, "C"),
164 (0x2227, 0x222A, "B"),
165 (0x2236, 0x2236, "B"),
166 (0x2238, 0x2238, "B"),
167 (0x2240, 0x2240, "C"),
168 (0x228C, 0x228E, "B"),
169 (0x2293, 0x2296, "B"),
170 (0x2297, 0x2297, "C"),
171 (0x2298, 0x2298, "B"),
172 (0x2299, 0x229B, "C"),
173 (0x229D, 0x229F, "B"),
174 (0x22A0, 0x22A1, "C"),
175 (0x22BA, 0x22BA, "C"),
176 (0x22BB, 0x22BD, "B"),
177 (0x22C4, 0x22C7, "C"),
178 (0x22C9, 0x22CC, "C"),
179 (0x22CE, 0x22CF, "B"),
180 (0x22D2, 0x22D3, "B"),
181 (0x2305, 0x2306, "C"),
182 (0x2794, 0x2794, "A"),
183 (0x2795, 0x2797, "B"),
184 (0x2799, 0x2799, "A"),
185 (0x279B, 0x27A1, "A"),
186 (0x27A5, 0x27A6, "A"),
187 (0x27A8, 0x27AF, "A"),
188 (0x27B1, 0x27B1, "A"),
189 (0x27B3, 0x27B3, "A"),
190 (0x27B5, 0x27B5, "A"),
191 (0x27B8, 0x27B8, "A"),
192 (0x27BA, 0x27BE, "A"),
193 (0x27CB, 0x27CB, "C"),
194 (0x27CD, 0x27CD, "C"),
195 (0x27F0, 0x27F1, "A"),
196 (0x27F4, 0x27FF, "A"),
197 (0x2900, 0x2920, "A"),
198 (0x2934, 0x2937, "A"),
199 (0x2942, 0x2975, "A"),
200 (0x297C, 0x297F, "A"),
201 (0x29B8, 0x29B8, "B"),
202 (0x29BC, 0x29BC, "B"),
203 (0x29C4, 0x29C5, "B"),
204 (0x29C6, 0x29C8, "C"),
205 (0x29D4, 0x29D7, "C"),
206 (0x29E2, 0x29E2, "C"),
207 (0x29F5, 0x29FB, "B"),
208 (0x2A1D, 0x2A1E, "C"),
209 (0x2A1F, 0x2A2E, "B"),
210 (0x2A2F, 0x2A37, "C"),
211 (0x2A38, 0x2A3A, "B"),
212 (0x2A3B, 0x2A3D, "C"),
213 (0x2A3E, 0x2A3E, "B"),
214 (0x2A3F, 0x2A3F, "C"),
215 (0x2A40, 0x2A4F, "B"),
216 (0x2A50, 0x2A50, "C"),
217 (0x2A51, 0x2A63, "B"),
218 (0x2A64, 0x2A65, "C"),
219 (0x2ADB, 0x2ADB, "B"),
220 (0x2ADC, 0x2ADD, "C"),
221 (0x2AF6, 0x2AF6, "B"),
222 (0x2AFB, 0x2AFB, "B"),
223 (0x2AFD, 0x2AFD, "B"),
224 (0x2AFE, 0x2AFE, "C"),
225 (0x2B04, 0x2B07, "A"),
226 (0x2B0C, 0x2B11, "A"),
227 (0x2B30, 0x2B3E, "A"),
228 (0x2B40, 0x2B4C, "A"),
229 (0x2B60, 0x2B65, "A"),
230 (0x2B6A, 0x2B6D, "A"),
231 (0x2B70, 0x2B73, "A"),
232 (0x2B7A, 0x2B7D, "A"),
233 (0x2B80, 0x2B87, "A"),
234 (0x2B95, 0x2B95, "A"),
235 (0x2BA0, 0x2BAF, "A"),
236 (0x2BB8, 0x2BB8, "A"),
237];
238const PREFIX_TABLE: &[(u32, u32, &str)] = &[
239 (0x0021, 0x0021, "D"),
240 (0x0028, 0x0028, "F"),
241 (0x002B, 0x002B, "D"),
242 (0x002D, 0x002D, "D"),
243 (0x005B, 0x005B, "F"),
244 (0x007B, 0x007B, "F"),
245 (0x007C, 0x007C, "F"),
246 (0x00AC, 0x00AC, "D"),
247 (0x00B1, 0x00B1, "D"),
248 (0x0331, 0x0331, "D"),
249 (0x2016, 0x2016, "F"),
250 (0x2018, 0x2018, "D"),
251 (0x201C, 0x201C, "D"),
252 (0x2145, 0x2146, "L"),
253 (0x2200, 0x2201, "D"),
254 (0x2202, 0x2202, "L"),
255 (0x2203, 0x2204, "D"),
256 (0x2207, 0x2207, "D"),
257 (0x220F, 0x2211, "J"),
258 (0x2212, 0x2213, "D"),
259 (0x221A, 0x221C, "L"),
260 (0x221F, 0x2222, "D"),
261 (0x222B, 0x2233, "H"),
262 (0x2234, 0x2235, "D"),
263 (0x223C, 0x223C, "D"),
264 (0x22BE, 0x22BF, "D"),
265 (0x22C0, 0x22C3, "J"),
266 (0x2308, 0x2308, "F"),
267 (0x230A, 0x230A, "F"),
268 (0x2310, 0x2310, "D"),
269 (0x2319, 0x2319, "D"),
270 (0x2329, 0x2329, "F"),
271 (0x2772, 0x2772, "F"),
272 (0x2795, 0x2796, "D"),
273 (0x27C0, 0x27C0, "D"),
274 (0x27E6, 0x27E6, "F"),
275 (0x27E8, 0x27E8, "F"),
276 (0x27EA, 0x27EA, "F"),
277 (0x27EC, 0x27EC, "F"),
278 (0x27EE, 0x27EE, "F"),
279 (0x2980, 0x2980, "F"),
280 (0x2983, 0x2983, "F"),
281 (0x2985, 0x2985, "F"),
282 (0x2987, 0x2987, "F"),
283 (0x2989, 0x2989, "F"),
284 (0x298B, 0x298B, "F"),
285 (0x298D, 0x298D, "F"),
286 (0x298F, 0x298F, "F"),
287 (0x2991, 0x2991, "F"),
288 (0x2993, 0x2993, "F"),
289 (0x2995, 0x2995, "F"),
290 (0x2997, 0x2997, "F"),
291 (0x2999, 0x2999, "F"),
292 (0x299B, 0x29AF, "D"),
293 (0x29D8, 0x29D8, "F"),
294 (0x29DA, 0x29DA, "F"),
295 (0x29FC, 0x29FC, "F"),
296 (0x2A00, 0x2A0A, "J"),
297 (0x2A0B, 0x2A1C, "H"),
298 (0x2A1D, 0x2A1E, "J"),
299 (0x2AEC, 0x2AED, "D"),
300 (0x2AFC, 0x2AFC, "J"),
301 (0x2AFF, 0x2AFF, "J"),
302];
303const POSTFIX_TABLE: &[(u32, u32, &str)] = &[
304 (0x0021, 0x0022, "E"),
305 (0x0025, 0x0027, "E"),
306 (0x0029, 0x0029, "G"),
307 (0x005D, 0x005D, "G"),
308 (0x005E, 0x005F, "I"),
309 (0x0060, 0x0060, "E"),
310 (0x007C, 0x007C, "G"),
311 (0x007D, 0x007D, "G"),
312 (0x007E, 0x007E, "I"),
313 (0x00A8, 0x00A8, "E"),
314 (0x00AF, 0x00AF, "I"),
315 (0x00B0, 0x00B0, "E"),
316 (0x00B2, 0x00B4, "E"),
317 (0x00B8, 0x00B9, "E"),
318 (0x02C6, 0x02C7, "I"),
319 (0x02C9, 0x02C9, "I"),
320 (0x02CA, 0x02CB, "E"),
321 (0x02CD, 0x02CD, "I"),
322 (0x02D8, 0x02DA, "E"),
323 (0x02DC, 0x02DC, "I"),
324 (0x02DD, 0x02DD, "E"),
325 (0x02F7, 0x02F7, "I"),
326 (0x0302, 0x0302, "I"),
327 (0x0311, 0x0311, "E"),
328 (0x0320, 0x0320, "E"),
329 (0x0325, 0x0325, "E"),
330 (0x0327, 0x0327, "E"),
331 (0x0331, 0x0331, "E"),
332 (0x2016, 0x2016, "G"),
333 (0x2019, 0x201B, "E"),
334 (0x201D, 0x201F, "E"),
335 (0x2032, 0x2037, "E"),
336 (0x203E, 0x203E, "I"),
337 (0x2057, 0x2057, "E"),
338 (0x20DB, 0x20DC, "E"),
339 (0x2309, 0x2309, "G"),
340 (0x230B, 0x230B, "G"),
341 (0x2322, 0x2323, "I"),
342 (0x232A, 0x232A, "G"),
343 (0x23B4, 0x23B5, "I"),
344 (0x23CD, 0x23CD, "E"),
345 (0x23DC, 0x23E1, "I"),
346 (0x2773, 0x2773, "G"),
347 (0x27E7, 0x27E7, "G"),
348 (0x27E9, 0x27E9, "G"),
349 (0x27EB, 0x27EB, "G"),
350 (0x27ED, 0x27ED, "G"),
351 (0x27EF, 0x27EF, "G"),
352 (0x2980, 0x2980, "G"),
353 (0x2984, 0x2984, "G"),
354 (0x2986, 0x2986, "G"),
355 (0x2988, 0x2988, "G"),
356 (0x298A, 0x298A, "G"),
357 (0x298C, 0x298C, "G"),
358 (0x298E, 0x298E, "G"),
359 (0x2990, 0x2990, "G"),
360 (0x2992, 0x2992, "G"),
361 (0x2994, 0x2994, "G"),
362 (0x2996, 0x2996, "G"),
363 (0x2998, 0x2998, "G"),
364 (0x2999, 0x2999, "G"),
365 (0x29D9, 0x29D9, "G"),
366 (0x29DB, 0x29DB, "G"),
367 (0x29FD, 0x29FD, "G"),
368 (0x1EEF0, 0x1EEF1, "I"),
369];
370const FENCE_TABLE: &[(u32, u32)] = &[
371 (0x0028, 0x0029),
372 (0x005B, 0x005B),
373 (0x005D, 0x005D),
374 (0x007B, 0x007D),
375 (0x0331, 0x0331),
376 (0x2016, 0x2016),
377 (0x2018, 0x2019),
378 (0x201C, 0x201D),
379 (0x2308, 0x230B),
380 (0x2329, 0x232A),
381 (0x2772, 0x2773),
382 (0x27E6, 0x27EF),
383 (0x2980, 0x2980),
384 (0x2983, 0x2999),
385 (0x29D8, 0x29DB),
386 (0x29FC, 0x29FD),
387];
388
389fn range_lookup(table: &[(u32, u32, &'static str)], code: u32) -> Option<&'static str> {
391 match table.binary_search_by(|&(a, b, _)| {
392 if code < a {
393 std::cmp::Ordering::Greater
394 } else if code > b {
395 std::cmp::Ordering::Less
396 } else {
397 std::cmp::Ordering::Equal
398 }
399 }) {
400 Ok(i) => Some(table[i].2),
401 Err(_) => None,
402 }
403}
404
405fn lookup_infix(code: u32) -> Option<&'static str> { range_lookup(INFIX_TABLE, code) }
406fn lookup_prefix(code: u32) -> Option<&'static str> { range_lookup(PREFIX_TABLE, code) }
407fn lookup_postfix(code: u32) -> Option<&'static str> { range_lookup(POSTFIX_TABLE, code) }
408
409fn category_to_properties(category: &str) -> OpDictProperties {
413 match category {
414 "A" => OpDictProperties {
415 lspace: THICK,
416 rspace: THICK,
417 stretchy: true,
418 ..Default::default()
419 },
420 "B" => OpDictProperties {
421 lspace: MED,
422 rspace: MED,
423 ..Default::default()
424 },
425 "C" => OpDictProperties {
426 lspace: THIN,
427 rspace: THIN,
428 ..Default::default()
429 },
430 "D" => OpDictProperties {
431 lspace: 0.0,
432 rspace: 0.0,
433 ..Default::default()
434 },
435 "E" => OpDictProperties {
436 lspace: 0.0,
437 rspace: 0.0,
438 ..Default::default()
439 },
440 "F" => OpDictProperties {
441 lspace: 0.0,
442 rspace: 0.0,
443 stretchy: true,
444 symmetric: true,
445 ..Default::default()
446 },
447 "G" => OpDictProperties {
448 lspace: 0.0,
449 rspace: 0.0,
450 stretchy: true,
451 symmetric: true,
452 ..Default::default()
453 },
454 "H" => OpDictProperties {
455 lspace: THIN,
456 rspace: THIN,
457 symmetric: true,
458 largeop: true,
459 ..Default::default()
460 },
461 "I" => OpDictProperties {
462 lspace: 0.0,
463 rspace: 0.0,
464 stretchy: true,
465 ..Default::default()
466 },
467 "J" => OpDictProperties {
468 lspace: THIN,
469 rspace: THIN,
470 symmetric: true,
471 largeop: true,
472 movablelimits: true,
473 ..Default::default()
474 },
475 "K" => OpDictProperties {
476 lspace: 0.0,
477 rspace: 0.0,
478 ..Default::default()
479 },
480 "L" => OpDictProperties {
481 lspace: THIN,
482 rspace: 0.0,
483 ..Default::default()
484 },
485 "M" => OpDictProperties {
486 lspace: 0.0,
487 rspace: THIN,
488 ..Default::default()
489 },
490 _ => OpDictProperties {
491 lspace: THICK,
492 rspace: THICK,
493 ..Default::default()
494 }, }
496}
497
498fn is_fence_operator(content: &str) -> bool {
500 let c = match content.chars().next() {
501 Some(c) => c as u32,
502 None => return false,
503 };
504 FENCE_TABLE.iter().any(|&(a, b)| (a..=b).contains(&c))
505}
506
507fn is_separator_operator(content: &str) -> bool {
509 let c = match content.chars().next() {
510 Some(c) => c as u32,
511 None => return false,
512 };
513 c == 0x002C || c == 0x003B || c == 0x2063
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_content_form_table_regressions() {
522 let p = opdict_lookup("\u{2A50}", "BINOP");
524 assert_eq!(p.lspace, THIN);
525 assert_eq!(p.rspace, THIN);
526 let p = opdict_lookup("\u{27A1}", "ARROW");
528 assert_eq!(p.lspace, THICK);
529 let p = opdict_lookup("\u{0331}", "OPEN");
531 assert!(p.fence);
532 }
533
534 #[test]
535 fn test_opdict_lookup_plus() {
536 let props = opdict_lookup("+", "ADDOP");
537 assert!((props.lspace - MED).abs() < 0.001); assert!((props.rspace - MED).abs() < 0.001);
539 assert!(!props.stretchy);
540 }
541
542 #[test]
550 fn prefix_role_zeroes_left_spacing_that_infix_role_keeps() {
551 for glyph in ["\u{2212}", "+"] {
552 let prefix = opdict_lookup(glyph, "OPERATOR");
553 let infix = opdict_lookup(glyph, "ADDOP");
554 assert_eq!(prefix.lspace, 0.0, "{glyph} prefix (OPERATOR) lspace");
555 assert!(
556 (infix.lspace - MED).abs() < 0.001,
557 "{glyph} infix (ADDOP) lspace should be MED"
558 );
559 assert!(
561 infix.lspace > prefix.lspace,
562 "{glyph}: infix must reserve more left space than prefix"
563 );
564 }
565 }
566
567 #[test]
568 fn test_opdict_lookup_open_paren() {
569 let props = opdict_lookup("(", "OPEN");
570 assert!((props.lspace - 0.0).abs() < 0.001); assert!(props.stretchy);
572 assert!(props.symmetric);
573 assert!(props.fence);
574 }
575
576 #[test]
577 fn test_opdict_lookup_integral() {
578 let props = opdict_lookup("\u{222B}", "INTOP");
579 assert!(props.largeop); assert!(props.symmetric);
581 }
582
583 #[test]
584 fn test_opdict_lookup_sum() {
585 let props = opdict_lookup("\u{2211}", "SUMOP");
586 assert!(props.largeop); assert!(props.movablelimits);
588 }
589
590 #[test]
591 fn test_opdict_lookup_comma() {
592 let props = opdict_lookup(",", "PUNCT");
593 assert!(props.separator);
594 assert!((props.rspace - THIN).abs() < 0.001); }
596
597 #[test]
598 fn test_opdict_lookup_invisible_times() {
599 let props = opdict_lookup("\u{2062}", "MULOP");
600 assert!((props.lspace - 0.0).abs() < 0.001); assert!((props.rspace - 0.0).abs() < 0.001);
602 }
603
604 #[test]
605 fn test_two_ascii_char_ops() {
606 assert_eq!(two_ascii_char_index("!="), Some(1));
607 assert_eq!(two_ascii_char_index("->"), Some(9));
608 assert_eq!(two_ascii_char_index("xx"), None);
609 }
610}