1use alloc::borrow::Cow;
9
10use crate::ParseError;
11use crate::build_common::{make_span, make_svg_span};
12use crate::dom_tree::{DomSpan, HtmlDomNode, LineNode, PathNode, SvgChildNode, SvgNode};
13use crate::mathml_tree::{MathNode, MathNodeType, TextNode};
14use crate::namespace::KeyMap;
15use crate::options::Options;
16use crate::parser::parse_node::AnyParseNode;
17use crate::types::ClassList;
18use crate::types::CssProperty;
19use crate::types::ParseErrorKind;
20use crate::units::make_em;
21use phf::{phf_map, phf_set};
22
23pub const STRETCHY_CODE_POINT: phf::Map<&'static str, &'static str> = phf_map! {
25 "widehat" => "^",
26 "widecheck" => "\u{2c7}",
27 "widetilde" => "~",
28 "utilde" => "~",
29 "overleftarrow" => "\u{2190}",
30 "underleftarrow" => "\u{2190}",
31 "xleftarrow" => "\u{2190}",
32 "overrightarrow" => "\u{2192}",
33 "underrightarrow" => "\u{2192}",
34 "xrightarrow" => "\u{2192}",
35 "underbrace" => "\u{23df}",
36 "overbrace" => "\u{23de}",
37 "overgroup" => "\u{23e0}",
38 "undergroup" => "\u{23e1}",
39 "overleftrightarrow" => "\u{2194}",
40 "underleftrightarrow" => "\u{2194}",
41 "xleftrightarrow" => "\u{2194}",
42 "Overrightarrow" => "\u{21d2}",
43 "xRightarrow" => "\u{21d2}",
44 "overleftharpoon" => "\u{21bc}",
45 "xleftharpoonup" => "\u{21bc}",
46 "overrightharpoon" => "\u{21c0}",
47 "xrightharpoonup" => "\u{21c0}",
48 "xLeftarrow" => "\u{21d0}",
49 "xLeftrightarrow" => "\u{21d4}",
50 "xhookleftarrow" => "\u{21a9}",
51 "xhookrightarrow" => "\u{21aa}",
52 "xmapsto" => "\u{21a6}",
53 "xrightharpoondown" => "\u{21c1}",
54 "xleftharpoondown" => "\u{21bd}",
55 "xrightleftharpoons" => "\u{21cc}",
56 "xleftrightharpoons" => "\u{21cb}",
57 "xtwoheadleftarrow" => "\u{219e}",
58 "xtwoheadrightarrow" => "\u{21a0}",
59 "xlongequal" => "=",
60 "xtofrom" => "\u{21c4}",
61 "xrightleftarrows" => "\u{21c4}",
62 "xrightequilibrium" => "\u{21cc}",
63 "xleftequilibrium" => "\u{21cb}",
64 "\\cdrightarrow" => "\u{2192}",
65 "\\cdleftarrow" => "\u{2190}",
66 "\\cdlongequal" => "=",
67};
68
69#[derive(Debug, Clone)]
71pub struct ImageData {
72 pub paths: &'static [&'static str],
74 pub min_width: f64,
76 pub height: f64,
78 pub align: Option<&'static str>,
80}
81
82impl ImageData {
83 #[must_use]
85 pub const fn new(
86 paths: &'static [&'static str],
87 min_width: f64,
88 height: f64,
89 align: Option<&'static str>,
90 ) -> Self {
91 Self {
92 paths,
93 min_width,
94 height,
95 align,
96 }
97 }
98}
99
100const IMAGES_DATA: phf::Map<&'static str, ImageData> = phf_map! {
101 "overrightarrow" => ImageData::new(&["rightarrow"], 0.888, 522.0, Some("xMaxYMin")),
102 "overleftarrow" => ImageData::new(&["leftarrow"], 0.888, 522.0, Some("xMinYMin")),
103 "underrightarrow" => ImageData::new(&["rightarrow"], 0.888, 522.0, Some("xMaxYMin")),
104 "underleftarrow" => ImageData::new(&["leftarrow"], 0.888, 522.0, Some("xMinYMin")),
105 "xrightarrow" => ImageData::new(&["rightarrow"], 1.469, 522.0, Some("xMaxYMin")),
106 "\\cdrightarrow" => ImageData::new(&["rightarrow"], 3.0, 522.0, Some("xMaxYMin")),
107 "xleftarrow" => ImageData::new(&["leftarrow"], 1.469, 522.0, Some("xMinYMin")),
108 "\\cdleftarrow" => ImageData::new(&["leftarrow"], 3.0, 522.0, Some("xMinYMin")),
109 "Overrightarrow" => ImageData::new(&["doublerightarrow"], 0.888, 560.0, Some("xMaxYMin")),
110 "xRightarrow" => ImageData::new(&["doublerightarrow"], 1.526, 560.0, Some("xMaxYMin")),
111 "xLeftarrow" => ImageData::new(&["doubleleftarrow"], 1.526, 560.0, Some("xMinYMin")),
112 "overleftharpoon" => ImageData::new(&["leftharpoon"], 0.888, 522.0, Some("xMinYMin")),
113 "xleftharpoonup" => ImageData::new(&["leftharpoon"], 0.888, 522.0, Some("xMinYMin")),
114 "xleftharpoondown" => ImageData::new(&["leftharpoondown"], 0.888, 522.0, Some("xMinYMin")),
115 "overrightharpoon" => ImageData::new(&["rightharpoon"], 0.888, 522.0, Some("xMaxYMin")),
116 "xrightharpoonup" => ImageData::new(&["rightharpoon"], 0.888, 522.0, Some("xMaxYMin")),
117 "xrightharpoondown" => ImageData::new(&["rightharpoondown"], 0.888, 522.0, Some("xMaxYMin")),
118 "xlongequal" => ImageData::new(&["longequal"], 0.888, 334.0, Some("xMinYMin")),
119 "\\cdlongequal" => ImageData::new(&["longequal"], 3.0, 334.0, Some("xMinYMin")),
120 "xtwoheadleftarrow" => ImageData::new(&["twoheadleftarrow"], 0.888, 334.0, Some("xMinYMin")),
121 "xtwoheadrightarrow" => ImageData::new(&["twoheadrightarrow"], 0.888, 334.0, Some("xMaxYMin")),
122 "overleftrightarrow" => ImageData::new(&["leftarrow", "rightarrow"], 0.888, 522.0, None),
123 "overbrace" => ImageData::new(&["leftbrace", "midbrace", "rightbrace"], 1.6, 548.0, None),
124 "underbrace" => ImageData::new(&["leftbraceunder", "midbraceunder", "rightbraceunder"], 1.6, 548.0, None),
125 "underleftrightarrow" => ImageData::new(&["leftarrow", "rightarrow"], 0.888, 522.0, None),
126 "xleftrightarrow" => ImageData::new(&["leftarrow", "rightarrow"], 1.75, 522.0, None),
127 "xLeftrightarrow" => ImageData::new(&["doubleleftarrow", "doublerightarrow"], 1.75, 560.0, None),
128 "xrightleftharpoons" => ImageData::new(&["leftharpoondownplus", "rightharpoonplus"], 1.75, 716.0, None),
129 "xleftrightharpoons" => ImageData::new(&["leftharpoonplus", "rightharpoondownplus"], 1.75, 716.0, None),
130 "xhookleftarrow" => ImageData::new(&["leftarrow", "righthook"], 1.08, 522.0, None),
131 "xhookrightarrow" => ImageData::new(&["lefthook", "rightarrow"], 1.08, 522.0, None),
132 "overlinesegment" => ImageData::new(&["leftlinesegment", "rightlinesegment"], 0.888, 522.0, None),
133 "underlinesegment" => ImageData::new(&["leftlinesegment", "rightlinesegment"], 0.888, 522.0, None),
134 "overgroup" => ImageData::new(&["leftgroup", "rightgroup"], 0.888, 342.0, None),
135 "undergroup" => ImageData::new(&["leftgroupunder", "rightgroupunder"], 0.888, 342.0, None),
136 "xmapsto" => ImageData::new(&["leftmapsto", "rightarrow"], 1.5, 522.0, None),
137 "xtofrom" => ImageData::new(&["leftToFrom", "rightToFrom"], 1.75, 528.0, None),
138 "xrightleftarrows" => ImageData::new(&["baraboveleftarrow", "rightarrowabovebar"], 1.75, 901.0, None),
139 "xrightequilibrium" => ImageData::new(&["baraboveshortleftharpoon", "rightharpoonaboveshortbar"], 1.75, 716.0, None),
140 "xleftequilibrium" => ImageData::new(&["shortbaraboveleftharpoon", "shortrightharpoonabovebar"], 1.75, 716.0, None),
141};
142
143const fn group_length(arg: &AnyParseNode) -> usize {
145 if let AnyParseNode::OrdGroup(ordgroup) = arg {
146 ordgroup.body.len()
147 } else {
148 1
149 }
150}
151
152const ACCENT_STRETCHY: phf::Set<&'static str> = phf_set! {
153 "widehat", "widecheck", "widetilde", "utilde"
154};
155
156const ACCENT_STRETCHY_OVER: phf::Set<&'static str> = phf_set! {
157 "widehat", "widecheck"
158};
159
160pub fn svg_span(group: &AnyParseNode, options: &Options) -> Result<HtmlDomNode, ParseError> {
162 let Some(label) = group.label() else {
164 return Err(ParseError::new(
165 ParseErrorKind::UnsupportedGroupTypeForSvgSpan,
166 ));
167 };
168
169 let Some(label) = label.strip_prefix('\\') else {
170 return Err(ParseError::new(ParseErrorKind::LabelMissingBackslashPrefix));
171 };
172
173 if ACCENT_STRETCHY.contains(label) {
174 let grp_base = match group {
176 AnyParseNode::Accent(acc) => &acc.base,
177 AnyParseNode::AccentUnder(acc_under) => &acc_under.base,
178 _ => {
179 return Err(ParseError::new(ParseErrorKind::InvalidGroupTypeForAccent));
180 }
181 };
182
183 let num_chars = group_length(grp_base) as f64;
184 let (view_box_width, view_box_height, height_val, path_name) = if num_chars > 5.0 {
185 if ACCENT_STRETCHY_OVER.contains(label) {
186 (2364.0, 420.0, 0.42, format!("{label}4"))
187 } else {
188 (2340.0, 312.0, 0.34, "tilde4".to_owned())
189 }
190 } else {
191 let img_index = [1, 1, 2, 2, 3, 3][num_chars as usize];
192 if ACCENT_STRETCHY_OVER.contains(label) {
193 let widths = [0.0, 1062.0, 2364.0, 2364.0, 2364.0];
194 let heights = [0.0, 239.0, 300.0, 360.0, 420.0];
195 let h_vals = [0.0, 0.24, 0.3, 0.3, 0.36, 0.42];
196 (
197 widths[img_index],
198 heights[img_index],
199 h_vals[img_index],
200 format!("{label}{img_index}"),
201 )
202 } else {
203 let widths = [0.0, 600.0, 1033.0, 2339.0, 2340.0];
204 let heights = [0.0, 260.0, 286.0, 306.0, 312.0];
205 let h_vals = [0.0, 0.26, 0.286, 0.3, 0.306, 0.34];
206 (
207 widths[img_index],
208 heights[img_index],
209 h_vals[img_index],
210 format!("tilde{img_index}"),
211 )
212 }
213 };
214
215 let path = PathNode {
216 path_name,
217 alternate: None,
218 };
219
220 let mut svg_node = SvgNode::builder()
221 .children(vec![SvgChildNode::Path(path)])
222 .build();
223 svg_node.attributes.extend([
224 ("width".to_owned(), "100%".to_owned()),
225 ("height".to_owned(), make_em(height_val)),
226 (
227 "viewBox".to_owned(),
228 format!("0 0 {view_box_width} {view_box_height}"),
229 ),
230 ("preserveAspectRatio".to_owned(), "none".to_owned()),
231 ]);
232 let mut span = make_svg_span(vec![], vec![svg_node], options);
233
234 span.height = height_val;
235 span.style.insert(CssProperty::Height, make_em(height_val));
236
237 Ok(span.into())
238 } else {
239 let data = IMAGES_DATA.get(label).ok_or_else(|| {
241 ParseError::new(ParseErrorKind::UnknownStretchyElement {
242 label: label.to_owned(),
243 })
244 })?;
245
246 let mut spans: Vec<HtmlDomNode> = Vec::new();
247 let height_val = data.height / 1000.0;
248 let view_box_width = 400000.0;
249
250 let (width_classes, aligns): (&[&str], &[&str]) = match data.paths.len() {
251 1 => {
252 let align = data.align.unwrap_or("xMinYMin");
253 (&["hide-tail"], &[align])
254 }
255 2 => (
256 &["halfarrow-left", "halfarrow-right"],
257 &["xMinYMin", "xMaxYMin"],
258 ),
259 3 => (
260 &["brace-left", "brace-center", "brace-right"],
261 &["xMinYMin", "xMidYMin", "xMaxYMin"],
262 ),
263 _ => {
264 return Err(ParseError::new(
265 ParseErrorKind::UnsupportedStretchyPathCount {
266 count: data.paths.len(),
267 },
268 ));
269 }
270 };
271
272 for (i, (width_class, align)) in width_classes.iter().zip(aligns.iter()).enumerate() {
273 let path = PathNode {
274 path_name: data.paths[i].to_owned(),
275 alternate: None,
276 };
277
278 let mut svg_node = SvgNode::builder()
279 .children(vec![SvgChildNode::Path(path)])
280 .build();
281
282 svg_node.attributes.extend([
283 ("width".to_owned(), "400em".to_owned()),
284 ("height".to_owned(), make_em(height_val)),
285 (
286 "viewBox".to_owned(),
287 format!("0 0 {} {}", view_box_width, data.height),
288 ),
289 ("preserveAspectRatio".to_owned(), format!("{align} slice")),
290 ]);
291
292 let span = make_span(
293 ClassList::Static(width_class),
294 vec![HtmlDomNode::SvgNode(svg_node)],
295 Some(options),
296 None,
297 );
298
299 let mut span = span;
300 if data.paths.len() == 1 {
301 span.height = height_val;
303 span.style.insert(CssProperty::Height, make_em(height_val));
304 if data.min_width > 0.0 {
305 span.style
306 .insert(CssProperty::MinWidth, make_em(data.min_width));
307 }
308 return Ok(span.into());
309 }
310
311 span.height = height_val;
313 span.style.insert(CssProperty::Height, make_em(height_val));
314 spans.push(span.into());
315 }
316
317 let mut span = make_span("stretchy", spans, Some(options), None);
319 span.height = height_val;
320 span.style.insert(CssProperty::Height, make_em(height_val));
321 if data.min_width > 0.0 {
322 span.style
323 .insert(CssProperty::MinWidth, make_em(data.min_width));
324 }
325
326 Ok(span.into())
327 }
328}
329
330pub fn enclose_span(
332 inner: &HtmlDomNode,
333 label: &str,
334 top_pad: f64,
335 bottom_pad: f64,
336 options: &Options,
337) -> DomSpan {
338 let total_height = inner.height() + inner.depth() + top_pad + bottom_pad;
339
340 let is_box_like = label.contains("fbox") || label.contains("color");
341 if is_box_like || label == "angl" {
342 let classes = vec![Cow::Borrowed("stretchy"), Cow::Owned(label.to_owned())];
343 let mut span = make_span(classes, vec![], Some(options), None);
344
345 if label == "fbox"
346 && let Some(color) = options.get_color()
347 {
348 span.style.insert(CssProperty::BorderColor, color);
349 }
350
351 span.style
352 .insert(CssProperty::Height, make_em(total_height));
353 span.height = total_height;
354 span
355 } else {
356 let mut lines = Vec::new();
358
359 if label == "bcancel" || label == "xcancel" {
360 lines.push(LineNode {
361 attributes: [
362 ("x1".to_owned(), "0".to_owned()),
363 ("y1".to_owned(), "0".to_owned()),
364 ("x2".to_owned(), "100%".to_owned()),
365 ("y2".to_owned(), "100%".to_owned()),
366 ("stroke-width".to_owned(), "0.046em".to_owned()),
367 ]
368 .iter()
369 .cloned()
370 .collect(),
371 });
372 }
373
374 if label == "cancel" || label == "xcancel" {
375 lines.push(LineNode {
376 attributes: [
377 ("x1".to_owned(), "0".to_owned()),
378 ("y1".to_owned(), "100%".to_owned()),
379 ("x2".to_owned(), "100%".to_owned()),
380 ("y2".to_owned(), "0".to_owned()),
381 ("stroke-width".to_owned(), "0.046em".to_owned()),
382 ]
383 .iter()
384 .cloned()
385 .collect(),
386 });
387 }
388
389 let svg_attributes = [
390 ("width".to_owned(), "100%".to_owned()),
391 ("height".to_owned(), make_em(total_height)),
392 ]
393 .iter()
394 .cloned()
395 .collect();
396
397 let svg_node = SvgNode::builder()
398 .children(lines.into_iter().map(SvgChildNode::Line).collect())
399 .attributes(svg_attributes)
400 .build();
401
402 let mut span = make_svg_span(vec![], vec![svg_node], options);
403 span.style
404 .insert(CssProperty::Height, make_em(total_height));
405 span.height = total_height;
406 span
407 }
408}
409
410#[must_use]
412pub fn math_ml_node(label: &str) -> MathNode {
413 let code_point = STRETCHY_CODE_POINT
414 .get(label.trim_start_matches('\\'))
415 .unwrap_or(&" ");
416
417 let text_node = TextNode {
418 text: (*code_point).to_owned(),
419 };
420
421 let mut node = MathNode {
422 node_type: MathNodeType::Mo,
423 attributes: KeyMap::default(),
424 children: vec![text_node.into()],
425 classes: ClassList::Empty,
426 };
427
428 node.attributes
429 .insert("stretchy".to_owned(), "true".to_owned());
430 node
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use crate::dom_tree::Span;
437 use crate::parser::parse_node::ParseNodeMathOrd;
438 use crate::types::Mode;
439
440 #[test]
441 fn test_stretchy_code_point() {
442 assert_eq!(STRETCHY_CODE_POINT.get("widehat"), Some(&"^"));
443 assert_eq!(STRETCHY_CODE_POINT.get("overleftarrow"), Some(&"\u{2190}"));
444 assert_eq!(STRETCHY_CODE_POINT.get("nonexistent"), None);
445 }
446
447 #[test]
448 fn test_get_katex_images_data() {
449 let data = IMAGES_DATA;
450 assert!(data.contains_key("overrightarrow"));
451 assert!(data.contains_key("overbrace"));
452
453 let overrightarrow = data.get("overrightarrow").unwrap();
454 assert_eq!(overrightarrow.min_width, 0.888f64);
455 assert_eq!(overrightarrow.height, 522f64);
456 assert_eq!(overrightarrow.align, Some("xMaxYMin"));
457 }
458
459 #[test]
460 fn test_group_length() {
461 let simple_node = AnyParseNode::MathOrd(ParseNodeMathOrd {
463 mode: Mode::Math,
464 loc: None,
465 text: "x".into(),
466 });
467 assert_eq!(group_length(&simple_node), 1);
468 }
469
470 #[test]
471 fn test_svg_span_basic_functionality() {
472 use crate::options::Options;
473 use crate::style;
474
475 let options = Options::builder()
476 .style(style::TEXT)
477 .phantom(false)
478 .max_size(1_000_000.0)
479 .min_rule_thickness(0.04)
480 .build();
481
482 let simple_node = AnyParseNode::MathOrd(ParseNodeMathOrd {
485 mode: Mode::Math,
486 loc: None,
487 text: "x".into(),
488 });
489
490 let result = svg_span(&simple_node, &options);
492 assert!(result.is_err());
493 }
494
495 #[test]
496 fn test_math_ml_node() {
497 let node = math_ml_node("widehat");
498 assert_eq!(node.node_type, MathNodeType::Mo);
499 assert_eq!(node.attributes.get("stretchy"), Some(&"true".to_owned()));
500 assert_eq!(node.children.len(), 1);
501 }
502
503 #[test]
504 fn test_enclose_span() {
505 use crate::options::Options;
506 use crate::style;
507
508 let options = Options::builder()
509 .style(style::TEXT)
510 .phantom(false)
511 .max_size(1_000_000.0)
512 .min_rule_thickness(0.04)
513 .build();
514
515 let inner: Span<HtmlDomNode> = Span::builder()
516 .children(vec![])
517 .height(1.0)
518 .depth(0.5)
519 .build(None);
520
521 let result = enclose_span(&HtmlDomNode::DomSpan(inner), "cancel", 0.1, 0.1, &options);
522 assert!(result.height > 0.0);
523 assert!(result.style.contains_key(CssProperty::Height));
524 }
525}