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        }
144    }
145}
146
147/// An ordered run of nodes with an optional owning node.
148#[derive(Debug, Clone)]
149pub(crate) struct Seq {
150    pub(crate) parent: Option<NodeId>,
151    pub(crate) items: Vec<NodeId>,
152}
153
154/// A node, which always lives inside a sequence.
155#[derive(Debug, Clone)]
156pub(crate) struct Node {
157    pub(crate) parent: SeqId,
158    pub(crate) kind: Kind,
159}
160
161/// Node payloads, every editable slot is a `SeqId`.
162#[derive(Debug, Clone)]
163pub(crate) enum Kind {
164    Atom(Symbol),
165    HostBox { token: u32 },
166    Frac { num: SeqId, den: SeqId, style: FracStyle },
167    Script { base: SeqId, sub: Option<SeqId>, sup: Option<SeqId> },
168    BigOp { op: Symbol, lower: SeqId, upper: SeqId },
169    Sqrt { index: SeqId, radicand: SeqId },
170    Delim { open: char, close: char, body: SeqId },
171    Accent { mark: Mark, base: SeqId },
172    UnderOver { base: SeqId, over: Option<SeqId>, under: Option<SeqId>, over_deco: Deco, under_deco: Deco },
173    Styled { variant: Variant, content: SeqId },
174    Matrix { env: MatrixEnv, rows: Vec<Vec<SeqId>> },
175}
176
177/// A leaf token plus its math class for editing heuristics.
178#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
179pub struct Symbol {
180    /// The math mode LaTeX emitted for this symbol.
181    pub latex: String,
182    /// The math class used by editing heuristics.
183    pub class: MathClass,
184}
185
186impl Symbol {
187    /// Build a symbol from a typed character, escaping TeX specials, `None` for control characters.
188    pub fn from_char(c: char) -> Option<Self> {
189        if c.is_control() {
190            return None;
191        }
192        let latex = match c {
193            '%' | '#' | '&' | '$' | '_' | '{' | '}' => format!("\\{c}"),
194            '~' => "\\sim".to_string(),
195            '\\' => "\\backslash".to_string(),
196            '^' => "\\text{\\textasciicircum}".to_string(),
197            '\'' => "\\prime".to_string(),
198            ' ' => "\\ ".to_string(),
199            // Text mode carries letters that XeTeX math mode will not render directly.
200            other if needs_text_mode(other) => format!("\\text{{{other}}}"),
201            other => other.to_string(),
202        };
203        let class = latex_class(&latex);
204        Some(Symbol { latex, class })
205    }
206
207    /// A symbol for LaTeX such as `\leq`, classed by the same table as [`Symbol::from_char`].
208    pub fn from_latex(latex: &str) -> Self {
209        Symbol { latex: latex.to_string(), class: latex_class(latex) }
210    }
211}
212
213/// A letter that XeTeX math mode won't render directly.
214fn needs_text_mode(c: char) -> bool {
215    let greek = ('\u{0370}'..='\u{03FF}').contains(&c) || ('\u{1F00}'..='\u{1FFF}').contains(&c);
216    // Letterlike symbols such as โ„ and the math alphanumerics such as ๐‘ฅ are math characters already.
217    let letterlike = ('\u{2100}'..='\u{214F}').contains(&c);
218    let math_alnum = ('\u{1D400}'..='\u{1D7FF}').contains(&c);
219    c.is_alphabetic() && !c.is_ascii() && !greek && !letterlike && !math_alnum
220}
221
222/// Default math class for a typed character.
223fn char_class(c: char) -> MathClass {
224    match c {
225        '+' | '-' | '*' | '\u{2212}' | 'ยฑ' | 'โˆ“' | 'ร—' | 'รท' | 'ยท' | 'โˆ˜' | 'โˆ™' => MathClass::Bin,
226        '=' | '<' | '>' | 'โ‰ค' | 'โ‰ฅ' | 'โ‰ ' | 'โ‰ˆ' | 'โ‰ก' | 'โˆผ' | 'โ‰…' | 'โˆ' | 'โ†’' | 'โ†' | 'โ‡’' | 'โ‡' | 'โ‡”'
227        | 'โˆˆ' | 'โˆ‰' | 'โŠ‚' | 'โІ' | 'โŠƒ' | 'โЇ' => MathClass::Rel,
228        ',' | ';' | '.' | ':' => MathClass::Punct,
229        '(' | '[' | '{' | 'โŸจ' | 'โŒˆ' | 'โŒŠ' => MathClass::Open,
230        ')' | ']' | '}' | 'โŸฉ' | 'โŒ‰' | 'โŒ‹' => MathClass::Close,
231        _ => MathClass::Ord,
232    }
233}
234
235/// Default class of a symbol's LaTeX, the one table behind every `Symbol` constructor.
236fn latex_class(latex: &str) -> MathClass {
237    let mut chars = latex.chars();
238    if let (Some(c), None) = (chars.next(), chars.next()) {
239        return char_class(c);
240    }
241    let Some(name) = latex.strip_prefix('\\') else {
242        return MathClass::Ord;
243    };
244    if OPERATOR_NAMES.contains(&name) {
245        return MathClass::Op;
246    }
247    match name {
248        "{" | "langle" | "lceil" | "lfloor" => MathClass::Open,
249        "}" | "rangle" | "rceil" | "rfloor" => MathClass::Close,
250        "leq" | "le" | "geq" | "ge" | "neq" | "ne" | "equiv" | "approx" | "cong" | "sim" | "simeq" | "propto"
251        | "to" | "gets" | "mapsto" | "implies" | "iff" | "in" | "notin" | "ni" | "subset" | "subseteq"
252        | "supset" | "supseteq" | "rightarrow" | "leftarrow" | "leftrightarrow" | "Rightarrow" | "Leftarrow"
253        | "Leftrightarrow" | "Longrightarrow" | "Longleftarrow" | "perp" | "parallel" | "mid" | "ll" | "gg" => {
254            MathClass::Rel
255        }
256        "pm" | "mp" | "times" | "div" | "cdot" | "ast" | "star" | "cup" | "cap" | "setminus" | "circ" | "oplus"
257        | "otimes" | "wedge" | "vee" | "land" | "lor" => MathClass::Bin,
258        "cdots" | "ldots" | "dots" | "vdots" | "ddots" => MathClass::Inner,
259        "sum" | "prod" | "coprod" | "int" | "iint" | "iiint" | "oint" | "bigcup" | "bigcap" | "bigsqcup" | "biguplus"
260        | "bigoplus" | "bigotimes" | "bigodot" | "bigvee" | "bigwedge" => MathClass::Op,
261        _ => MathClass::Ord,
262    }
263}
264
265/// Operator names that typeset upright, such as `\sin`, classed `Op`.
266const OPERATOR_NAMES: &[&str] = &[
267    "sin", "cos", "tan", "cot", "sec", "csc", "sinh", "cosh", "tanh", "arcsin", "arccos", "arctan", "log", "ln",
268    "exp", "lim", "max", "min", "sup", "inf", "gcd", "det", "dim", "ker", "arg", "deg", "hom",
269];
270
271/// Math atom classification used by editing heuristics.
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case")]
274pub enum MathClass {
275    /// Ordinary math atom.
276    Ord,
277    /// Operator atom.
278    Op,
279    /// Binary operator atom.
280    Bin,
281    /// Relation atom.
282    Rel,
283    /// Opening delimiter atom.
284    Open,
285    /// Closing delimiter atom.
286    Close,
287    /// Punctuation atom.
288    Punct,
289    /// Inner atom.
290    Inner,
291}
292
293/// Fraction rendering style.
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
295#[serde(rename_all = "snake_case")]
296pub enum FracStyle {
297    /// Standard fraction bar style.
298    Bar,
299    /// Display fraction style.
300    Display,
301    /// Text fraction style.
302    Text,
303    /// Binomial fraction style.
304    Binom,
305    /// Fraction layout without a bar.
306    Atop,
307}
308
309/// Script slot selector.
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
311#[serde(rename_all = "snake_case")]
312pub enum ScriptSlot {
313    /// Subscript slot.
314    Sub,
315    /// Superscript slot.
316    Sup,
317}
318
319/// Accent mark type.
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
321#[serde(rename_all = "snake_case")]
322pub enum Mark {
323    /// Hat accent.
324    Hat,
325    /// Vector accent.
326    Vec,
327    /// Bar accent.
328    Bar,
329    /// Tilde accent.
330    Tilde,
331    /// Dot accent.
332    Dot,
333    /// Double dot accent.
334    Ddot,
335    /// Wide hat accent.
336    Widehat,
337    /// Wide tilde accent.
338    Widetilde,
339    /// Overline accent.
340    Overline,
341    /// Underline accent.
342    Underline,
343    /// Check accent.
344    Check,
345    /// Breve accent.
346    Breve,
347}
348
349/// Decoration drawn between an under or over label and its base.
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
351#[serde(rename_all = "snake_case")]
352pub enum Deco {
353    /// No decoration, the label sits directly above or below.
354    None,
355    /// A horizontal brace.
356    Brace,
357    /// A rightward arrow.
358    Arrow,
359    /// A horizontal line.
360    Line,
361}
362
363/// Font or text variant.
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
365#[serde(rename_all = "snake_case")]
366pub enum Variant {
367    /// Normal math style.
368    Normal,
369    /// Bold math style.
370    Bold,
371    /// Blackboard bold math style.
372    Blackboard,
373    /// Calligraphic math style.
374    Calligraphic,
375    /// Fraktur math style.
376    Fraktur,
377    /// Roman math style.
378    Roman,
379    /// Sans serif math style.
380    SansSerif,
381    /// Typewriter math style.
382    Typewriter,
383    /// Text mode, whose slot holds only atoms.
384    Text,
385    /// Operator name style.
386    OperatorName,
387}
388
389/// Matrix environment type.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391#[serde(rename_all = "snake_case")]
392pub enum MatrixEnv {
393    /// Plain matrix environment.
394    Matrix,
395    /// Parenthesized matrix environment.
396    Pmatrix,
397    /// Bracketed matrix environment.
398    Bmatrix,
399    /// Vertically barred matrix environment.
400    Vmatrix,
401    /// Cases environment.
402    Cases,
403    /// Aligned environment.
404    Aligned,
405    /// Array environment with centered columns.
406    Array,
407}
408
409/// Spec for inserting an under or over construct.
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
411pub struct UnderOverSpec {
412    /// Whether to include an over slot.
413    pub over: bool,
414    /// Whether to include an under slot.
415    pub under: bool,
416    /// The over decoration to apply.
417    pub over_deco: Deco,
418    /// The under decoration to apply.
419    pub under_deco: Deco,
420}
421
422/// A caret is a gap in a sequence.
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424pub(crate) struct Cursor {
425    pub(crate) seq: SeqId,
426    pub(crate) index: usize,
427}
428
429/// A contiguous run within one sequence between two gaps.
430#[derive(Debug, Clone, Copy, PartialEq, Eq)]
431pub(crate) struct SeqRange {
432    pub(crate) seq: SeqId,
433    pub(crate) anchor: usize,
434    pub(crate) focus: usize,
435}
436
437impl SeqRange {
438    pub(crate) fn lo(&self) -> usize {
439        self.anchor.min(self.focus)
440    }
441
442    pub(crate) fn hi(&self) -> usize {
443        self.anchor.max(self.focus)
444    }
445}