mathtex-editor-core 0.3.0

Headless core of the mathtex structural math editor: model, operations, navigation, selection, IR matching
Documentation
//! Stable caret addresses: a chain of child indexes and named slots from the root plus a gap index.

use std::fmt;

use serde::{Deserialize, Serialize};

use crate::model::{Cursor, Kind, NodeId, SeqId, Tree};

/// A named slot of a structure.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Slot {
    /// Fraction numerator.
    Numerator,
    /// Fraction denominator.
    Denominator,
    /// Base of a script, accent, or under over construct.
    Base,
    /// Subscript.
    Sub,
    /// Superscript.
    Sup,
    /// Lower limit of a big operator.
    Lower,
    /// Upper limit of a big operator.
    Upper,
    /// Degree of a radical.
    Index,
    /// Radicand of a radical.
    Radicand,
    /// Body of delimiters or content of a styled run.
    Body,
    /// Label above an under over construct.
    Over,
    /// Label below an under over construct.
    Under,
    /// Matrix cell by zero based row and column.
    Cell {
        /// Row index.
        row: usize,
        /// Column index.
        col: usize,
    },
}

/// One step down the tree: the child at `node` in the current sequence, then its `slot`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Step {
    /// Index of the child node in its sequence.
    pub node: usize,
    /// The slot of that node to descend into.
    pub slot: Slot,
}

/// A caret position that survives serialization and editor rebuilds.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CaretPath {
    /// Steps from the root sequence down to the caret's sequence, empty for the root.
    pub steps: Vec<Step>,
    /// Gap index in that sequence, `0` is before the first item.
    pub index: usize,
}

impl CaretPath {
    /// A caret at gap `index` of the root sequence.
    pub fn root(index: usize) -> Self {
        Self { steps: Vec::new(), index }
    }
}

/// A selection between two carets in the same sequence.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Selection {
    /// The fixed end.
    pub anchor: CaretPath,
    /// The moving end, where the caret is drawn.
    pub focus: CaretPath,
}

/// Why a path does not address a caret position in the current document.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathError {
    /// Step `depth` names a child index past the end of its sequence.
    NoNode {
        /// Zero based step index.
        depth: usize,
    },
    /// Step `depth` names a slot its node does not have.
    NoSlot {
        /// Zero based step index.
        depth: usize,
    },
    /// The gap index is past the end of the addressed sequence.
    GapOutOfRange {
        /// Length of the addressed sequence.
        len: usize,
        /// The requested gap.
        index: usize,
    },
    /// Anchor and focus address different sequences.
    SplitSelection,
}

impl fmt::Display for PathError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PathError::NoNode { depth } => write!(f, "step {depth} names a missing node"),
            PathError::NoSlot { depth } => write!(f, "step {depth} names a missing slot"),
            PathError::GapOutOfRange { len, index } => {
                write!(f, "gap {index} is outside a sequence of length {len}")
            }
            PathError::SplitSelection => write!(f, "selection ends lie in different sequences"),
        }
    }
}

impl std::error::Error for PathError {}

impl Tree {
    /// The named slot `seq` occupies in `node`.
    pub(crate) fn slot_of(&self, node: NodeId, seq: SeqId) -> Option<Slot> {
        let hit = |s: SeqId| s == seq;
        Some(match self.kind(node)? {
            Kind::Atom(_) | Kind::HostBox { .. } => return None,
            Kind::Frac { num, den, .. } => {
                if hit(*num) {
                    Slot::Numerator
                } else if hit(*den) {
                    Slot::Denominator
                } else {
                    return None;
                }
            }
            Kind::Script { base, sub, sup } => {
                if hit(*base) {
                    Slot::Base
                } else if sub.is_some_and(hit) {
                    Slot::Sub
                } else if sup.is_some_and(hit) {
                    Slot::Sup
                } else {
                    return None;
                }
            }
            Kind::BigOp { lower, upper, .. } => {
                if hit(*lower) {
                    Slot::Lower
                } else if hit(*upper) {
                    Slot::Upper
                } else {
                    return None;
                }
            }
            Kind::Sqrt { index, radicand } => {
                if hit(*index) {
                    Slot::Index
                } else if hit(*radicand) {
                    Slot::Radicand
                } else {
                    return None;
                }
            }
            Kind::Delim { body, .. } | Kind::Styled { content: body, .. } => {
                if hit(*body) {
                    Slot::Body
                } else {
                    return None;
                }
            }
            Kind::Accent { base, .. } => {
                if hit(*base) {
                    Slot::Base
                } else {
                    return None;
                }
            }
            Kind::UnderOver { base, over, under, .. } => {
                if hit(*base) {
                    Slot::Base
                } else if over.is_some_and(hit) {
                    Slot::Over
                } else if under.is_some_and(hit) {
                    Slot::Under
                } else {
                    return None;
                }
            }
            Kind::Matrix { rows, .. } => {
                let (row, col) = rows
                    .iter()
                    .enumerate()
                    .find_map(|(r, cells)| cells.iter().position(|&c| c == seq).map(|c| (r, c)))?;
                Slot::Cell { row, col }
            }
        })
    }

    /// The sequence behind a named slot of `node`.
    pub(crate) fn slot_seq(&self, node: NodeId, slot: Slot) -> Option<SeqId> {
        match (self.kind(node)?, slot) {
            (Kind::Frac { num, .. }, Slot::Numerator) => Some(*num),
            (Kind::Frac { den, .. }, Slot::Denominator) => Some(*den),
            (Kind::Script { base, .. }, Slot::Base) => Some(*base),
            (Kind::Script { sub, .. }, Slot::Sub) => *sub,
            (Kind::Script { sup, .. }, Slot::Sup) => *sup,
            (Kind::BigOp { lower, .. }, Slot::Lower) => Some(*lower),
            (Kind::BigOp { upper, .. }, Slot::Upper) => Some(*upper),
            (Kind::Sqrt { index, .. }, Slot::Index) => Some(*index),
            (Kind::Sqrt { radicand, .. }, Slot::Radicand) => Some(*radicand),
            (Kind::Delim { body, .. }, Slot::Body) => Some(*body),
            (Kind::Styled { content, .. }, Slot::Body) => Some(*content),
            (Kind::Accent { base, .. }, Slot::Base) => Some(*base),
            (Kind::UnderOver { base, .. }, Slot::Base) => Some(*base),
            (Kind::UnderOver { over, .. }, Slot::Over) => *over,
            (Kind::UnderOver { under, .. }, Slot::Under) => *under,
            (Kind::Matrix { rows, .. }, Slot::Cell { row, col }) => rows.get(row)?.get(col).copied(),
            _ => None,
        }
    }

    /// The steps from the root down to `seq`.
    pub(crate) fn seq_steps(&self, seq: SeqId) -> Vec<Step> {
        let mut steps = Vec::new();
        let mut cur = seq;
        while let Some(node) = self.seq_parent(cur) {
            let (Some((pseq, idx)), Some(slot)) = (self.index_in_parent(node), self.slot_of(node, cur)) else {
                break;
            };
            steps.push(Step { node: idx, slot });
            cur = pseq;
        }
        steps.reverse();
        steps
    }

    pub(crate) fn path_of(&self, at: Cursor) -> CaretPath {
        CaretPath { steps: self.seq_steps(at.seq), index: at.index }
    }

    pub(crate) fn resolve_steps(&self, steps: &[Step]) -> Result<SeqId, PathError> {
        let mut seq = self.root();
        for (depth, step) in steps.iter().enumerate() {
            let node = *self.items(seq).get(step.node).ok_or(PathError::NoNode { depth })?;
            seq = self.slot_seq(node, step.slot).ok_or(PathError::NoSlot { depth })?;
        }
        Ok(seq)
    }

    pub(crate) fn resolve(&self, path: &CaretPath) -> Result<Cursor, PathError> {
        let seq = self.resolve_steps(&path.steps)?;
        let len = self.len(seq);
        if path.index > len {
            return Err(PathError::GapOutOfRange { len, index: path.index });
        }
        Ok(Cursor { seq, index: path.index })
    }
}