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(_) => 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    /// A fraction with numerator and denominator slots.
150    Frac {
151        /// The numerator slot.
152        num: SeqId,
153        /// The denominator slot.
154        den: SeqId,
155        /// The visual fraction style.
156        style: FracStyle,
157    },
158    /// Subscripts and superscripts on an editable base.
159    Script {
160        /// The base slot.
161        base: SeqId,
162        /// The optional subscript slot.
163        sub: Option<SeqId>,
164        /// The optional superscript slot.
165        sup: Option<SeqId>,
166    },
167    /// A fixed big operator with editable lower and upper limit slots.
168    BigOp {
169        /// The operator nucleus.
170        op: Symbol,
171        /// The lower limit slot.
172        lower: SeqId,
173        /// The upper limit slot.
174        upper: SeqId,
175    },
176    /// A radical with degree and radicand slots.
177    Sqrt {
178        /// The degree slot.
179        index: SeqId,
180        /// The radicand slot.
181        radicand: SeqId,
182    },
183    /// A delimited expression.
184    Delim {
185        /// The opening delimiter.
186        open: char,
187        /// The closing delimiter.
188        close: char,
189        /// The delimited body slot.
190        body: SeqId,
191    },
192    /// A fixed accent mark attached to an editable base.
193    Accent {
194        /// The accent mark.
195        mark: Mark,
196        /// The accented base slot.
197        base: SeqId,
198    },
199    /// An editable base with optional over and under labels.
200    UnderOver {
201        /// The base slot.
202        base: SeqId,
203        /// The optional over slot.
204        over: Option<SeqId>,
205        /// The optional under slot.
206        under: Option<SeqId>,
207        /// The over decoration.
208        over_deco: Deco,
209        /// The under decoration.
210        under_deco: Deco,
211    },
212    /// A styled content slot.
213    Styled {
214        /// The style variant.
215        variant: Variant,
216        /// The styled content slot.
217        content: SeqId,
218    },
219    /// A rectangular grid of editable cell slots.
220    Matrix {
221        /// The matrix environment.
222        env: MatrixEnv,
223        /// The matrix cell slots by row.
224        rows: Vec<Vec<SeqId>>,
225    },
226}
227
228/// A leaf token plus its math class for editing heuristics.
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230pub struct Symbol {
231    /// The LaTeX emitted for this symbol.
232    pub latex: String,
233    /// The math class used by editing heuristics.
234    pub class: MathClass,
235}
236
237impl Symbol {
238    /// Build a symbol from a typed character while escaping TeX special characters.
239    pub fn from_char(c: char) -> Self {
240        let latex = match c {
241            '%' => "\\%".to_string(),
242            '#' => "\\#".to_string(),
243            '&' => "\\&".to_string(),
244            '$' => "\\$".to_string(),
245            '_' => "\\_".to_string(),
246            '{' => "\\{".to_string(),
247            '}' => "\\}".to_string(),
248            '~' => "\\sim".to_string(),
249            '\\' => "\\backslash".to_string(),
250            // Text mode carries letters that XeTeX math mode will not render directly.
251            other if needs_text_mode(other) => format!("\\text{{{other}}}"),
252            other => other.to_string(),
253        };
254        Symbol {
255            latex,
256            class: char_class(c),
257        }
258    }
259}
260
261/// A letter that XeTeX math mode won't render directly.
262fn needs_text_mode(c: char) -> bool {
263    let greek = ('\u{0370}'..='\u{03FF}').contains(&c) || ('\u{1F00}'..='\u{1FFF}').contains(&c);
264    c.is_alphabetic() && !c.is_ascii() && !greek
265}
266
267/// Default math class for a typed character.
268pub(crate) fn char_class(c: char) -> MathClass {
269    match c {
270        '+' | '-' | '*' => MathClass::Bin,
271        '=' | '<' | '>' => MathClass::Rel,
272        ',' | ';' | '.' | ':' => MathClass::Punct,
273        '(' | '[' => MathClass::Open,
274        ')' | ']' => MathClass::Close,
275        _ => MathClass::Ord,
276    }
277}
278
279/// Math atom classification used by editing heuristics.
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum MathClass {
283    /// Ordinary math atom.
284    Ord,
285    /// Operator atom.
286    Op,
287    /// Binary operator atom.
288    Bin,
289    /// Relation atom.
290    Rel,
291    /// Opening delimiter atom.
292    Open,
293    /// Closing delimiter atom.
294    Close,
295    /// Punctuation atom.
296    Punct,
297    /// Inner atom.
298    Inner,
299}
300
301/// Fraction rendering style.
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(rename_all = "snake_case")]
304pub enum FracStyle {
305    /// Standard fraction bar style.
306    Bar,
307    /// Display fraction style.
308    Display,
309    /// Text fraction style.
310    Text,
311    /// Binomial fraction style.
312    Binom,
313    /// Fraction layout without a bar.
314    Atop,
315}
316
317/// Script slot selector.
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
319#[serde(rename_all = "snake_case")]
320pub enum ScriptSlot {
321    /// Subscript slot.
322    Sub,
323    /// Superscript slot.
324    Sup,
325}
326
327/// Accent mark type.
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum Mark {
331    /// Hat accent.
332    Hat,
333    /// Vector accent.
334    Vec,
335    /// Bar accent.
336    Bar,
337    /// Tilde accent.
338    Tilde,
339    /// Dot accent.
340    Dot,
341    /// Double dot accent.
342    Ddot,
343    /// Wide hat accent.
344    Widehat,
345    /// Wide tilde accent.
346    Widetilde,
347    /// Overline accent.
348    Overline,
349    /// Underline accent.
350    Underline,
351    /// Check accent.
352    Check,
353    /// Breve accent.
354    Breve,
355}
356
357/// Decoration used by under and over constructs.
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
359#[serde(rename_all = "snake_case")]
360pub enum Deco {
361    /// No decoration.
362    None,
363    /// Brace decoration.
364    Brace,
365    /// Arrow decoration.
366    Arrow,
367    /// Line decoration.
368    Line,
369}
370
371/// Font or text variant.
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(rename_all = "snake_case")]
374pub enum Variant {
375    /// Normal math style.
376    Normal,
377    /// Bold math style.
378    Bold,
379    /// Blackboard bold math style.
380    Blackboard,
381    /// Calligraphic math style.
382    Calligraphic,
383    /// Fraktur math style.
384    Fraktur,
385    /// Roman math style.
386    Roman,
387    /// Sans serif math style.
388    SansSerif,
389    /// Typewriter math style.
390    Typewriter,
391    /// Text mode style.
392    Text,
393    /// Operator name style.
394    OperatorName,
395}
396
397/// Matrix environment type.
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "snake_case")]
400pub enum MatrixEnv {
401    /// Plain matrix environment.
402    Matrix,
403    /// Parenthesized matrix environment.
404    Pmatrix,
405    /// Bracketed matrix environment.
406    Bmatrix,
407    /// Vertically barred matrix environment.
408    Vmatrix,
409    /// Cases environment.
410    Cases,
411    /// Aligned environment.
412    Aligned,
413    /// Array environment.
414    Array,
415}
416
417/// Spec for inserting an under/over construct.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
419pub struct UnderOverSpec {
420    /// Whether to include an over slot.
421    pub over: bool,
422    /// Whether to include an under slot.
423    pub under: bool,
424    /// The over decoration to apply.
425    pub over_deco: Deco,
426    /// The under decoration to apply.
427    pub under_deco: Deco,
428}
429
430/// A cursor is a gap in a sequence.
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432pub struct Cursor {
433    /// The sequence containing the cursor.
434    pub seq: SeqId,
435    /// The gap index inside the sequence.
436    pub index: usize,
437}
438
439/// A selection is a contiguous run within one sequence.
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub struct Selection {
442    /// The selected sequence.
443    pub seq: SeqId,
444    /// The anchor gap index.
445    pub anchor: usize,
446    /// The focus gap index.
447    pub focus: usize,
448}