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}