1use std::rc::Rc;
2
3use crate::anvil::{
4 atom_family_name, cancel_bin_atoms, command_name, em_value, is_null_delimiter,
5 join_with_spacing, math_choice_variant, math_spacing, resolve_symbol, SpacableItem,
6};
7use crate::ast::{ColumnSeparationType, Measurement, OperatorContent, ParseNode, StyleLevel};
8use crate::unicode::block::{display_width, Block};
9use crate::unicode::block::center_text;
10use crate::unicode::config::{LineStyle, RenderConfig, RenderState};
11use crate::unicode::unicode_array::{cell_block, render_array_block, render_array_inline, render_leftright_block};
12use crate::unicode::unicode_cd::render_cd_block;
13use crate::unicode::unicode_frac::{barless_delimited_block, frac_block, render_genfrac_block, wrap_delims};
14use crate::unicode_font::unicode_font_character;
15use crate::unicode_scripts::{unicode_script_character, UnicodeScriptKind};
16
17use super::atomic::is_atomic_expression;
18
19pub fn render(nodes: &[ParseNode], config: RenderConfig) -> String {
23 render_internal(
24 nodes,
25 &RenderState {
26 style: StyleLevel::TextStyle,
27 in_display: false,
28 in_tight: false,
29 config: Rc::new(config),
30 },
31 )
32}
33
34fn render_internal(nodes: &[ParseNode], state: &RenderState) -> String {
35 render_internal_block(nodes, state).render()
36}
37
38fn render_internal_block(nodes: &[ParseNode], state: &RenderState) -> Block {
39 let items = merge_not_overlay(collect_spacable_items(nodes, state));
40 let cancelled = cancel_bin_atoms(items);
41 if cancelled.iter().any(|it| it.text.contains('\n')) {
42 join_with_block(&cancelled, state.in_tight, &state.config.spacing)
43 } else {
44 Block::from(&join_with_spacing(&cancelled, state.in_tight, &state.config.spacing))
45 }
46}
47
48fn join_with_block(items: &[SpacableItem], tight: bool, spec: &crate::anvil::SpacingSpec) -> Block {
49 let mut result = item_to_block(&items[0]);
50 for i in 1..items.len() {
51 let prev = &items[i - 1];
52 let curr = &items[i];
53 if let (Some(left), Some(right)) = (&prev.atom_type, &curr.atom_type)
54 && let Some(space) = math_spacing(left, right, tight, spec) {
55 result = result.beside(&Block::from(&space));
56 }
57 result = result.beside(&item_to_block(curr));
58 }
59 result
60}
61
62fn item_to_block(item: &SpacableItem) -> Block {
63 let mut b = Block::from(&item.text);
64 b.baseline = item.baseline;
65 b
66}
67
68pub(crate) fn render_node_internal(node: &ParseNode, state: &RenderState) -> String {
69 let slash = &state.config.fraction_slash;
70 match node {
71 ParseNode::Internal { .. } | ParseNode::EnvironmentEnd { .. } => String::new(),
72 ParseNode::Raw { string, .. } => resolve_symbol(string),
73 ParseNode::ColorToken { color, .. } => resolve_symbol(color),
74 ParseNode::TextOrd { text, .. }
75 | ParseNode::MathOrd { text, .. }
76 | ParseNode::Spacing { text, .. }
77 | ParseNode::AccentToken { text, .. }
78 | ParseNode::OperatorToken { text, .. }
79 | ParseNode::Atom { text, .. } => resolve_symbol(text),
80 ParseNode::Size { value, .. } => render_measurement(value),
81 ParseNode::Url { url, .. } => url.clone(),
82 ParseNode::Styling { body, style, .. } => render_internal(
83 body,
84 &RenderState {
85 style: *style,
86 in_display: state.in_display || *style == StyleLevel::DisplayStyle,
87 ..state.clone()
88 },
89 ),
90 ParseNode::Text { body, .. } => render_internal(
91 body,
92 &RenderState {
93 in_tight: true,
94 ..state.clone()
95 },
96 ),
97 ParseNode::MClass { body, .. }
98 | ParseNode::HBox { body, .. }
99 | ParseNode::Sizing { body, .. }
100 | ParseNode::Color { body, .. }
101 | ParseNode::Href { body, .. }
102 | ParseNode::Html { body, .. }
103 | ParseNode::OrdGroup { body, .. } => render_internal(body, state),
104 ParseNode::Sqrt {
105 body, index: None, ..
106 } => render_radical("", body, state),
107 ParseNode::Sqrt {
108 body,
109 index: Some(index),
110 ..
111 } => render_root(index, body, state),
112 ParseNode::Infix { replace_with, .. } => replace_with.clone(),
113 ParseNode::GenFrac {
114 numer,
115 denom,
116 has_bar_line: false,
117 left_delim: None,
118 right_delim: None,
119 ..
120 } => {
121 if state.in_display {
122 return render_genfrac_block(numer, denom, state, false);
123 }
124 inline_atop(numer, denom, state)
125 }
126 ParseNode::GenFrac {
127 numer,
128 denom,
129 has_bar_line: false,
130 left_delim,
131 right_delim,
132 ..
133 } if state.in_display => barless_delimited_block(
134 numer,
135 denom,
136 left_delim.as_deref(),
137 right_delim.as_deref(),
138 state,
139 )
140 .render(),
141 ParseNode::GenFrac {
142 numer,
143 denom,
144 has_bar_line: false,
145 left_delim,
146 right_delim,
147 ..
148 } => render_delimited(
149 left_delim.as_deref(),
150 inline_atop(numer, denom, state),
151 right_delim.as_deref(),
152 ),
153 ParseNode::GenFrac {
154 numer,
155 denom,
156 left_delim: None,
157 right_delim: None,
158 ..
159 } => {
160 if state.in_display {
161 return render_genfrac_block(numer, denom, state, true);
162 }
163 inline_fraction(numer, denom, slash, state)
164 }
165 ParseNode::GenFrac {
166 numer,
167 denom,
168 has_bar_line,
169 left_delim,
170 right_delim,
171 ..
172 } => {
173 if state.in_display {
174 wrap_delims(
175 &frac_block(numer, denom, state, *has_bar_line),
176 left_delim.as_deref(),
177 right_delim.as_deref(),
178 )
179 .render()
180 } else {
181 render_delimited(
182 left_delim.as_deref(),
183 inline_fraction(numer, denom, slash, state),
184 right_delim.as_deref(),
185 )
186 }
187 }
188 ParseNode::Font { font, body, .. } => render_font(font, body, state),
189 ParseNode::Op { content, .. } => render_operator_content(content, state),
190 ParseNode::OperatorName { body, .. } => render_internal(body, state),
191 ParseNode::Overline { body, .. } => {
192 format!("overline({})", render_node_internal(body, state))
193 }
194 ParseNode::Underline { body, .. } => {
195 format!("underline({})", render_node_internal(body, state))
196 }
197 ParseNode::Smash { body, .. }
198 | ParseNode::VCenter { body, .. }
199 | ParseNode::RaiseBox { body, .. }
200 | ParseNode::Lap { body, .. } => render_node_internal(body, state),
201 ParseNode::VPhantom { body, .. } => {
202 format!("vphantom({})", render_node_internal(body, state))
203 }
204 ParseNode::CdParent { fragment, .. } => render_node_internal(fragment, state),
205 ParseNode::Phantom { body, .. } => format!("phantom({})", render_internal(body, state)),
206 ParseNode::Pmb { body, .. } => format!("bold({})", render_internal(body, state)),
207 ParseNode::Rule { width, height, .. } => format!(
208 "rule({},{})",
209 render_measurement(width),
210 render_measurement(height)
211 ),
212 ParseNode::MathChoice {
213 display,
214 text,
215 script,
216 scriptscript,
217 ..
218 } => render_internal(
219 &math_choice_variant(display, text, script, scriptscript, state.style),
220 state,
221 ),
222 ParseNode::HorizBrace { label, base, .. } => {
223 format!("{}({})", command_name(label), render_node_internal(base, state))
224 }
225 ParseNode::XArrow {
226 label,
227 body,
228 below: None,
229 ..
230 } => format!("{}({})", command_name(label), render_node_internal(body, state)),
231 ParseNode::XArrow {
232 label,
233 body,
234 below: Some(below),
235 ..
236 } => format!(
237 "{}({},{})",
238 command_name(label),
239 render_node_internal(body, state),
240 render_node_internal(below, state)
241 ),
242 ParseNode::AccentUnder { label, base, .. } => format!(
243 "underaccent({},{})",
244 command_name(label),
245 render_node_internal(base, state)
246 ),
247 ParseNode::DelimSizing { delim, .. }
248 | ParseNode::LeftRightRight { delim, .. }
249 | ParseNode::Middle { delim, .. } => render_delimiter(delim),
250 ParseNode::LeftRight { body, left, right, .. } => {
251 if state.in_display {
252 render_leftright_display_block(body, left, right, state).render()
253 } else {
254 format!(
255 "{}{}{}",
256 render_delimiter(left),
257 render_internal(body, state),
258 render_delimiter(right)
259 )
260 }
261 }
262 ParseNode::Kern { dimension, .. } => render_kern(dimension),
263 ParseNode::Enclose { label, body, .. } if label == "\\fbox" => {
264 render_enclose_block(body, state).render()
265 }
266 ParseNode::Enclose { label, body, .. } => format!(
267 "{}({})",
268 command_name(label),
269 render_node_internal(body, state)
270 ),
271 ParseNode::IncludeGraphics { alt, .. } => format!("image({alt})"),
272 ParseNode::Tag { body, tag, .. } => format!(
273 "{}\t({})",
274 render_internal(body, state),
275 render_internal(tag, state)
276 ),
277 ParseNode::Array {
278 body,
279 column_separation_type: Some(ColumnSeparationType::CdSeparation),
280 ..
281 } => render_cd_block(body, state).render(),
282 ParseNode::Array {
283 body,
284 columns,
285 hlines_before_row,
286 column_separation_type,
287 ..
288 } => {
289 if state.in_display {
290 render_array_block(
291 body,
292 columns.as_deref(),
293 hlines_before_row,
294 *column_separation_type,
295 state,
296 )
297 } else {
298 render_array_inline(body, state)
299 }
300 }
301 ParseNode::CdLabel { side, label, .. } => {
302 format!("{side}({})", render_node_internal(label, state))
303 }
304 ParseNode::Cr {
305 new_line: true, ..
306 } => "\n".to_string(),
307 ParseNode::Cr { .. } => String::new(),
308 ParseNode::HtmlMathML { mathml, .. } => render_internal(mathml, state),
309 ParseNode::SupSub { base, sup, sub, .. } => render_sup_sub(base, sup, sub, state),
310 ParseNode::Accent { label, base, .. } => render_accent(label, base, state),
311 ParseNode::Verb { body, .. } => body.clone(),
312 }
313}
314
315fn merge_not_overlay(items: Vec<SpacableItem>) -> Vec<SpacableItem> {
316 let mut result: Vec<SpacableItem> = Vec::new();
317 let mut skip_next = false;
318 let mut i = 0;
319 while i < items.len() {
320 if skip_next {
321 skip_next = false;
322 i += 1;
323 continue;
324 }
325 let item = items[i].clone();
326 if item.text == "\u{338}" && i + 1 < items.len() {
327 let next = items[i + 1].clone();
328 let next_text = next.text.clone();
329 if next.atom_type.is_some()
330 && !next_text.is_empty()
331 && is_single_character_output(&next_text)
332 && !crate::unicode::unicode_width::is_zero_width_mark(
333 next_text.chars().next().unwrap() as u32,
334 )
335 {
336 result.push(SpacableItem {
337 text: format!("{next_text}\u{338}"),
338 ..next
339 });
340 skip_next = true;
341 i += 1;
342 continue;
343 }
344 }
345 result.push(item);
346 i += 1;
347 }
348 result
349}
350
351fn collect_spacable_items(nodes: &[ParseNode], state: &RenderState) -> Vec<SpacableItem> {
352 let mut acc: Vec<SpacableItem> = Vec::new();
353 for n in nodes {
354 match n {
355 ParseNode::Styling { body, style, .. } => {
356 let child_state = RenderState {
357 style: *style,
358 in_display: state.in_display || *style == StyleLevel::DisplayStyle,
359 ..state.clone()
360 };
361 acc.extend(collect_spacable_items(body, &child_state));
362 }
363 ParseNode::Color { body, .. }
364 | ParseNode::Sizing { body, .. }
365 | ParseNode::HBox { body, .. }
366 | ParseNode::Href { body, .. }
367 | ParseNode::Html { body, .. } => acc.extend(collect_spacable_items(body, state)),
368 ParseNode::Spacing { text, .. } => acc.push(SpacableItem {
369 atom_type: None,
370 text: resolve_symbol(text),
371 baseline: 0,
372 }),
373 _ => {
374 let block = content_block(n, state);
375 acc.push(SpacableItem {
376 atom_type: get_outer_atom_type(n),
377 text: block.render(),
378 baseline: block.baseline(),
379 });
380 }
381 }
382 }
383 acc
384}
385
386fn get_outer_atom_type(node: &ParseNode) -> Option<String> {
387 match node {
388 ParseNode::Atom { family, .. } => Some(atom_family_name(*family)),
389 ParseNode::MathOrd { .. }
390 | ParseNode::TextOrd { .. }
391 | ParseNode::Raw { .. }
392 | ParseNode::ColorToken { .. }
393 | ParseNode::AccentToken { .. } => Some("mord".to_string()),
394 ParseNode::OperatorToken { .. } | ParseNode::Op { .. } | ParseNode::OperatorName { .. } => {
395 Some("mop".to_string())
396 }
397 ParseNode::SupSub {
398 base: Some(base),
399 ..
400 } => {
401 if matches!(
402 **base,
403 ParseNode::Op {
404 content: OperatorContent::SymbolOperator(_),
405 ..
406 }
407 ) {
408 Some("mbig".to_string())
409 } else {
410 get_outer_atom_type(base)
411 }
412 }
413 ParseNode::SupSub { base: None, .. } => Some("mord".to_string()),
414 ParseNode::Accent { base, .. } => get_outer_atom_type(base),
415 ParseNode::Font { body, .. } => get_outer_atom_type(body),
416 ParseNode::VCenter { body, .. }
417 | ParseNode::RaiseBox { body, .. }
418 | ParseNode::Lap { body, .. }
419 | ParseNode::Smash { body, .. } => get_outer_atom_type(body),
420 ParseNode::CdParent { fragment, .. } => get_outer_atom_type(fragment),
421 ParseNode::MClass { mclass, .. } => Some(atom_family_name(*mclass)),
422 ParseNode::DelimSizing { mclass, .. } => Some(atom_family_name(*mclass)),
423 ParseNode::Pmb { mclass, .. } => Some(atom_family_name(*mclass)),
424 ParseNode::LeftRight { .. }
425 | ParseNode::LeftRightRight { .. }
426 | ParseNode::Middle { .. } => Some("minner".to_string()),
427 ParseNode::Spacing { .. } => None,
428 ParseNode::Internal { .. } | ParseNode::EnvironmentEnd { .. } | ParseNode::Infix { .. } => {
429 None
430 }
431 ParseNode::Color { .. }
432 | ParseNode::Sizing { .. }
433 | ParseNode::HBox { .. }
434 | ParseNode::Href { .. }
435 | ParseNode::Html { .. } => Some("mord".to_string()),
436 _ => Some("mord".to_string()),
437 }
438}
439
440fn render_operator_content(content: &OperatorContent, state: &RenderState) -> String {
441 match content {
442 OperatorContent::SymbolOperator(text) => resolve_symbol(text),
443 OperatorContent::NamedOperator(text) => command_name(text),
444 OperatorContent::BodyOperator(body) => render_internal(body, state),
445 }
446}
447
448pub(crate) fn render_delimiter(text: &str) -> String {
449 if is_null_delimiter(text) {
450 return String::new();
451 }
452 resolve_symbol(text)
453}
454
455fn render_font(font: &str, body: &ParseNode, state: &RenderState) -> String {
456 if font == "mathrm" {
457 return render_node_internal(body, state);
458 }
459 match font_char_sequence(font, body) {
460 Some(rendered) => rendered,
461 None => format!("{font}({})", render_node_internal(body, state)),
462 }
463}
464
465fn font_char_sequence(font: &str, node: &ParseNode) -> Option<String> {
466 let mut chars: Vec<String> = Vec::new();
467 if !collect_font_chars(node, &mut chars) {
468 return None;
469 }
470 Some(render_font_text(font, &chars))
471}
472
473fn collect_font_chars(node: &ParseNode, chars: &mut Vec<String>) -> bool {
474 match node {
475 ParseNode::MathOrd { text, .. }
476 | ParseNode::TextOrd { text, .. }
477 | ParseNode::Atom { text, .. } => {
478 let rendered = resolve_symbol(text);
479 chars.extend(rendered.chars().map(|ch| ch.to_string()));
480 true
481 }
482 ParseNode::OrdGroup { body, .. } => {
483 for child in body {
484 if !collect_font_chars(child, chars) {
485 return false;
486 }
487 }
488 true
489 }
490 _ => false,
491 }
492}
493
494fn render_font_text(font: &str, chars: &[String]) -> String {
495 let mut result = String::new();
496 let mut failed: Vec<String> = Vec::new();
497 for s in chars {
498 match unicode_font_character(font, s) {
499 Some(mapped) => {
500 if !failed.is_empty() {
501 result.push_str(&font_fallback(font, &failed));
502 failed.clear();
503 }
504 result.push_str(&mapped);
505 }
506 None if is_font_letter(s) => failed.push(s.clone()),
507 None => {
508 if !failed.is_empty() {
509 result.push_str(&font_fallback(font, &failed));
510 failed.clear();
511 }
512 result.push_str(s);
513 }
514 }
515 }
516 if !failed.is_empty() {
517 result.push_str(&font_fallback(font, &failed));
518 }
519 result
520}
521
522fn is_font_letter(s: &str) -> bool {
523 let mut chars = s.chars();
524 let Some(c) = chars.next() else {
525 return false;
526 };
527 if chars.next().is_some() {
528 return false;
529 }
530 c.is_ascii_uppercase() || c.is_ascii_lowercase() || ('Α'..='ω').contains(&c)
531}
532
533fn font_fallback(font: &str, chars: &[String]) -> String {
534 let text: String = chars.concat();
535 format!("{font}({text})")
536}
537
538fn inline_atop(numer: &ParseNode, denom: &ParseNode, state: &RenderState) -> String {
539 let num_state = RenderState {
540 in_tight: true,
541 ..state.clone()
542 };
543 let den_state = RenderState {
544 in_tight: true,
545 ..state.clone()
546 };
547 format!(
548 "{},{}",
549 render_node_internal(numer, &num_state),
550 render_node_internal(denom, &den_state)
551 )
552}
553
554fn inline_fraction(numer: &ParseNode, denom: &ParseNode, slash: &str, state: &RenderState) -> String {
555 let num_state = RenderState {
556 in_tight: true,
557 ..state.clone()
558 };
559 let den_state = RenderState {
560 in_tight: true,
561 ..state.clone()
562 };
563 format!(
564 "{}{}{}",
565 render_operand(numer, &num_state),
566 slash,
567 render_operand(denom, &den_state)
568 )
569}
570
571fn render_leftright_display_block(
572 body: &[ParseNode],
573 left: &str,
574 right: &str,
575 state: &RenderState,
576) -> Block {
577 match body {
578 [ParseNode::Array {
579 body: arr_body,
580 columns,
581 hlines_before_row,
582 column_separation_type,
583 ..
584 }] => render_leftright_block(
585 &render_delimiter(left),
586 &render_delimiter(right),
587 arr_body,
588 columns.as_deref(),
589 hlines_before_row,
590 *column_separation_type,
591 state,
592 ),
593 _ => wrap_delims(&render_internal_block(body, state), Some(left), Some(right)),
594 }
595}
596
597fn render_delimited(left: Option<&str>, content: String, right: Option<&str>) -> String {
598 let left = left.map(render_delimiter).unwrap_or_default();
599 let right = right.map(render_delimiter).unwrap_or_default();
600 format!("{left}{content}{right}")
601}
602
603fn render_enclose_block(body: &ParseNode, state: &RenderState) -> Block {
604 enclose_box(
605 &render_internal_block(std::slice::from_ref(body), state),
606 state.config.line_style,
607 )
608}
609
610fn enclose_box(block: &Block, style: LineStyle) -> Block {
611 let w = block.width;
612 let top = box_border(style, w, true);
613 let bottom = box_border(style, w, false);
614 let side = box_side(style);
615 let middle: Vec<String> = block.lines.iter().map(|l| format!("{side}{l}{side}")).collect();
616 let mut lines = Vec::with_capacity(middle.len() + 2);
617 lines.push(top);
618 lines.extend(middle);
619 lines.push(bottom);
620 Block {
621 lines,
622 width: w + 2,
623 baseline: block.baseline + 1,
624 }
625}
626
627fn box_border(style: LineStyle, w: usize, is_top: bool) -> String {
628 match style {
629 LineStyle::Ascii => "-".repeat(w + 2),
630 LineStyle::Unicode => {
631 if is_top {
632 format!("┌{}┐", "─".repeat(w))
633 } else {
634 format!("└{}┘", "─".repeat(w))
635 }
636 }
637 }
638}
639
640fn box_side(style: LineStyle) -> String {
641 match style {
642 LineStyle::Ascii => "|".to_string(),
643 LineStyle::Unicode => "│".to_string(),
644 }
645}
646
647fn render_measurement(measurement: &Measurement) -> String {
648 format!("{}{}", measurement.number, measurement.unit)
649}
650
651fn render_kern(dimension: &Measurement) -> String {
652 if dimension.unit != "em" && dimension.unit != "mu" {
653 return format!("kern({})", render_measurement(dimension));
654 }
655 let em = em_value(dimension).unwrap_or_default();
656 if em <= 0.0 {
657 return String::new();
658 }
659 let n = (em * 2.0).round();
660 let count = if n < 1.0 { 1 } else { n as usize };
661 " ".repeat(count)
662}
663
664fn render_root(index: &ParseNode, body: &ParseNode, state: &RenderState) -> String {
665 render_root_block(index, body, state).render()
666}
667
668fn render_root_block(index: &ParseNode, body: &ParseNode, state: &RenderState) -> Block {
669 let tight_state = RenderState {
670 in_display: false,
671 in_tight: true,
672 ..state.clone()
673 };
674 let index_text = render_node_internal(index, &tight_state);
675 match unicode_script(&index_text, UnicodeScriptKind::UnicodeSuperscript) {
676 Some(prefix) => render_radical_block(&prefix, body, state),
677 None => Block::from(&format!("root({index_text},"))
678 .beside(&render_operand_block(body, state))
679 .beside(&Block::from(")")),
680 }
681}
682
683fn render_radical(prefix: &str, body: &ParseNode, state: &RenderState) -> String {
684 render_radical_block(prefix, body, state).render()
685}
686
687fn render_radical_block(prefix: &str, body: &ParseNode, state: &RenderState) -> Block {
688 Block::from(&format!("{prefix}√")).beside(&render_operand_block(body, state))
689}
690
691fn render_operand(node: &ParseNode, state: &RenderState) -> String {
692 render_operand_block(node, state).render()
693}
694
695fn render_operand_block(node: &ParseNode, state: &RenderState) -> Block {
696 let content = content_block(node, state);
697 if is_atomic_expression(node) {
698 content
699 } else {
700 wrap_delims(&content, Some("("), Some(")"))
701 }
702}
703
704fn render_sup_sub(
705 base: &Option<Box<ParseNode>>,
706 sup: &Option<Box<ParseNode>>,
707 sub: &Option<Box<ParseNode>>,
708 state: &RenderState,
709) -> String {
710 render_sup_sub_block(base, sup, sub, state).render()
711}
712
713fn render_sup_sub_block(
714 base: &Option<Box<ParseNode>>,
715 sup: &Option<Box<ParseNode>>,
716 sub: &Option<Box<ParseNode>>,
717 state: &RenderState,
718) -> Block {
719 if let Some(b) = base
720 && operator_uses_limits(b, state) {
721 return render_limits_block(b, sup, sub, state);
722 }
723 let base_block = base
724 .as_ref()
725 .map(|b| render_operand_block(b, state))
726 .unwrap_or_else(Block::empty);
727 let sub_text = render_script(sub, UnicodeScriptKind::UnicodeSubscript, "_", state);
728 let sup_text = render_script(sup, UnicodeScriptKind::UnicodeSuperscript, "^", state);
729 base_block.beside(&Block::from(&format!("{sub_text}{sup_text}")))
730}
731
732fn operator_uses_limits(base: &ParseNode, state: &RenderState) -> bool {
733 match base {
734 ParseNode::Op {
735 limits: true,
736 always_handle_sup_sub,
737 ..
738 } => state.in_display || *always_handle_sup_sub,
739 ParseNode::OperatorName {
740 always_handle_sup_sub: true,
741 ..
742 } => true,
743 _ => false,
744 }
745}
746
747fn render_limits_block(
748 base: &ParseNode,
749 sup: &Option<Box<ParseNode>>,
750 sub: &Option<Box<ParseNode>>,
751 state: &RenderState,
752) -> Block {
753 let tight_state = RenderState {
754 in_display: false,
755 in_tight: true,
756 ..state.clone()
757 };
758 let op_text = render_node_internal(base, &tight_state);
759 let sup_text = sup
760 .as_ref()
761 .map(|s| render_node_internal(s, &tight_state))
762 .unwrap_or_default();
763 let sub_text = sub
764 .as_ref()
765 .map(|s| render_node_internal(s, &tight_state))
766 .unwrap_or_default();
767 let w = display_width(&op_text)
768 .max(display_width(&sup_text))
769 .max(display_width(&sub_text));
770 let mut lines: Vec<String> = Vec::new();
771 if !sup_text.is_empty() {
772 lines.push(center_text(&sup_text, w));
773 }
774 lines.push(center_text(&op_text, w));
775 let baseline = lines.len() - 1;
776 if !sub_text.is_empty() {
777 lines.push(center_text(&sub_text, w));
778 }
779 Block {
780 lines,
781 width: w,
782 baseline,
783 }
784}
785
786fn render_script(
787 node: &Option<Box<ParseNode>>,
788 kind: UnicodeScriptKind,
789 fallback_prefix: &str,
790 state: &RenderState,
791) -> String {
792 let tight_state = RenderState {
793 in_display: false,
794 in_tight: true,
795 ..state.clone()
796 };
797 let Some(node) = node else {
798 return String::new();
799 };
800 let text = render_node_internal(node, &tight_state);
801 let default = || format!("{fallback_prefix}{}", render_operand(node, &tight_state));
802 match (kind, split_prime_prefix(&text)) {
803 (UnicodeScriptKind::UnicodeSuperscript, Some((primes, rest))) => {
804 match unicode_script(&rest, kind) {
805 Some(mapped) => format!("{primes}{mapped}"),
806 None => default(),
807 }
808 }
809 _ => unicode_script(&text, kind).unwrap_or_else(default),
810 }
811}
812
813fn split_prime_prefix(text: &str) -> Option<(String, String)> {
814 let mut count = 0;
815 for c in text.chars() {
816 if c == '′' {
817 count += 1;
818 } else {
819 break;
820 }
821 }
822 if count == 0 {
823 return None;
824 }
825 let primes = "'".repeat(count);
826 let rest: String = text.chars().skip(count).collect();
827 Some((primes, rest))
828}
829
830fn render_function_block(name: &str, body: &Block) -> Block {
831 Block::from(&format!("{name}(")).beside(body).beside(&Block::from(")"))
832}
833
834fn render_font_block(font: &str, body: &ParseNode, state: &RenderState) -> Block {
835 if font == "mathrm" {
836 return content_block(body, state);
837 }
838 match font_char_sequence(font, body) {
839 Some(rendered) => Block::from(&rendered),
840 None => render_function_block(font, &content_block(body, state)),
841 }
842}
843
844fn render_operator_content_block(content: &OperatorContent, state: &RenderState) -> Block {
845 match content {
846 OperatorContent::SymbolOperator(text) => Block::from(&resolve_symbol(text)),
847 OperatorContent::NamedOperator(text) => Block::from(&command_name(text)),
848 OperatorContent::BodyOperator(body) => content_body_block(body, state),
849 }
850}
851
852pub(crate) fn content_block(node: &ParseNode, state: &RenderState) -> Block {
853 match node {
854 ParseNode::Styling { body, style, .. } => content_body_block(
855 body,
856 &RenderState {
857 style: *style,
858 in_display: state.in_display || *style == StyleLevel::DisplayStyle,
859 ..state.clone()
860 },
861 ),
862 ParseNode::Text { body, .. } => content_body_block(
863 body,
864 &RenderState {
865 in_tight: true,
866 ..state.clone()
867 },
868 ),
869 ParseNode::OrdGroup { body, .. }
870 | ParseNode::MClass { body, .. }
871 | ParseNode::HBox { body, .. }
872 | ParseNode::Sizing { body, .. }
873 | ParseNode::Color { body, .. }
874 | ParseNode::Href { body, .. }
875 | ParseNode::Html { body, .. } => content_body_block(body, state),
876 ParseNode::Smash { body, .. }
877 | ParseNode::VCenter { body, .. }
878 | ParseNode::RaiseBox { body, .. }
879 | ParseNode::Lap { body, .. } => content_block(body, state),
880 ParseNode::CdParent { fragment, .. } => content_block(fragment, state),
881 ParseNode::GenFrac {
882 numer,
883 denom,
884 has_bar_line: false,
885 left_delim: None,
886 right_delim: None,
887 ..
888 } if state.in_display => frac_block(numer, denom, state, false),
889 ParseNode::GenFrac {
890 numer,
891 denom,
892 has_bar_line: false,
893 left_delim,
894 right_delim,
895 ..
896 } if state.in_display => barless_delimited_block(
897 numer,
898 denom,
899 left_delim.as_deref(),
900 right_delim.as_deref(),
901 state,
902 ),
903 ParseNode::GenFrac {
904 numer,
905 denom,
906 left_delim: None,
907 right_delim: None,
908 ..
909 } if state.in_display => frac_block(numer, denom, state, true),
910 ParseNode::GenFrac {
911 numer,
912 denom,
913 has_bar_line,
914 left_delim,
915 right_delim,
916 ..
917 } if state.in_display => wrap_delims(
918 &frac_block(numer, denom, state, *has_bar_line),
919 left_delim.as_deref(),
920 right_delim.as_deref(),
921 ),
922 ParseNode::LeftRight { body, left, right, .. } if state.in_display => {
923 render_leftright_display_block(body, left, right, state)
924 }
925 ParseNode::Array {
926 body,
927 column_separation_type: Some(ColumnSeparationType::CdSeparation),
928 ..
929 } => render_cd_block(body, state),
930 ParseNode::Array {
931 body,
932 columns,
933 hlines_before_row,
934 column_separation_type,
935 ..
936 } if state.in_display => cell_block(
937 body,
938 columns.as_deref(),
939 hlines_before_row,
940 *column_separation_type,
941 state,
942 ),
943 ParseNode::Array { .. } => Block::from(&render_node_internal(node, state)),
944 ParseNode::Enclose { label, body, .. } if label == "\\fbox" => {
945 render_enclose_block(body, state)
946 }
947 ParseNode::Sqrt {
948 body, index: None, ..
949 } => render_radical_block("", body, state),
950 ParseNode::Sqrt {
951 body,
952 index: Some(index),
953 ..
954 } => render_root_block(index, body, state),
955 ParseNode::SupSub { base, sup, sub, .. } => render_sup_sub_block(base, sup, sub, state),
956 ParseNode::Overline { body, .. } => {
957 render_function_block("overline", &content_block(body, state))
958 }
959 ParseNode::Underline { body, .. } => {
960 render_function_block("underline", &content_block(body, state))
961 }
962 ParseNode::VPhantom { body, .. } => {
963 render_function_block("vphantom", &content_block(body, state))
964 }
965 ParseNode::Phantom { body, .. } => {
966 render_function_block("phantom", &content_body_block(body, state))
967 }
968 ParseNode::Pmb { body, .. } => render_function_block("bold", &content_body_block(body, state)),
969 ParseNode::Enclose { label, body, .. } => {
970 render_function_block(&command_name(label), &content_block(body, state))
971 }
972 ParseNode::HorizBrace { label, base, .. } => {
973 render_function_block(&command_name(label), &content_block(base, state))
974 }
975 ParseNode::XArrow {
976 label,
977 body,
978 below: None,
979 ..
980 } => render_function_block(&command_name(label), &content_block(body, state)),
981 ParseNode::XArrow {
982 label,
983 body,
984 below: Some(below),
985 ..
986 } => render_function_block(
987 &command_name(label),
988 &content_block(body, state)
989 .beside(&Block::from(","))
990 .beside(&content_block(below, state)),
991 ),
992 ParseNode::AccentUnder { label, base, .. } => Block::from(&format!(
993 "underaccent({},",
994 command_name(label)
995 ))
996 .beside(&content_block(base, state))
997 .beside(&Block::from(")")),
998 ParseNode::Accent { label, base, .. } => render_accent_block(label, base, state),
999 ParseNode::Font { font, body, .. } => render_font_block(font, body, state),
1000 ParseNode::Op { content, .. } => render_operator_content_block(content, state),
1001 ParseNode::OperatorName { body, .. } => content_body_block(body, state),
1002 ParseNode::MathChoice {
1003 display,
1004 text,
1005 script,
1006 scriptscript,
1007 ..
1008 } => content_body_block(
1009 &math_choice_variant(display, text, script, scriptscript, state.style),
1010 state,
1011 ),
1012 ParseNode::Tag { body, tag, .. } => content_body_block(body, state)
1013 .beside(&Block::from("\t"))
1014 .beside(&content_body_block(tag, state)),
1015 ParseNode::HtmlMathML { mathml, .. } => content_body_block(mathml, state),
1016 ParseNode::Internal { .. } | ParseNode::EnvironmentEnd { .. } => Block::empty(),
1017 _ => Block::from(&render_node_internal(node, state)),
1018 }
1019}
1020
1021fn content_body_block(body: &[ParseNode], state: &RenderState) -> Block {
1022 match body {
1023 [] => Block::empty(),
1024 [single] => content_block(single, state),
1025 _ => render_internal_block(body, state),
1026 }
1027}
1028
1029fn unicode_script(text: &str, kind: UnicodeScriptKind) -> Option<String> {
1030 let mut acc: Option<String> = Some(String::new());
1031 for character in text.chars() {
1032 let ch = character.to_string();
1033 acc = match acc {
1034 None => None,
1035 Some(s) => unicode_script_character(kind, &ch).map(|script| format!("{s}{script}")),
1036 };
1037 }
1038 acc
1039}
1040
1041fn render_accent(label: &str, base: &ParseNode, state: &RenderState) -> String {
1042 render_accent_block(label, base, state).render()
1043}
1044
1045fn render_accent_block(label: &str, base: &ParseNode, state: &RenderState) -> Block {
1046 let mark = match label {
1047 "\\acute" => Some("\u{301}"),
1048 "\\grave" => Some("\u{300}"),
1049 "\\ddot" => Some("\u{308}"),
1050 "\\tilde" => Some("\u{303}"),
1051 "\\bar" => Some("\u{304}"),
1052 "\\breve" => Some("\u{306}"),
1053 "\\check" => Some("\u{30C}"),
1054 "\\hat" => Some("\u{302}"),
1055 "\\dot" => Some("\u{307}"),
1056 "\\mathring" => Some("\u{30A}"),
1057 "\\vec" => Some("\u{20D7}"),
1058 _ => None,
1059 };
1060 let rendered_base = render_node_internal(base, state);
1061 match mark {
1062 Some(mark) if is_single_character_output(&rendered_base) => {
1063 Block::from(&format!("{rendered_base}{mark}"))
1064 }
1065 _ => render_function_block(&command_name(label), &content_block(base, state)),
1066 }
1067}
1068
1069fn is_single_character_output(s: &str) -> bool {
1070 s.chars().count() == 1
1071}
1072