Skip to main content

mathtex_editor_core/
model.rs

1//! The editable tree: sequences of nodes with stable slotmap ids and parent links.
2
3use serde::{Deserialize, Serialize};
4use slotmap::{new_key_type, SlotMap};
5
6new_key_type! {
7    /// Stable identity of a node.
8    pub(crate) struct NodeId;
9    /// Stable identity of an editable sequence.
10    pub(crate) struct SeqId;
11}
12
13/// The editable math tree.
14#[derive(Debug, Clone)]
15pub(crate) struct Tree {
16    pub(crate) nodes: SlotMap<NodeId, Node>,
17    pub(crate) seqs: SlotMap<SeqId, Seq>,
18    pub(crate) root: SeqId,
19    // Bumped by every primitive that changes content, so callers detect no-op commands.
20    pub(crate) edits: u64,
21}
22
23impl Tree {
24    pub(crate) fn new() -> Self {
25        let mut seqs: SlotMap<SeqId, Seq> = SlotMap::with_key();
26        let root = seqs.insert(Seq { parent: None, items: Vec::new() });
27        Self { nodes: SlotMap::with_key(), seqs, root, edits: 0 }
28    }
29
30    pub(crate) fn root(&self) -> SeqId {
31        self.root
32    }
33
34    pub(crate) fn kind(&self, id: NodeId) -> Option<&Kind> {
35        self.nodes.get(id).map(|n| &n.kind)
36    }
37
38    pub(crate) fn items(&self, id: SeqId) -> &[NodeId] {
39        self.seqs.get(id).map_or(&[], |s| s.items.as_slice())
40    }
41
42    pub(crate) fn len(&self, id: SeqId) -> usize {
43        self.items(id).len()
44    }
45
46    pub(crate) fn is_empty(&self, id: SeqId) -> bool {
47        self.items(id).is_empty()
48    }
49
50    pub(crate) fn touch(&mut self) {
51        self.edits += 1;
52    }
53
54    /// The node that owns this sequence as a slot, or `None` for the root.
55    pub(crate) fn seq_parent(&self, id: SeqId) -> Option<NodeId> {
56        self.seqs.get(id).and_then(|s| s.parent)
57    }
58
59    /// If `seq` is the base slot of a Script, the owning Script node.
60    pub(crate) fn script_base_node(&self, seq: SeqId) -> Option<NodeId> {
61        let parent = self.seq_parent(seq)?;
62        match self.kind(parent) {
63            Some(Kind::Script { base, .. }) if *base == seq => Some(parent),
64            _ => None,
65        }
66    }
67
68    /// Whether `seq` is the content of a `\text{}` node, where only atoms may live.
69    pub(crate) fn is_text_slot(&self, seq: SeqId) -> bool {
70        let Some(parent) = self.seq_parent(seq) else {
71            return false;
72        };
73        matches!(self.kind(parent), Some(Kind::Styled { variant: Variant::Text, .. }))
74    }
75
76    /// The sequence and index where this node currently lives.
77    pub(crate) fn index_in_parent(&self, node: NodeId) -> Option<(SeqId, usize)> {
78        let parent = self.nodes.get(node)?.parent;
79        let idx = self.seqs.get(parent)?.items.iter().position(|&n| n == node)?;
80        Some((parent, idx))
81    }
82
83    /// The gap just before the node that owns `seq`, or `None` for the root.
84    pub(crate) fn before_parent(&self, seq: SeqId) -> Option<Cursor> {
85        let node = self.seq_parent(seq)?;
86        let (seq, index) = self.index_in_parent(node)?;
87        Some(Cursor { seq, index })
88    }
89
90    /// Number of slots enclosing `seq`, zero for the root.
91    pub(crate) fn seq_depth(&self, seq: SeqId) -> usize {
92        let mut depth = 0;
93        let mut cur = seq;
94        while let Some(node) = self.seq_parent(cur) {
95            depth += 1;
96            let Some(n) = self.nodes.get(node) else { break };
97            cur = n.parent;
98        }
99        depth
100    }
101
102    /// Slot levels a node adds below its own sequence, zero for leaves.
103    pub(crate) fn node_height(&self, node: NodeId) -> usize {
104        self.child_seqs(node)
105            .into_iter()
106            .map(|s| 1 + self.seq_height(s))
107            .max()
108            .unwrap_or(0)
109    }
110
111    pub(crate) fn seq_height(&self, seq: SeqId) -> usize {
112        self.items(seq).iter().map(|&n| self.node_height(n)).max().unwrap_or(0)
113    }
114
115    /// All present slot sequences of a node in canonical navigation and ownership order.
116    pub(crate) fn child_seqs(&self, node: NodeId) -> Vec<SeqId> {
117        let Some(n) = self.nodes.get(node) else {
118            return Vec::new();
119        };
120        match &n.kind {
121            Kind::Atom(_) | Kind::HostBox { .. } => Vec::new(),
122            Kind::Frac { num, den, .. } => vec![*num, *den],
123            Kind::Script { base, sub, sup } => {
124                let mut v = vec![*base];
125                v.extend(sub.iter().copied());
126                v.extend(sup.iter().copied());
127                v
128            }
129            // Upper first so leftward navigation enters the lower limit before the upper one.
130            Kind::BigOp { upper, lower, .. } => vec![*upper, *lower],
131            Kind::Sqrt { index, radicand } => vec![*index, *radicand],
132            Kind::Delim { body, .. } => vec![*body],
133            Kind::Accent { base, .. } => vec![*base],
134            Kind::UnderOver { base, over, under, .. } => {
135                let mut v = Vec::new();
136                v.extend(over.iter().copied());
137                v.push(*base);
138                v.extend(under.iter().copied());
139                v
140            }
141            Kind::Styled { content, .. } => vec![*content],
142            Kind::Matrix { rows, .. } => rows.iter().flatten().copied().collect(),
143            Kind::Ratio { left, right } => vec![*left, *right],
144            // Like a Script, the body comes first and the limits follow lower then upper.
145            Kind::EvalBar { body, lower, upper } => vec![*body, *lower, *upper],
146            Kind::LabeledArrow { over, under, .. } => vec![*over, *under],
147        }
148    }
149}
150
151/// An ordered run of nodes with an optional owning node.
152#[derive(Debug, Clone)]
153pub(crate) struct Seq {
154    pub(crate) parent: Option<NodeId>,
155    pub(crate) items: Vec<NodeId>,
156}
157
158/// A node, which always lives inside a sequence.
159#[derive(Debug, Clone)]
160pub(crate) struct Node {
161    pub(crate) parent: SeqId,
162    pub(crate) kind: Kind,
163}
164
165/// Node payloads, every editable slot is a `SeqId`.
166#[derive(Debug, Clone)]
167pub(crate) enum Kind {
168    Atom(Symbol),
169    HostBox { token: u32 },
170    Frac { num: SeqId, den: SeqId, style: FracStyle },
171    Script { base: SeqId, sub: Option<SeqId>, sup: Option<SeqId> },
172    BigOp { op: Symbol, lower: SeqId, upper: SeqId },
173    Sqrt { index: SeqId, radicand: SeqId },
174    Delim { open: char, close: char, body: SeqId },
175    Accent { mark: Mark, base: SeqId },
176    UnderOver { base: SeqId, over: Option<SeqId>, under: Option<SeqId>, over_deco: Deco, under_deco: Deco },
177    Styled { variant: Variant, content: SeqId },
178    Matrix { env: MatrixEnv, rows: Vec<Vec<SeqId>> },
179    Ratio { left: SeqId, right: SeqId },
180    EvalBar { body: SeqId, lower: SeqId, upper: SeqId },
181    LabeledArrow { arrow: Arrow, over: SeqId, under: SeqId },
182}
183
184/// A leaf token plus its math class for editing heuristics.
185#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
186pub struct Symbol {
187    /// The math mode LaTeX emitted for this symbol.
188    pub latex: String,
189    /// The math class used by editing heuristics.
190    pub class: MathClass,
191}
192
193impl Symbol {
194    /// Build a symbol from a typed character, escaping TeX specials, `None` for control characters.
195    pub fn from_char(c: char) -> Option<Self> {
196        if c.is_control() {
197            return None;
198        }
199        let latex = match c {
200            '%' | '#' | '&' | '$' | '_' | '{' | '}' => format!("\\{c}"),
201            '~' => "\\sim".to_string(),
202            '\\' => "\\backslash".to_string(),
203            '^' => "\\text{\\textasciicircum}".to_string(),
204            '\'' => "\\prime".to_string(),
205            ' ' => "\\ ".to_string(),
206            // Text mode carries letters that XeTeX math mode will not render directly.
207            other if needs_text_mode(other) => format!("\\text{{{other}}}"),
208            other => other.to_string(),
209        };
210        let class = latex_class(&latex);
211        Some(Symbol { latex, class })
212    }
213
214    /// A symbol for LaTeX such as `\leq`, classed by the same table as [`Symbol::from_char`].
215    pub fn from_latex(latex: &str) -> Self {
216        Symbol { latex: latex.to_string(), class: latex_class(latex) }
217    }
218}
219
220/// A letter that XeTeX math mode won't render directly.
221fn needs_text_mode(c: char) -> bool {
222    let greek = ('\u{0370}'..='\u{03FF}').contains(&c) || ('\u{1F00}'..='\u{1FFF}').contains(&c);
223    // Letterlike symbols such as โ„ and the math alphanumerics such as ๐‘ฅ are math characters already.
224    let letterlike = ('\u{2100}'..='\u{214F}').contains(&c);
225    let math_alnum = ('\u{1D400}'..='\u{1D7FF}').contains(&c);
226    c.is_alphabetic() && !c.is_ascii() && !greek && !letterlike && !math_alnum
227}
228
229/// Default math class for a typed character.
230fn char_class(c: char) -> MathClass {
231    match c {
232        '+' | '-' | '*' | '\u{2212}' | 'ยฑ' | 'โˆ“' | 'ร—' | 'รท' | 'ยท' | 'โˆ˜' | 'โˆ™' => MathClass::Bin,
233        '=' | '<' | '>' | 'โ‰ค' | 'โ‰ฅ' | 'โ‰ ' | 'โ‰ˆ' | 'โ‰ก' | 'โˆผ' | 'โ‰…' | 'โˆ' | 'โ†’' | 'โ†' | 'โ‡’' | 'โ‡' | 'โ‡”'
234        | 'โˆˆ' | 'โˆ‰' | 'โŠ‚' | 'โІ' | 'โŠƒ' | 'โЇ' => MathClass::Rel,
235        ',' | ';' | '.' | ':' => MathClass::Punct,
236        '(' | '[' | '{' | 'โŸจ' | 'โŒˆ' | 'โŒŠ' => MathClass::Open,
237        ')' | ']' | '}' | 'โŸฉ' | 'โŒ‰' | 'โŒ‹' => MathClass::Close,
238        _ => MathClass::Ord,
239    }
240}
241
242/// Default class of a symbol's LaTeX, the one table behind every `Symbol` constructor.
243fn latex_class(latex: &str) -> MathClass {
244    let mut chars = latex.chars();
245    if let (Some(c), None) = (chars.next(), chars.next()) {
246        return char_class(c);
247    }
248    let Some(name) = latex.strip_prefix('\\') else {
249        return MathClass::Ord;
250    };
251    if OPERATOR_NAMES.contains(&name) || name.starts_with("operatorname{") {
252        return MathClass::Op;
253    }
254    if name.starts_with("mathbin{") {
255        return MathClass::Bin;
256    }
257    if name.starts_with("mathrel{") {
258        return MathClass::Rel;
259    }
260    match name {
261        "{" | "langle" | "lceil" | "lfloor" => MathClass::Open,
262        "}" | "rangle" | "rceil" | "rfloor" => MathClass::Close,
263        "leq" | "le" | "geq" | "ge" | "neq" | "ne" | "equiv" | "approx" | "cong" | "sim" | "simeq" | "propto"
264        | "to" | "gets" | "mapsto" | "implies" | "iff" | "in" | "notin" | "ni" | "subset" | "subseteq"
265        | "supset" | "supseteq" | "rightarrow" | "leftarrow" | "leftrightarrow" | "Rightarrow" | "Leftarrow"
266        | "Leftrightarrow" | "Longrightarrow" | "Longleftarrow" | "perp" | "parallel" | "mid" | "ll" | "gg" | "uparrow"
267        | "downarrow" | "nearrow" | "searrow" | "lesssim" | "gtrsim" | "asymp" | "nmid" | "subsetneq" | "supsetneq"
268        | "nsubseteq" | "vdash" | "models" | "coloneq" | "hookrightarrow" | "twoheadrightarrow" | "prec" | "preceq"
269        | "succ" | "succeq" | "trianglelefteq" | "lneq" => MathClass::Rel,
270        "pm" | "mp" | "times" | "div" | "cdot" | "ast" | "star" | "cup" | "cap" | "setminus" | "circ" | "oplus"
271        | "otimes" | "wedge" | "vee" | "land" | "lor" | "bmod" | "sqcup" | "triangleleft" | "ltimes" | "rtimes"
272        | "wr" => MathClass::Bin,
273        "colon" => MathClass::Punct,
274        "cdots" | "ldots" | "dots" | "vdots" | "ddots" => MathClass::Inner,
275        "sum" | "prod" | "coprod" | "int" | "iint" | "iiint" | "oint" | "bigcup" | "bigcap" | "bigsqcup" | "biguplus"
276        | "bigoplus" | "bigotimes" | "bigodot" | "bigvee" | "bigwedge" => MathClass::Op,
277        _ => MathClass::Ord,
278    }
279}
280
281/// Operator names that typeset upright, such as `\sin`, classed `Op`.
282const OPERATOR_NAMES: &[&str] = &[
283    "sin", "cos", "tan", "cot", "sec", "csc", "sinh", "cosh", "tanh", "arcsin", "arccos", "arctan", "log", "ln",
284    "exp", "lim", "max", "min", "sup", "inf", "gcd", "det", "dim", "ker", "arg", "deg", "hom", "limsup", "liminf",
285];
286
287/// Math atom classification used by editing heuristics.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum MathClass {
291    /// Ordinary math atom.
292    Ord,
293    /// Operator atom.
294    Op,
295    /// Binary operator atom.
296    Bin,
297    /// Relation atom.
298    Rel,
299    /// Opening delimiter atom.
300    Open,
301    /// Closing delimiter atom.
302    Close,
303    /// Punctuation atom.
304    Punct,
305    /// Inner atom.
306    Inner,
307}
308
309/// Fraction rendering style.
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
311#[serde(rename_all = "snake_case")]
312pub enum FracStyle {
313    /// Standard fraction bar style.
314    Bar,
315    /// Display fraction style.
316    Display,
317    /// Text fraction style.
318    Text,
319    /// Binomial fraction style.
320    Binom,
321    /// Fraction layout without a bar.
322    Atop,
323}
324
325/// Script slot selector.
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
327#[serde(rename_all = "snake_case")]
328pub enum ScriptSlot {
329    /// Subscript slot.
330    Sub,
331    /// Superscript slot.
332    Sup,
333}
334
335/// Accent mark type.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338pub enum Mark {
339    /// Hat accent.
340    Hat,
341    /// Vector accent.
342    Vec,
343    /// Bar accent.
344    Bar,
345    /// Tilde accent.
346    Tilde,
347    /// Dot accent.
348    Dot,
349    /// Double dot accent.
350    Ddot,
351    /// Wide hat accent.
352    Widehat,
353    /// Wide tilde accent.
354    Widetilde,
355    /// Overline accent.
356    Overline,
357    /// Underline accent.
358    Underline,
359    /// Check accent.
360    Check,
361    /// Breve accent.
362    Breve,
363    /// Ring accent.
364    Ring,
365}
366
367/// Decoration drawn between an under or over label and its base.
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
369#[serde(rename_all = "snake_case")]
370pub enum Deco {
371    /// No decoration, the label sits directly above or below.
372    None,
373    /// A horizontal brace.
374    Brace,
375    /// A rightward arrow.
376    Arrow,
377    /// A horizontal line.
378    Line,
379}
380
381/// Font or text variant.
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub enum Variant {
385    /// Normal math style.
386    Normal,
387    /// Bold math style.
388    Bold,
389    /// Blackboard bold math style.
390    Blackboard,
391    /// Calligraphic math style.
392    Calligraphic,
393    /// Fraktur math style.
394    Fraktur,
395    /// Roman math style.
396    Roman,
397    /// Sans serif math style.
398    SansSerif,
399    /// Typewriter math style.
400    Typewriter,
401    /// Text mode, whose slot holds only atoms.
402    Text,
403    /// Operator name style.
404    OperatorName,
405}
406
407/// The extensible arrow a labeled arrow draws between its labels.
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
409#[serde(rename_all = "snake_case")]
410pub enum Arrow {
411    /// Rightward arrow.
412    Right,
413    /// Leftward arrow.
414    Left,
415}
416
417/// Matrix environment type.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
419#[serde(rename_all = "snake_case")]
420pub enum MatrixEnv {
421    /// Plain matrix environment.
422    Matrix,
423    /// Parenthesized matrix environment.
424    Pmatrix,
425    /// Bracketed matrix environment.
426    Bmatrix,
427    /// Vertically barred matrix environment.
428    Vmatrix,
429    /// Cases environment.
430    Cases,
431    /// Aligned environment.
432    Aligned,
433    /// Array environment with centered columns.
434    Array,
435}
436
437/// Spec for inserting an under or over construct.
438#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
439pub struct UnderOverSpec {
440    /// Whether to include an over slot.
441    pub over: bool,
442    /// Whether to include an under slot.
443    pub under: bool,
444    /// The over decoration to apply.
445    pub over_deco: Deco,
446    /// The under decoration to apply.
447    pub under_deco: Deco,
448}
449
450/// A caret is a gap in a sequence.
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452pub(crate) struct Cursor {
453    pub(crate) seq: SeqId,
454    pub(crate) index: usize,
455}
456
457/// A contiguous run within one sequence between two gaps.
458#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459pub(crate) struct SeqRange {
460    pub(crate) seq: SeqId,
461    pub(crate) anchor: usize,
462    pub(crate) focus: usize,
463}
464
465impl SeqRange {
466    pub(crate) fn lo(&self) -> usize {
467        self.anchor.min(self.focus)
468    }
469
470    pub(crate) fn hi(&self) -> usize {
471        self.anchor.max(self.focus)
472    }
473}