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}