Skip to main content

mathtex_editor_core/
model.rs

1//! The editable data model uses a `Seq` and `Node` tree backed by stable slotmap ids.
2
3use serde::{Deserialize, Serialize};
4use slotmap::{new_key_type, SlotMap};
5
6new_key_type! {
7    /// Stable identity of a node.
8    pub struct NodeId;
9    /// Stable identity of an editable sequence.
10    pub struct SeqId;
11}
12
13/// The editable math tree.
14#[derive(Debug, Clone)]
15pub struct Tree {
16    pub(crate) nodes: SlotMap<NodeId, Node>,
17    pub(crate) seqs: SlotMap<SeqId, Seq>,
18    pub(crate) root: SeqId,
19}
20
21impl Default for Tree {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl Tree {
28    /// A new tree with an empty root sequence.
29    pub fn new() -> Self {
30        let mut seqs: SlotMap<SeqId, Seq> = SlotMap::with_key();
31        let root = seqs.insert(Seq {
32            parent: None,
33            items: Vec::new(),
34        });
35        Self {
36            nodes: SlotMap::with_key(),
37            seqs,
38            root,
39        }
40    }
41
42    /// Return the root sequence id.
43    pub fn root(&self) -> SeqId {
44        self.root
45    }
46    /// Return a node by id.
47    pub fn node(&self, id: NodeId) -> Option<&Node> {
48        self.nodes.get(id)
49    }
50    /// Return a sequence by id.
51    pub fn seq(&self, id: SeqId) -> Option<&Seq> {
52        self.seqs.get(id)
53    }
54    /// Return a node payload by id.
55    pub fn kind(&self, id: NodeId) -> Option<&Kind> {
56        self.nodes.get(id).map(|n| &n.kind)
57    }
58    /// Return the node ids stored in a sequence.
59    pub fn items(&self, id: SeqId) -> &[NodeId] {
60        self.seqs.get(id).map_or(&[], |s| s.items.as_slice())
61    }
62    /// Return the number of nodes in a sequence.
63    pub fn len(&self, id: SeqId) -> usize {
64        self.items(id).len()
65    }
66    /// An empty sequence is a structural placeholder.
67    pub fn is_empty(&self, id: SeqId) -> bool {
68        self.items(id).is_empty()
69    }
70
71    /// The node that owns this sequence as a slot, or `None` for the root.
72    pub fn seq_parent(&self, id: SeqId) -> Option<NodeId> {
73        self.seqs.get(id).and_then(|s| s.parent)
74    }
75
76    /// If `seq` is the `base` slot of a `Script`, the owning Script node.
77    pub(crate) fn script_base_node(&self, seq: SeqId) -> Option<NodeId> {
78        let parent = self.seq_parent(seq)?;
79        match self.kind(parent) {
80            Some(Kind::Script { base, .. }) if *base == seq => Some(parent),
81            _ => None,
82        }
83    }
84
85    /// The sequence and index where this node currently lives.
86    pub fn index_in_parent(&self, node: NodeId) -> Option<(SeqId, usize)> {
87        let parent = self.nodes.get(node)?.parent;
88        let idx = self.seqs.get(parent)?.items.iter().position(|&n| n == node)?;
89        Some((parent, idx))
90    }
91
92    /// All present slot sequences of a node in canonical navigation and ownership order.
93    pub fn child_seqs(&self, node: NodeId) -> Vec<SeqId> {
94        let Some(n) = self.nodes.get(node) else {
95            return Vec::new();
96        };
97        match &n.kind {
98            Kind::Atom(_) | Kind::HostBox { .. } => Vec::new(),
99            Kind::Frac { num, den, .. } => vec![*num, *den],
100            Kind::Script { base, sub, sup } => {
101                let mut v = vec![*base];
102                v.extend(sub.iter().copied());
103                v.extend(sup.iter().copied());
104                v
105            }
106            // Upper first so leftward navigation enters the lower limit before the upper one.
107            Kind::BigOp { upper, lower, .. } => vec![*upper, *lower],
108            Kind::Sqrt { index, radicand } => vec![*index, *radicand],
109            Kind::Delim { body, .. } => vec![*body],
110            Kind::Accent { base, .. } => vec![*base],
111            Kind::UnderOver {
112                base, over, under, ..
113            } => {
114                let mut v = Vec::new();
115                v.extend(over.iter().copied());
116                v.push(*base);
117                v.extend(under.iter().copied());
118                v
119            }
120            Kind::Styled { content, .. } => vec![*content],
121            Kind::Matrix { rows, .. } => rows.iter().flatten().copied().collect(),
122        }
123    }
124}
125
126/// An ordered run of nodes with an optional owning node.
127#[derive(Debug, Clone)]
128pub struct Seq {
129    /// The node that owns this sequence, or `None` for the root sequence.
130    pub parent: Option<NodeId>,
131    /// The ordered nodes stored in this sequence.
132    pub items: Vec<NodeId>,
133}
134
135/// A node, which always lives inside a sequence.
136#[derive(Debug, Clone)]
137pub struct Node {
138    /// The sequence that contains this node.
139    pub parent: SeqId,
140    /// The payload carried by this node.
141    pub kind: Kind,
142}
143
144/// Node payloads. Every editable slot is a `SeqId`.
145#[derive(Debug, Clone)]
146pub enum Kind {
147    /// A single leaf token.
148    Atom(Symbol),
149    /// An opaque host owned object referenced by a host minted token.
150    HostBox {
151        /// The host minted token identifying the object.
152        token: u32,
153    },
154    /// A fraction with numerator and denominator slots.
155    Frac {
156        /// The numerator slot.
157        num: SeqId,
158        /// The denominator slot.
159        den: SeqId,
160        /// The visual fraction style.
161        style: FracStyle,
162    },
163    /// Subscripts and superscripts on an editable base.
164    Script {
165        /// The base slot.
166        base: SeqId,
167        /// The optional subscript slot.
168        sub: Option<SeqId>,
169        /// The optional superscript slot.
170        sup: Option<SeqId>,
171    },
172    /// A fixed big operator with editable lower and upper limit slots.
173    BigOp {
174        /// The operator nucleus.
175        op: Symbol,
176        /// The lower limit slot.
177        lower: SeqId,
178        /// The upper limit slot.
179        upper: SeqId,
180    },
181    /// A radical with degree and radicand slots.
182    Sqrt {
183        /// The degree slot.
184        index: SeqId,
185        /// The radicand slot.
186        radicand: SeqId,
187    },
188    /// A delimited expression.
189    Delim {
190        /// The opening delimiter.
191        open: char,
192        /// The closing delimiter.
193        close: char,
194        /// The delimited body slot.
195        body: SeqId,
196    },
197    /// A fixed accent mark attached to an editable base.
198    Accent {
199        /// The accent mark.
200        mark: Mark,
201        /// The accented base slot.
202        base: SeqId,
203    },
204    /// An editable base with optional over and under labels.
205    UnderOver {
206        /// The base slot.
207        base: SeqId,
208        /// The optional over slot.
209        over: Option<SeqId>,
210        /// The optional under slot.
211        under: Option<SeqId>,
212        /// The over decoration.
213        over_deco: Deco,
214        /// The under decoration.
215        under_deco: Deco,
216    },
217    /// A styled content slot.
218    Styled {
219        /// The style variant.
220        variant: Variant,
221        /// The styled content slot.
222        content: SeqId,
223    },
224    /// A rectangular grid of editable cell slots.
225    Matrix {
226        /// The matrix environment.
227        env: MatrixEnv,
228        /// The matrix cell slots by row.
229        rows: Vec<Vec<SeqId>>,
230    },
231}
232
233/// A leaf token plus its math class for editing heuristics.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct Symbol {
236    /// The LaTeX emitted for this symbol.
237    pub latex: String,
238    /// The math class used by editing heuristics.
239    pub class: MathClass,
240}
241
242impl Symbol {
243    /// Build a symbol from a typed character while escaping TeX special characters.
244    pub fn from_char(c: char) -> Self {
245        let latex = match c {
246            '%' => "\\%".to_string(),
247            '#' => "\\#".to_string(),
248            '&' => "\\&".to_string(),
249            '$' => "\\$".to_string(),
250            '_' => "\\_".to_string(),
251            '{' => "\\{".to_string(),
252            '}' => "\\}".to_string(),
253            '~' => "\\sim".to_string(),
254            '\\' => "\\backslash".to_string(),
255            // Text mode carries letters that XeTeX math mode will not render directly.
256            other if needs_text_mode(other) => format!("\\text{{{other}}}"),
257            other => other.to_string(),
258        };
259        Symbol {
260            latex,
261            class: char_class(c),
262        }
263    }
264}
265
266/// A letter that XeTeX math mode won't render directly.
267fn needs_text_mode(c: char) -> bool {
268    let greek = ('\u{0370}'..='\u{03FF}').contains(&c) || ('\u{1F00}'..='\u{1FFF}').contains(&c);
269    c.is_alphabetic() && !c.is_ascii() && !greek
270}
271
272/// Default math class for a typed character.
273pub(crate) fn char_class(c: char) -> MathClass {
274    match c {
275        '+' | '-' | '*' => MathClass::Bin,
276        '=' | '<' | '>' => MathClass::Rel,
277        ',' | ';' | '.' | ':' => MathClass::Punct,
278        '(' | '[' => MathClass::Open,
279        ')' | ']' => MathClass::Close,
280        _ => MathClass::Ord,
281    }
282}
283
284/// Math atom classification used by editing heuristics.
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
286#[serde(rename_all = "snake_case")]
287pub enum MathClass {
288    /// Ordinary math atom.
289    Ord,
290    /// Operator atom.
291    Op,
292    /// Binary operator atom.
293    Bin,
294    /// Relation atom.
295    Rel,
296    /// Opening delimiter atom.
297    Open,
298    /// Closing delimiter atom.
299    Close,
300    /// Punctuation atom.
301    Punct,
302    /// Inner atom.
303    Inner,
304}
305
306/// Fraction rendering style.
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(rename_all = "snake_case")]
309pub enum FracStyle {
310    /// Standard fraction bar style.
311    Bar,
312    /// Display fraction style.
313    Display,
314    /// Text fraction style.
315    Text,
316    /// Binomial fraction style.
317    Binom,
318    /// Fraction layout without a bar.
319    Atop,
320}
321
322/// Script slot selector.
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(rename_all = "snake_case")]
325pub enum ScriptSlot {
326    /// Subscript slot.
327    Sub,
328    /// Superscript slot.
329    Sup,
330}
331
332/// Accent mark type.
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335pub enum Mark {
336    /// Hat accent.
337    Hat,
338    /// Vector accent.
339    Vec,
340    /// Bar accent.
341    Bar,
342    /// Tilde accent.
343    Tilde,
344    /// Dot accent.
345    Dot,
346    /// Double dot accent.
347    Ddot,
348    /// Wide hat accent.
349    Widehat,
350    /// Wide tilde accent.
351    Widetilde,
352    /// Overline accent.
353    Overline,
354    /// Underline accent.
355    Underline,
356    /// Check accent.
357    Check,
358    /// Breve accent.
359    Breve,
360}
361
362/// Decoration used by under and over constructs.
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(rename_all = "snake_case")]
365pub enum Deco {
366    /// No decoration.
367    None,
368    /// Brace decoration.
369    Brace,
370    /// Arrow decoration.
371    Arrow,
372    /// Line decoration.
373    Line,
374}
375
376/// Font or text variant.
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
378#[serde(rename_all = "snake_case")]
379pub enum Variant {
380    /// Normal math style.
381    Normal,
382    /// Bold math style.
383    Bold,
384    /// Blackboard bold math style.
385    Blackboard,
386    /// Calligraphic math style.
387    Calligraphic,
388    /// Fraktur math style.
389    Fraktur,
390    /// Roman math style.
391    Roman,
392    /// Sans serif math style.
393    SansSerif,
394    /// Typewriter math style.
395    Typewriter,
396    /// Text mode style.
397    Text,
398    /// Operator name style.
399    OperatorName,
400}
401
402/// Matrix environment type.
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
404#[serde(rename_all = "snake_case")]
405pub enum MatrixEnv {
406    /// Plain matrix environment.
407    Matrix,
408    /// Parenthesized matrix environment.
409    Pmatrix,
410    /// Bracketed matrix environment.
411    Bmatrix,
412    /// Vertically barred matrix environment.
413    Vmatrix,
414    /// Cases environment.
415    Cases,
416    /// Aligned environment.
417    Aligned,
418    /// Array environment.
419    Array,
420}
421
422/// Spec for inserting an under/over construct.
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
424pub struct UnderOverSpec {
425    /// Whether to include an over slot.
426    pub over: bool,
427    /// Whether to include an under slot.
428    pub under: bool,
429    /// The over decoration to apply.
430    pub over_deco: Deco,
431    /// The under decoration to apply.
432    pub under_deco: Deco,
433}
434
435/// A cursor is a gap in a sequence.
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub struct Cursor {
438    /// The sequence containing the cursor.
439    pub seq: SeqId,
440    /// The gap index inside the sequence.
441    pub index: usize,
442}
443
444/// A selection is a contiguous run within one sequence.
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub struct Selection {
447    /// The selected sequence.
448    pub seq: SeqId,
449    /// The anchor gap index.
450    pub anchor: usize,
451    /// The focus gap index.
452    pub focus: usize,
453}