Skip to main content

katex_parser/unicode/
unicode.rs

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
19/// Renders parsed KaTeX nodes as Unicode text. Nodes without a direct Unicode
20/// rendering use UnicodeMath-style function syntax so that their structure is
21/// retained in plain text.
22pub 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