use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use slotmap::SecondaryMap;
use texform_interface::syntax_node::{self, SyntaxNode};
pub use crate::ast::NodeKind;
use crate::ast::{
Argument, ArgumentKind, ArgumentSlot, ArgumentValue, Ast, ContentMode, Delimiter, GroupKind,
Node, NodeId as RawNodeId, Slot,
};
use crate::parse::Span;
use crate::serialize::{SerializeError, SerializeOptions, serialize, serialize_with};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DocumentId(u64);
static NEXT_DOCUMENT_ID: AtomicU64 = AtomicU64::new(1);
fn next_document_id() -> DocumentId {
DocumentId(NEXT_DOCUMENT_ID.fetch_add(1, Ordering::Relaxed))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct NodeId {
document: DocumentId,
raw: RawNodeId,
}
impl NodeId {
fn new(document: DocumentId, raw: RawNodeId) -> Self {
Self { document, raw }
}
fn raw(self) -> RawNodeId {
self.raw
}
fn document(self) -> DocumentId {
self.document
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FromSyntaxError {
NotARoot,
InvalidPrimeCount,
PrimeInTextMode,
}
impl std::fmt::Display for FromSyntaxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FromSyntaxError::NotARoot => f.write_str("from_syntax expects a SyntaxNode::Root"),
FromSyntaxError::InvalidPrimeCount => {
f.write_str("Prime count must be greater than zero")
}
FromSyntaxError::PrimeInTextMode => {
f.write_str("Prime nodes are only valid in math mode")
}
}
}
}
impl std::error::Error for FromSyntaxError {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EditError {
NodeNotFound,
ReadOnlyDocument,
CannotEditRoot,
NotAContainer,
SlotShapeMismatch { expected: &'static str },
WouldCreateCycle,
IndexOutOfBounds,
DuplicateChild,
ExpectedDetachedRoot,
ForeignNode,
}
impl std::fmt::Display for EditError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EditError::NodeNotFound => f.write_str("node not found"),
EditError::ReadOnlyDocument => f.write_str("document is read-only"),
EditError::CannotEditRoot => f.write_str("cannot edit the root node"),
EditError::NotAContainer => f.write_str("node is not a container"),
EditError::SlotShapeMismatch { expected } => {
write!(f, "slot shape mismatch: expected {expected}")
}
EditError::WouldCreateCycle => f.write_str("edit would create a cycle"),
EditError::IndexOutOfBounds => f.write_str("index out of bounds"),
EditError::DuplicateChild => f.write_str("node cannot appear more than once"),
EditError::ExpectedDetachedRoot => f.write_str("expected a detached root"),
EditError::ForeignNode => f.write_str("node belongs to a different document"),
}
}
}
impl std::error::Error for EditError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ArgValue {
Math(NodeId),
Text(NodeId),
Delimiter(DelimiterValue),
CSName(String),
Dimension(String),
Integer(String),
KeyVal(String),
Column(String),
Boolean(bool),
}
impl ArgValue {
pub fn math(id: NodeId) -> Self {
Self::Math(id)
}
pub fn text(id: NodeId) -> Self {
Self::Text(id)
}
pub fn delimiter(delimiter: DelimiterValue) -> Self {
Self::Delimiter(delimiter)
}
pub fn cs_name(value: impl Into<String>) -> Self {
Self::CSName(value.into())
}
pub fn dimension(value: impl Into<String>) -> Self {
Self::Dimension(value.into())
}
pub fn integer(value: impl Into<String>) -> Self {
Self::Integer(value.into())
}
pub fn key_val(value: impl Into<String>) -> Self {
Self::KeyVal(value.into())
}
pub fn column(value: impl Into<String>) -> Self {
Self::Column(value.into())
}
pub fn boolean(value: bool) -> Self {
Self::Boolean(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DelimiterValue {
None,
Char(char),
Control(String),
}
impl DelimiterValue {
fn into_ast(self) -> Delimiter {
match self {
DelimiterValue::None => Delimiter::None,
DelimiterValue::Char(ch) => Delimiter::Char(ch),
DelimiterValue::Control(name) => Delimiter::Control(name),
}
}
}
pub struct Document {
ast: Ast,
spans: SecondaryMap<RawNodeId, Span>,
has_errors: bool,
id: DocumentId,
}
impl Document {
pub fn new() -> Self {
Self::with_mode(ContentMode::Math)
}
pub fn with_mode(mode: ContentMode) -> Self {
Document {
ast: Ast::with_root_mode(mode),
spans: SecondaryMap::new(),
has_errors: false,
id: next_document_id(),
}
}
pub fn from_syntax(node: &SyntaxNode) -> Result<Document, FromSyntaxError> {
Self::validate_syntax(node, None, true)?;
let ast = Ast::from_syntax_root(node);
let has_errors = ast
.find(ast.root(), |node| matches!(node, Node::Error { .. }))
.is_some();
Ok(Document {
ast,
spans: SecondaryMap::new(),
has_errors,
id: next_document_id(),
})
}
#[allow(dead_code)]
pub(crate) fn from_syntax_with_spans(
node: &SyntaxNode,
path_spans: &[(String, Span)],
) -> Result<Document, FromSyntaxError> {
let mut doc = Document::from_syntax(node)?;
let lookup: HashMap<&str, &Span> = path_spans
.iter()
.map(|(path, span)| (path.as_str(), span))
.collect();
let mut spans = SecondaryMap::new();
Self::assign_spans(&doc.ast, node, doc.ast.root(), "root", &lookup, &mut spans);
doc.spans = spans;
Ok(doc)
}
pub fn id(&self) -> DocumentId {
self.id
}
pub fn root(&self) -> NodeRef<'_> {
NodeRef {
doc: self,
raw: self.ast.root(),
}
}
pub fn node(&self, id: NodeId) -> Result<NodeRef<'_>, EditError> {
let raw = self.check_node_owner(id)?;
Ok(NodeRef { doc: self, raw })
}
pub fn has_errors(&self) -> bool {
self.has_errors
}
pub fn errors(&self) -> impl Iterator<Item = NodeRef<'_>> + '_ {
self.ast
.find_all(self.ast.root(), |node| matches!(node, Node::Error { .. }))
.into_iter()
.map(move |raw| NodeRef { doc: self, raw })
}
pub fn is_read_only(&self) -> bool {
self.has_errors
}
pub fn find<'a>(
&'a self,
start: NodeRef<'a>,
pred: impl Fn(NodeRef<'a>) -> bool + 'a,
) -> Option<NodeRef<'a>> {
if start.doc.id != self.id || !self.ast.contains(start.raw) {
return None;
}
self.ast
.find_all(start.raw, |_| true)
.into_iter()
.map(|raw| NodeRef { doc: self, raw })
.find(|node| pred(*node))
}
pub fn find_all<'a>(
&'a self,
start: NodeRef<'a>,
pred: impl Fn(NodeRef<'a>) -> bool + 'a,
) -> impl Iterator<Item = NodeRef<'a>> + 'a {
let raws = if start.doc.id == self.id && self.ast.contains(start.raw) {
self.ast.find_all(start.raw, |_| true)
} else {
Vec::new()
};
raws.into_iter().filter_map(move |raw| {
let node = NodeRef { doc: self, raw };
pred(node).then_some(node)
})
}
pub fn find_commands<'a>(&'a self, name: &'a str) -> impl Iterator<Item = NodeRef<'a>> + 'a {
self.find_all(self.root(), move |node| node.is_command(name))
}
pub fn find_environments<'a>(
&'a self,
name: &'a str,
) -> impl Iterator<Item = NodeRef<'a>> + 'a {
self.find_all(self.root(), move |node| node.env_name() == Some(name))
}
pub fn to_syntax(&self) -> SyntaxNode {
self.ast.to_syntax_root()
}
#[doc(hidden)]
pub fn __texform_engine_ast_mut(&mut self) -> &mut Ast {
&mut self.ast
}
pub fn to_latex(&self) -> Result<String, SerializeError> {
Ok(serialize(&self.ast))
}
pub fn to_latex_with(&self, options: &SerializeOptions) -> Result<String, SerializeError> {
Ok(serialize_with(&self.ast, options))
}
pub fn create_char(&mut self, c: char) -> Result<NodeId, EditError> {
self.create_node(Node::Char(c))
}
pub fn create_text(&mut self, s: impl Into<String>) -> Result<NodeId, EditError> {
self.create_node(Node::Text(s.into()))
}
pub fn create_active_space(&mut self) -> Result<NodeId, EditError> {
self.create_node(Node::ActiveSpace)
}
pub fn create_group(&mut self, mode: ContentMode) -> Result<NodeId, EditError> {
self.create_node(Node::Group {
children: Vec::new(),
kind: GroupKind::Explicit,
mode,
})
}
pub fn create_command(
&mut self,
name: impl Into<String>,
args: Vec<ArgValue>,
) -> Result<NodeId, EditError> {
self.check_writable()?;
let args = self.build_arg_slots(args)?;
self.create_node(Node::Command {
name: name.into(),
args,
known: false,
})
}
pub fn create_declarative(
&mut self,
name: impl Into<String>,
args: Vec<ArgValue>,
) -> Result<NodeId, EditError> {
self.check_writable()?;
let args = self.build_arg_slots(args)?;
self.create_node(Node::Declarative {
name: name.into(),
args,
})
}
pub fn create_environment(
&mut self,
name: impl Into<String>,
args: Vec<ArgValue>,
body: NodeId,
) -> Result<NodeId, EditError> {
self.check_writable()?;
let body = self.check_node_owner(body)?;
self.check_detached(body)?;
if !matches!(self.ast.node_opt(body), Some(Node::Group { .. })) {
return Err(EditError::SlotShapeMismatch { expected: "group" });
}
let args = self.build_arg_slots(args)?;
self.create_node(Node::Environment {
name: name.into(),
args,
known: false,
body,
})
}
pub fn append_child(&mut self, parent: NodeId, child: NodeId) -> Result<(), EditError> {
self.check_writable()?;
self.insert_child_at(parent, self.child_len(parent)?, child)
}
pub fn insert_before(&mut self, anchor: NodeId, new: NodeId) -> Result<(), EditError> {
self.check_writable()?;
let (parent, index) = self.group_child_position(anchor)?;
self.insert_child_at(parent, index, new)
}
pub fn insert_after(&mut self, anchor: NodeId, new: NodeId) -> Result<(), EditError> {
self.check_writable()?;
let (parent, index) = self.group_child_position(anchor)?;
self.insert_child_at(parent, index + 1, new)
}
pub fn insert_child(
&mut self,
parent: NodeId,
index: usize,
child: NodeId,
) -> Result<(), EditError> {
self.check_writable()?;
self.insert_child_at(parent, index, child)
}
pub fn extract(&mut self, id: NodeId) -> Result<NodeId, EditError> {
self.check_writable()?;
let raw = self.check_node_owner(id)?;
if raw == self.ast.root() {
return Err(EditError::CannotEditRoot);
}
if !matches!(self.ast.slot(raw), Some(Slot::GroupChild(_))) {
return Err(EditError::SlotShapeMismatch {
expected: "group child",
});
}
Ok(NodeId::new(self.id, self.ast.detach(raw)))
}
pub fn remove(&mut self, id: NodeId) -> Result<(), EditError> {
self.check_writable()?;
let raw = self.extract(id)?.raw();
self.ast.remove_detached(raw);
Ok(())
}
pub fn replace_with(&mut self, target: NodeId, replacement: NodeId) -> Result<(), EditError> {
self.check_writable()?;
let target = self.check_node_owner(target)?;
let replacement = self.check_node_owner(replacement)?;
if target == self.ast.root() {
return Err(EditError::CannotEditRoot);
}
self.check_detached(replacement)?;
self.check_no_cycle(replacement, target)?;
match self.ast.slot(target) {
Some(Slot::GroupChild(index)) => {
let parent = self.ast.parent_id(target).ok_or(EditError::NodeNotFound)?;
let detached = self.ast.detach(target);
self.ast.insert_child(parent, index, replacement);
self.ast.remove_detached(detached);
Ok(())
}
Some(_) => {
let slot = self.ast.slot(target).ok_or(EditError::NodeNotFound)?;
self.check_slot_shape(slot, replacement)?;
self.ast.replace_content_child(target, replacement);
if self.ast.contains(target)
&& self.ast.parent_id(target).is_none()
&& target != self.ast.root()
{
self.ast.remove_detached(target);
}
Ok(())
}
None => Err(EditError::NodeNotFound),
}
}
pub fn clear(&mut self, container: NodeId) -> Result<(), EditError> {
self.check_writable()?;
let raw = self.check_node_owner(container)?;
self.check_container(raw)?;
let len = self.ast.children(raw).len();
let removed = self.ast.detach_children_range(raw, 0..len);
for child in removed {
self.ast.remove_detached(child);
}
Ok(())
}
pub fn set_command_name(
&mut self,
id: NodeId,
name: impl Into<String>,
) -> Result<(), EditError> {
self.check_writable()?;
let raw = self.check_node_owner(id)?;
match self.ast.node_opt_mut(raw) {
Some(Node::Command { name: current, .. })
| Some(Node::Infix { name: current, .. })
| Some(Node::Declarative { name: current, .. })
| Some(Node::Environment { name: current, .. }) => {
*current = name.into();
Ok(())
}
Some(_) => Err(EditError::SlotShapeMismatch {
expected: "command-like node",
}),
None => Err(EditError::NodeNotFound),
}
}
pub fn set_text(&mut self, id: NodeId, s: impl Into<String>) -> Result<(), EditError> {
self.check_writable()?;
let raw = self.check_node_owner(id)?;
match self.ast.node_opt_mut(raw) {
Some(Node::Text(text)) => {
*text = s.into();
Ok(())
}
Some(_) => Err(EditError::SlotShapeMismatch {
expected: "text node",
}),
None => Err(EditError::NodeNotFound),
}
}
pub fn set_char(&mut self, id: NodeId, c: char) -> Result<(), EditError> {
self.check_writable()?;
let raw = self.check_node_owner(id)?;
match self.ast.node_opt_mut(raw) {
Some(Node::Char(ch)) => {
*ch = c;
Ok(())
}
Some(_) => Err(EditError::SlotShapeMismatch {
expected: "char node",
}),
None => Err(EditError::NodeNotFound),
}
}
pub fn set_arg(&mut self, id: NodeId, index: usize, value: ArgValue) -> Result<(), EditError> {
self.check_writable()?;
let raw = self.check_node_owner(id)?;
let kind = match self.ast.node_opt(raw) {
Some(
Node::Command { args, .. }
| Node::Infix { args, .. }
| Node::Declarative { args, .. }
| Node::Environment { args, .. },
) => {
let Some(slot) = args.get(index) else {
return Err(EditError::IndexOutOfBounds);
};
let Some(argument) = slot else {
return Err(EditError::SlotShapeMismatch {
expected: "filled argument slot",
});
};
argument.kind.clone()
}
Some(_) => {
return Err(EditError::SlotShapeMismatch {
expected: "command-like node",
});
}
None => return Err(EditError::NodeNotFound),
};
let value = self.build_arg_value_for_kind(value, &kind)?;
self.replace_single_arg_value(raw, index, value)
}
pub fn wrap(&mut self, target: NodeId, wrapper: NodeId) -> Result<NodeId, EditError> {
self.check_writable()?;
let target = self.check_node_owner(target)?;
let wrapper = self.check_node_owner(wrapper)?;
if target == self.ast.root() {
return Err(EditError::CannotEditRoot);
}
self.check_container(wrapper)?;
self.check_detached(wrapper)?;
let (parent, index) = match self.ast.slot(target) {
Some(Slot::GroupChild(index)) => (
self.ast.parent_id(target).ok_or(EditError::NodeNotFound)?,
index,
),
Some(_) => {
return Err(EditError::SlotShapeMismatch {
expected: "group child",
});
}
None => return Err(EditError::NodeNotFound),
};
self.check_no_cycle(wrapper, parent)?;
let target = self.ast.detach(target);
self.ast.insert_child(parent, index, wrapper);
self.ast.append_child(wrapper, target);
Ok(NodeId::new(self.id, wrapper))
}
pub fn unwrap(&mut self, group: NodeId) -> Result<Vec<NodeId>, EditError> {
self.check_writable()?;
let group = self.check_node_owner(group)?;
if group == self.ast.root() {
return Err(EditError::CannotEditRoot);
}
if !matches!(self.ast.node_opt(group), Some(Node::Group { .. })) {
return Err(EditError::SlotShapeMismatch { expected: "group" });
}
let (parent, index) = match self.ast.slot(group) {
Some(Slot::GroupChild(index)) => (
self.ast.parent_id(group).ok_or(EditError::NodeNotFound)?,
index,
),
Some(_) => {
return Err(EditError::SlotShapeMismatch {
expected: "group child",
});
}
None => return Err(EditError::NodeNotFound),
};
let count = self.ast.children(group).len();
let children = self.ast.detach_children_range(group, 0..count);
let detached_group = self.ast.detach(group);
self.ast.remove_detached(detached_group);
for (offset, child) in children.iter().copied().enumerate() {
self.ast.insert_child(parent, index + offset, child);
}
Ok(children
.into_iter()
.map(|raw| NodeId::new(self.id, raw))
.collect())
}
fn create_node(&mut self, node: Node) -> Result<NodeId, EditError> {
self.check_writable()?;
self.check_unique_direct_children(&node)?;
Ok(NodeId::new(self.id, self.ast.new_node(node)))
}
fn child_len(&self, parent: NodeId) -> Result<usize, EditError> {
let raw = self.check_node_owner(parent)?;
self.check_container(raw)?;
Ok(self.ast.children(raw).len())
}
fn check_writable(&self) -> Result<(), EditError> {
if self.has_errors {
Err(EditError::ReadOnlyDocument)
} else {
Ok(())
}
}
fn check_node_owner(&self, id: NodeId) -> Result<RawNodeId, EditError> {
if id.document() != self.id {
return Err(EditError::ForeignNode);
}
let raw = id.raw();
if self.ast.contains(raw) {
Ok(raw)
} else {
Err(EditError::NodeNotFound)
}
}
fn check_container(&self, id: RawNodeId) -> Result<(), EditError> {
match self.ast.node_opt(id) {
Some(Node::Root { .. }) | Some(Node::Group { .. }) => Ok(()),
Some(_) => Err(EditError::NotAContainer),
None => Err(EditError::NodeNotFound),
}
}
fn check_detached(&self, id: RawNodeId) -> Result<(), EditError> {
if !self.ast.contains(id) {
return Err(EditError::NodeNotFound);
}
if id == self.ast.root() {
return Err(EditError::CannotEditRoot);
}
if self.ast.parent_id(id).is_some() || !self.ast.is_detached_root(id) {
return Err(EditError::ExpectedDetachedRoot);
}
Ok(())
}
fn check_no_cycle(&self, child: RawNodeId, new_parent: RawNodeId) -> Result<(), EditError> {
if self.ast.subtree_contains_node(child, new_parent) {
Err(EditError::WouldCreateCycle)
} else {
Ok(())
}
}
fn check_slot_shape(&self, slot: Slot, child: RawNodeId) -> Result<(), EditError> {
if matches!(slot, Slot::EnvBody)
&& !matches!(self.ast.node_opt(child), Some(Node::Group { .. }))
{
Err(EditError::SlotShapeMismatch { expected: "group" })
} else {
Ok(())
}
}
fn check_unique_direct_children(&self, node: &Node) -> Result<(), EditError> {
let mut seen = HashSet::new();
for child in Self::direct_children(node) {
if !seen.insert(child) {
return Err(EditError::DuplicateChild);
}
}
Ok(())
}
fn direct_children(node: &Node) -> Vec<RawNodeId> {
let mut children = Vec::new();
match node {
Node::Root {
children: group_children,
..
}
| Node::Group {
children: group_children,
..
} => children.extend(group_children.iter().copied()),
Node::Command { args, .. } | Node::Declarative { args, .. } => {
Self::push_arg_children(args, &mut children);
}
Node::Infix {
args, left, right, ..
} => {
children.push(*left);
Self::push_arg_children(args, &mut children);
children.push(*right);
}
Node::Environment { args, body, .. } => {
Self::push_arg_children(args, &mut children);
children.push(*body);
}
Node::Scripted {
base,
subscript,
superscript,
} => {
children.push(*base);
children.extend(*subscript);
children.extend(*superscript);
}
Node::Prime { .. }
| Node::Text(_)
| Node::Char(_)
| Node::ActiveSpace
| Node::Error { .. } => {}
}
children
}
fn validate_syntax(
node: &SyntaxNode,
current_mode: Option<ContentMode>,
is_top_level: bool,
) -> Result<(), FromSyntaxError> {
if is_top_level && !matches!(node, SyntaxNode::Root { .. }) {
return Err(FromSyntaxError::NotARoot);
}
match node {
SyntaxNode::Root { mode, children } => {
if !is_top_level {
return Err(FromSyntaxError::NotARoot);
}
for child in children {
Self::validate_syntax(child, Some(*mode), false)?;
}
}
SyntaxNode::Group { mode, children, .. } => {
for child in children {
Self::validate_syntax(child, Some(*mode), false)?;
}
}
SyntaxNode::Command { args, .. } | SyntaxNode::Declarative { args, .. } => {
Self::validate_syntax_args(args)?;
}
SyntaxNode::Infix {
args, left, right, ..
} => {
Self::validate_syntax_args(args)?;
Self::validate_syntax(left, current_mode, false)?;
Self::validate_syntax(right, current_mode, false)?;
}
SyntaxNode::Environment { args, body, .. } => {
Self::validate_syntax_args(args)?;
Self::validate_syntax(body, None, false)?;
}
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
Self::validate_syntax(base, current_mode, false)?;
if let Some(subscript) = subscript {
Self::validate_syntax(subscript, current_mode, false)?;
}
if let Some(superscript) = superscript {
Self::validate_syntax(superscript, current_mode, false)?;
}
}
SyntaxNode::Prime { count } => {
if *count == 0 {
return Err(FromSyntaxError::InvalidPrimeCount);
}
if current_mode != Some(ContentMode::Math) {
return Err(FromSyntaxError::PrimeInTextMode);
}
}
SyntaxNode::Error { .. }
| SyntaxNode::Text(_)
| SyntaxNode::Char(_)
| SyntaxNode::ActiveSpace => {}
}
Ok(())
}
fn validate_syntax_args(args: &[syntax_node::ArgumentSlot]) -> Result<(), FromSyntaxError> {
for arg in args.iter().flatten() {
match &arg.value {
syntax_node::ArgumentValue::MathContent(node) => {
Self::validate_syntax(node, Some(ContentMode::Math), false)?;
}
syntax_node::ArgumentValue::TextContent(node) => {
Self::validate_syntax(node, Some(ContentMode::Text), false)?;
}
_ => {}
}
}
Ok(())
}
fn push_arg_children(args: &[ArgumentSlot], out: &mut Vec<RawNodeId>) {
for arg in args.iter().flatten() {
match &arg.value {
ArgumentValue::MathContent(child) | ArgumentValue::TextContent(child) => {
out.push(*child);
}
_ => {}
}
}
}
fn group_child_position(&self, anchor: NodeId) -> Result<(NodeId, usize), EditError> {
let raw = self.check_node_owner(anchor)?;
match self.ast.slot(raw) {
Some(Slot::GroupChild(index)) => {
let parent = self.ast.parent_id(raw).ok_or(EditError::NodeNotFound)?;
Ok((NodeId::new(self.id, parent), index))
}
Some(_) => Err(EditError::SlotShapeMismatch {
expected: "group child",
}),
None => Err(EditError::NodeNotFound),
}
}
fn insert_child_at(
&mut self,
parent: NodeId,
index: usize,
child: NodeId,
) -> Result<(), EditError> {
self.check_writable()?;
let parent = self.check_node_owner(parent)?;
let child = self.check_node_owner(child)?;
self.check_container(parent)?;
self.check_detached(child)?;
self.check_no_cycle(child, parent)?;
if index > self.ast.children(parent).len() {
return Err(EditError::IndexOutOfBounds);
}
self.ast.insert_child(parent, index, child);
Ok(())
}
fn build_arg_slots(&mut self, args: Vec<ArgValue>) -> Result<Vec<ArgumentSlot>, EditError> {
args.into_iter()
.map(|arg| self.build_arg_slot(arg))
.collect()
}
fn build_arg_slot(&mut self, value: ArgValue) -> Result<ArgumentSlot, EditError> {
let (kind, value) = match value {
ArgValue::Math(id) => {
let raw = self.check_node_owner(id)?;
self.check_detached(raw)?;
(ArgumentKind::Mandatory, ArgumentValue::MathContent(raw))
}
ArgValue::Text(id) => {
let raw = self.check_node_owner(id)?;
self.check_detached(raw)?;
(ArgumentKind::Mandatory, ArgumentValue::TextContent(raw))
}
ArgValue::Delimiter(d) => (
ArgumentKind::Mandatory,
ArgumentValue::Delimiter(d.into_ast()),
),
ArgValue::CSName(s) => (ArgumentKind::Mandatory, ArgumentValue::CSName(s)),
ArgValue::Dimension(s) => (ArgumentKind::Mandatory, ArgumentValue::Dimension(s)),
ArgValue::Integer(s) => (ArgumentKind::Mandatory, ArgumentValue::Integer(s)),
ArgValue::KeyVal(s) => (ArgumentKind::Mandatory, ArgumentValue::KeyVal(s)),
ArgValue::Column(s) => (ArgumentKind::Mandatory, ArgumentValue::Column(s)),
ArgValue::Boolean(b) => (ArgumentKind::Star, ArgumentValue::Boolean(b)),
};
Ok(Some(Argument { kind, value }))
}
fn build_arg_value_for_kind(
&self,
value: ArgValue,
kind: &ArgumentKind,
) -> Result<ArgumentValue, EditError> {
match (kind, value) {
(ArgumentKind::Star, ArgValue::Boolean(value)) => Ok(ArgumentValue::Boolean(value)),
(ArgumentKind::Star, _) => Err(EditError::SlotShapeMismatch {
expected: "boolean value",
}),
(_, ArgValue::Boolean(_)) => Err(EditError::SlotShapeMismatch {
expected: "non-boolean argument value",
}),
(_, ArgValue::Math(id)) => {
let raw = self.check_node_owner(id)?;
self.check_detached(raw)?;
Ok(ArgumentValue::MathContent(raw))
}
(_, ArgValue::Text(id)) => {
let raw = self.check_node_owner(id)?;
self.check_detached(raw)?;
Ok(ArgumentValue::TextContent(raw))
}
(_, ArgValue::Delimiter(delimiter)) => {
Ok(ArgumentValue::Delimiter(delimiter.into_ast()))
}
(_, ArgValue::CSName(value)) => Ok(ArgumentValue::CSName(value)),
(_, ArgValue::Dimension(value)) => Ok(ArgumentValue::Dimension(value)),
(_, ArgValue::Integer(value)) => Ok(ArgumentValue::Integer(value)),
(_, ArgValue::KeyVal(value)) => Ok(ArgumentValue::KeyVal(value)),
(_, ArgValue::Column(value)) => Ok(ArgumentValue::Column(value)),
}
}
fn replace_single_arg_value(
&mut self,
id: RawNodeId,
index: usize,
value: ArgumentValue,
) -> Result<(), EditError> {
let old_content = self
.ast
.arg_slots(id)
.get(index)
.and_then(|slot| slot.as_ref())
.and_then(|arg| match &arg.value {
ArgumentValue::MathContent(content) | ArgumentValue::TextContent(content) => {
Some(*content)
}
_ => None,
});
let mut node = self.ast.node(id).clone();
match &mut node {
Node::Command { args, .. }
| Node::Infix { args, .. }
| Node::Declarative { args, .. }
| Node::Environment { args, .. } => {
let Some(Some(argument)) = args.get_mut(index) else {
return Err(EditError::SlotShapeMismatch {
expected: "filled argument slot",
});
};
argument.value = value;
}
_ => {
return Err(EditError::SlotShapeMismatch {
expected: "command-like node",
});
}
}
self.ast.replace_node(id, node);
if let Some(old) = old_content
&& self.ast.contains(old)
&& self.ast.parent_id(old).is_none()
&& old != self.ast.root()
{
self.ast.remove_detached(old);
}
Ok(())
}
pub fn node_spans(&self) -> Vec<(String, Span)> {
let mut out = Vec::new();
self.collect_node_spans(self.ast.root(), "root", &mut out);
out
}
fn collect_node_spans(&self, id: RawNodeId, path: &str, out: &mut Vec<(String, Span)>) {
if let Some(span) = self.spans.get(id) {
out.push((path.to_string(), span.clone()));
}
match self.ast.node(id) {
Node::Root { children, .. } | Node::Group { children, .. } => {
for (index, child) in children.iter().enumerate() {
self.collect_node_spans(*child, &format!("{path}.child.{index}"), out);
}
}
Node::Command { args, .. } | Node::Declarative { args, .. } => {
self.collect_arg_node_spans(args, path, out);
}
Node::Infix {
args, left, right, ..
} => {
self.collect_node_spans(*left, &format!("{path}.left"), out);
self.collect_arg_node_spans(args, path, out);
self.collect_node_spans(*right, &format!("{path}.right"), out);
}
Node::Environment { args, body, .. } => {
self.collect_arg_node_spans(args, path, out);
self.collect_node_spans(*body, &format!("{path}.body"), out);
}
Node::Scripted {
base,
subscript,
superscript,
} => {
self.collect_node_spans(*base, &format!("{path}.base"), out);
if let Some(sub) = subscript {
self.collect_node_spans(*sub, &format!("{path}.sub"), out);
}
if let Some(sup) = superscript {
self.collect_node_spans(*sup, &format!("{path}.sup"), out);
}
}
_ => {}
}
}
fn collect_arg_node_spans(
&self,
args: &[ArgumentSlot],
path: &str,
out: &mut Vec<(String, Span)>,
) {
for (index, slot) in args.iter().enumerate() {
let Some(arg) = slot else { continue };
if let ArgumentValue::MathContent(id) | ArgumentValue::TextContent(id) = &arg.value {
self.collect_node_spans(*id, &format!("{path}.arg.{index}.content"), out);
}
}
}
#[allow(dead_code)]
fn assign_spans(
ast: &Ast,
syntax: &SyntaxNode,
id: RawNodeId,
path: &str,
lookup: &HashMap<&str, &Span>,
out: &mut SecondaryMap<RawNodeId, Span>,
) {
if let Some(span) = lookup.get(path) {
out.insert(id, (*span).clone());
}
match (syntax, ast.node(id)) {
(
SyntaxNode::Root {
children: syntax_children,
..
},
Node::Root { children, .. },
)
| (
SyntaxNode::Group {
children: syntax_children,
..
},
Node::Group { children, .. },
) => {
for (index, (syntax_child, ast_child)) in
syntax_children.iter().zip(children.iter()).enumerate()
{
Self::assign_spans(
ast,
syntax_child,
*ast_child,
&format!("{path}.child.{index}"),
lookup,
out,
);
}
}
(
SyntaxNode::Command {
args: syntax_args, ..
},
Node::Command { args: ast_args, .. },
)
| (
SyntaxNode::Declarative {
args: syntax_args, ..
},
Node::Declarative { args: ast_args, .. },
) => Self::assign_arg_spans(ast, syntax_args, ast_args, path, lookup, out),
(
SyntaxNode::Infix {
args: syntax_args,
left: syntax_left,
right: syntax_right,
..
},
Node::Infix {
args: ast_args,
left,
right,
..
},
) => {
Self::assign_spans(
ast,
syntax_left,
*left,
&format!("{path}.left"),
lookup,
out,
);
Self::assign_arg_spans(ast, syntax_args, ast_args, path, lookup, out);
Self::assign_spans(
ast,
syntax_right,
*right,
&format!("{path}.right"),
lookup,
out,
);
}
(
SyntaxNode::Environment {
args: syntax_args,
body: syntax_body,
..
},
Node::Environment {
args: ast_args,
body,
..
},
) => {
Self::assign_arg_spans(ast, syntax_args, ast_args, path, lookup, out);
Self::assign_spans(
ast,
syntax_body,
*body,
&format!("{path}.body"),
lookup,
out,
);
}
(
SyntaxNode::Scripted {
base: syntax_base,
subscript: syntax_subscript,
superscript: syntax_superscript,
},
Node::Scripted {
base,
subscript,
superscript,
},
) => {
Self::assign_spans(
ast,
syntax_base,
*base,
&format!("{path}.base"),
lookup,
out,
);
if let (Some(syntax), Some(ast_id)) = (syntax_subscript, subscript) {
Self::assign_spans(ast, syntax, *ast_id, &format!("{path}.sub"), lookup, out);
}
if let (Some(syntax), Some(ast_id)) = (syntax_superscript, superscript) {
Self::assign_spans(ast, syntax, *ast_id, &format!("{path}.sup"), lookup, out);
}
}
_ => {}
}
}
#[allow(dead_code)]
fn assign_arg_spans(
ast: &Ast,
syntax_args: &[syntax_node::ArgumentSlot],
ast_args: &[ArgumentSlot],
path: &str,
lookup: &HashMap<&str, &Span>,
out: &mut SecondaryMap<RawNodeId, Span>,
) {
for (index, (syntax_slot, ast_slot)) in syntax_args.iter().zip(ast_args.iter()).enumerate()
{
let (Some(syntax_arg), Some(ast_arg)) = (syntax_slot, ast_slot) else {
continue;
};
let content_path = format!("{path}.arg.{index}.content");
if let (
syntax_node::ArgumentValue::MathContent(syntax_node)
| syntax_node::ArgumentValue::TextContent(syntax_node),
ArgumentValue::MathContent(ast_id) | ArgumentValue::TextContent(ast_id),
) = (&syntax_arg.value, &ast_arg.value)
{
Self::assign_spans(ast, syntax_node, *ast_id, &content_path, lookup, out);
}
}
}
}
impl Default for Document {
fn default() -> Self {
Self::new()
}
}
impl Clone for Document {
fn clone(&self) -> Self {
Document {
ast: self.ast.clone(),
spans: self.spans.clone(),
has_errors: self.has_errors,
id: next_document_id(),
}
}
}
impl std::fmt::Debug for Document {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Document")
.field("id", &self.id)
.field("root", &self.ast.root())
.field("has_errors", &self.has_errors)
.finish()
}
}
impl std::fmt::Display for Document {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let latex = self.to_latex().map_err(|_| std::fmt::Error)?;
f.write_str(&latex)
}
}
#[derive(Clone, Copy)]
pub struct NodeRef<'a> {
doc: &'a Document,
raw: RawNodeId,
}
impl std::fmt::Debug for NodeRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeRef").field("id", &self.id()).finish()
}
}
impl<'a> NodeRef<'a> {
pub fn id(&self) -> NodeId {
NodeId::new(self.doc.id, self.raw)
}
pub fn kind(&self) -> NodeKind {
self.doc.ast.kind(self.raw)
}
pub fn is_command(&self, name: &str) -> bool {
matches!(self.node(), Node::Command { name: current, .. } if current == name)
}
pub fn is_char(&self, c: char) -> bool {
matches!(self.node(), Node::Char(ch) if *ch == c)
}
pub fn is_error(&self) -> bool {
matches!(self.node(), Node::Error { .. })
}
pub fn parent(&self) -> Option<NodeRef<'a>> {
self.doc
.ast
.parent_id(self.raw)
.map(|raw| self.sibling(raw))
}
pub fn children(&self) -> impl Iterator<Item = NodeRef<'a>> + 'a {
let doc = self.doc;
doc.ast
.children(self.raw)
.to_vec()
.into_iter()
.map(move |raw| NodeRef { doc, raw })
}
pub fn next_sibling(&self) -> Option<NodeRef<'a>> {
self.doc
.ast
.next_sibling(self.raw)
.map(|raw| self.sibling(raw))
}
pub fn prev_sibling(&self) -> Option<NodeRef<'a>> {
self.doc
.ast
.prev_sibling(self.raw)
.map(|raw| self.sibling(raw))
}
pub fn ancestors(&self) -> impl Iterator<Item = NodeRef<'a>> + 'a {
let doc = self.doc;
let mut current = doc.ast.parent_id(self.raw);
std::iter::from_fn(move || {
let raw = current?;
current = doc.ast.parent_id(raw);
Some(NodeRef { doc, raw })
})
}
pub fn descendants(&self) -> impl Iterator<Item = NodeRef<'a>> + 'a {
let doc = self.doc;
let start = self.raw;
doc.ast
.find_all(start, |_| true)
.into_iter()
.filter(move |raw| *raw != start)
.map(move |raw| NodeRef { doc, raw })
}
pub fn command_name(&self) -> Option<&'a str> {
match self.node() {
Node::Command { name, .. }
| Node::Infix { name, .. }
| Node::Declarative { name, .. } => Some(name),
_ => None,
}
}
pub fn env_name(&self) -> Option<&'a str> {
match self.node() {
Node::Environment { name, .. } => Some(name),
_ => None,
}
}
pub fn text(&self) -> Option<&'a str> {
match self.node() {
Node::Text(text) => Some(text),
_ => None,
}
}
pub fn char(&self) -> Option<char> {
match self.node() {
Node::Char(ch) => Some(*ch),
_ => None,
}
}
pub fn prime_count(&self) -> Option<usize> {
match self.node() {
Node::Prime { count } => Some(*count),
_ => None,
}
}
pub fn error_parts(&self) -> Option<(&'a str, &'a str)> {
match self.node() {
Node::Error { message, snippet } => Some((message, snippet)),
_ => None,
}
}
pub fn content_mode(&self) -> Option<ContentMode> {
match self.node() {
Node::Root { mode, .. } | Node::Group { mode, .. } => Some(*mode),
_ => None,
}
}
pub fn group_kind(&self) -> Option<GroupKindRef<'a>> {
match self.node() {
Node::Group { kind, .. } => Some(self.group_kind_ref(kind)),
_ => None,
}
}
pub fn arg_count(&self) -> usize {
self.doc.ast.arg_slots(self.raw).len()
}
pub fn arg(&self, index: usize) -> Option<ArgRef<'a>> {
let arg = self.doc.ast.arg_slots(self.raw).get(index)?.as_ref()?;
Some(self.arg_ref(arg))
}
pub fn arg_slots(&self) -> impl Iterator<Item = Option<ArgRef<'a>>> + 'a {
let this = *self;
(0..self.arg_count()).map(move |index| this.arg(index))
}
pub fn script_base(&self) -> Option<NodeRef<'a>> {
match self.node() {
Node::Scripted { base, .. } => Some(self.sibling(*base)),
_ => None,
}
}
pub fn subscript(&self) -> Option<NodeRef<'a>> {
match self.node() {
Node::Scripted { subscript, .. } => subscript.map(|raw| self.sibling(raw)),
_ => None,
}
}
pub fn superscript(&self) -> Option<NodeRef<'a>> {
match self.node() {
Node::Scripted { superscript, .. } => superscript.map(|raw| self.sibling(raw)),
_ => None,
}
}
pub fn infix_left(&self) -> Option<NodeRef<'a>> {
match self.node() {
Node::Infix { left, .. } => Some(self.sibling(*left)),
_ => None,
}
}
pub fn infix_right(&self) -> Option<NodeRef<'a>> {
match self.node() {
Node::Infix { right, .. } => Some(self.sibling(*right)),
_ => None,
}
}
pub fn env_body(&self) -> Option<NodeRef<'a>> {
match self.node() {
Node::Environment { body, .. } => Some(self.sibling(*body)),
_ => None,
}
}
pub fn span(&self) -> Option<Span> {
self.doc.spans.get(self.raw).cloned()
}
fn node(&self) -> &'a Node {
self.doc.ast.node(self.raw)
}
fn sibling(&self, raw: RawNodeId) -> NodeRef<'a> {
NodeRef { doc: self.doc, raw }
}
fn arg_ref(&self, arg: &'a Argument) -> ArgRef<'a> {
match &arg.value {
ArgumentValue::MathContent(id) => ArgRef::Math(self.sibling(*id)),
ArgumentValue::TextContent(id) => ArgRef::Text(self.sibling(*id)),
ArgumentValue::Delimiter(delimiter) => {
ArgRef::Delimiter(Self::delimiter_ref(delimiter))
}
ArgumentValue::CSName(value) => ArgRef::CSName(value),
ArgumentValue::Dimension(value) => ArgRef::Dimension(value),
ArgumentValue::Integer(value) => ArgRef::Integer(value),
ArgumentValue::KeyVal(value) => ArgRef::KeyVal(value),
ArgumentValue::Column(value) => ArgRef::Column(value),
ArgumentValue::Boolean(value) => ArgRef::Boolean(*value),
}
}
fn group_kind_ref(&self, kind: &'a GroupKind) -> GroupKindRef<'a> {
match kind {
GroupKind::Explicit => GroupKindRef::Explicit,
GroupKind::Implicit => GroupKindRef::Implicit,
GroupKind::Delimited { left, right } => GroupKindRef::Delimited {
left: Self::delimiter_ref(left),
right: Self::delimiter_ref(right),
},
GroupKind::InlineMath => GroupKindRef::InlineMath,
}
}
fn delimiter_ref(delimiter: &'a Delimiter) -> DelimiterRef<'a> {
match delimiter {
Delimiter::None => DelimiterRef::None,
Delimiter::Char(ch) => DelimiterRef::Char(*ch),
Delimiter::Control(name) => DelimiterRef::Control(name),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DelimiterRef<'a> {
None,
Char(char),
Control(&'a str),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GroupKindRef<'a> {
Explicit,
Implicit,
Delimited {
left: DelimiterRef<'a>,
right: DelimiterRef<'a>,
},
InlineMath,
}
#[derive(Clone, Copy, Debug)]
pub enum ArgRef<'a> {
Math(NodeRef<'a>),
Text(NodeRef<'a>),
Delimiter(DelimiterRef<'a>),
CSName(&'a str),
Dimension(&'a str),
Integer(&'a str),
KeyVal(&'a str),
Column(&'a str),
Boolean(bool),
}
impl<'a> ArgRef<'a> {
pub fn as_node(self) -> Option<NodeRef<'a>> {
match self {
ArgRef::Math(node) | ArgRef::Text(node) => Some(node),
_ => None,
}
}
}
#[cfg(test)]
impl Document {
fn from_ast_for_test(build: impl FnOnce(&mut Ast)) -> Self {
let mut ast = Ast::new();
build(&mut ast);
ast.assert_invariants();
Document {
ast,
spans: SecondaryMap::new(),
has_errors: false,
id: next_document_id(),
}
}
fn from_ast_with_errors_for_test(build: impl FnOnce(&mut Ast)) -> Self {
let mut ast = Ast::new();
build(&mut ast);
ast.assert_invariants();
let has_errors = ast
.find(ast.root(), |node| matches!(node, Node::Error { .. }))
.is_some();
Document {
ast,
spans: SecondaryMap::new(),
has_errors,
id: next_document_id(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_document_is_empty_and_editable() {
let doc = Document::new();
assert_eq!(doc.root().kind(), NodeKind::Root);
assert!(!doc.has_errors());
assert!(!doc.is_read_only());
assert_eq!(doc.errors().count(), 0);
}
#[test]
fn document_ids_are_unique_across_construction_and_clone() {
let a = Document::new();
let b = Document::new();
assert_ne!(a.id(), b.id());
let c = a.clone();
assert_ne!(a.id(), c.id());
}
#[test]
fn node_ref_reads_synthesized_chars() {
let doc = Document::from_ast_for_test(|ast| {
let a = ast.new_node(Node::Char('a'));
let b = ast.new_node(Node::Char('b'));
ast.append_child(ast.root(), a);
ast.append_child(ast.root(), b);
});
let mut children = doc.root().children();
let first = children.next().unwrap();
let second = children.next().unwrap();
assert!(first.is_char('a'));
assert!(second.is_char('b'));
assert_eq!(first.next_sibling().map(|n| n.id()), Some(second.id()));
assert_eq!(second.prev_sibling().map(|n| n.id()), Some(first.id()));
assert_eq!(doc.root().children().count(), 2);
}
#[test]
fn find_all_collects_matching_nodes() {
let doc = Document::from_ast_for_test(|ast| {
let a = ast.new_node(Node::Char('a'));
let group = ast.new_node(Node::Group {
children: vec![a],
kind: crate::ast::GroupKind::Explicit,
mode: ContentMode::Math,
});
let b = ast.new_node(Node::Char('b'));
ast.append_child(ast.root(), group);
ast.append_child(ast.root(), b);
});
let chars: Vec<_> = doc
.find_all(doc.root(), |node| node.char().is_some())
.filter_map(|node| node.char())
.collect();
assert_eq!(chars, vec!['a', 'b']);
}
#[test]
fn edit_error_implements_display_and_error() {
fn assert_error<E: std::error::Error>() {}
assert_error::<EditError>();
assert_eq!(
EditError::CannotEditRoot.to_string(),
"cannot edit the root node"
);
assert_eq!(
EditError::SlotShapeMismatch { expected: "group" }.to_string(),
"slot shape mismatch: expected group"
);
}
#[test]
fn create_and_append_children() {
let mut doc = Document::new();
let a = doc.create_char('a').unwrap();
let b = doc.create_char('b').unwrap();
let root = doc.root().id();
doc.append_child(root, a).unwrap();
doc.append_child(root, b).unwrap();
let chars: Vec<_> = doc
.root()
.children()
.filter_map(|node| node.char())
.collect();
assert_eq!(chars, vec!['a', 'b']);
}
#[test]
fn insert_before_and_after() {
let mut doc = Document::new();
let a = doc.create_char('a').unwrap();
let c = doc.create_char('c').unwrap();
let b = doc.create_char('b').unwrap();
let d = doc.create_char('d').unwrap();
let root = doc.root().id();
doc.append_child(root, a).unwrap();
doc.append_child(root, c).unwrap();
doc.insert_before(c, b).unwrap();
doc.insert_after(c, d).unwrap();
let chars: Vec<_> = doc
.root()
.children()
.filter_map(|node| node.char())
.collect();
assert_eq!(chars, vec!['a', 'b', 'c', 'd']);
}
#[test]
fn remove_and_extract() {
let mut doc = Document::new();
let a = doc.create_char('a').unwrap();
let b = doc.create_char('b').unwrap();
let root = doc.root().id();
doc.append_child(root, a).unwrap();
doc.append_child(root, b).unwrap();
let extracted = doc.extract(a).unwrap();
assert_eq!(extracted, a);
doc.remove(b).unwrap();
assert_eq!(doc.root().children().count(), 0);
doc.append_child(root, extracted).unwrap();
assert_eq!(
doc.root().children().next().and_then(|node| node.char()),
Some('a')
);
}
#[test]
fn replace_with_swaps_node() {
let mut doc = Document::new();
let a = doc.create_char('a').unwrap();
let b = doc.create_char('b').unwrap();
let root = doc.root().id();
doc.append_child(root, a).unwrap();
doc.replace_with(a, b).unwrap();
assert_eq!(
doc.root().children().next().and_then(|node| node.char()),
Some('b')
);
}
#[test]
fn set_text_and_char_and_command_name() {
let mut doc = Document::new();
let text = doc.create_text("old").unwrap();
let ch = doc.create_char('a').unwrap();
let cmd = doc.create_command("alpha", Vec::new()).unwrap();
doc.set_text(text, "new").unwrap();
doc.set_char(ch, 'b').unwrap();
doc.set_command_name(cmd, "beta").unwrap();
assert_eq!(doc.node(text).unwrap().text(), Some("new"));
assert_eq!(doc.node(ch).unwrap().char(), Some('b'));
assert_eq!(doc.node(cmd).unwrap().command_name(), Some("beta"));
}
#[test]
fn cannot_edit_root() {
let mut doc = Document::new();
let root = doc.root().id();
assert_eq!(doc.remove(root), Err(EditError::CannotEditRoot));
assert_eq!(doc.extract(root), Err(EditError::CannotEditRoot));
}
#[test]
fn append_to_non_container_fails() {
let mut doc = Document::new();
let parent = doc.create_char('a').unwrap();
let child = doc.create_char('b').unwrap();
assert_eq!(
doc.append_child(parent, child),
Err(EditError::NotAContainer)
);
}
#[test]
fn create_command_with_args_and_read_back() {
let mut doc = Document::new();
let numerator = doc.create_char('a').unwrap();
let denominator = doc.create_char('b').unwrap();
let frac = doc
.create_command(
"frac",
vec![ArgValue::math(numerator), ArgValue::math(denominator)],
)
.unwrap();
let node = doc.node(frac).unwrap();
assert_eq!(node.command_name(), Some("frac"));
assert_eq!(node.arg_count(), 2);
assert_eq!(
node.arg(0)
.and_then(|arg| arg.as_node())
.and_then(|node| node.char()),
Some('a')
);
assert_eq!(
node.arg(1)
.and_then(|arg| arg.as_node())
.and_then(|node| node.char()),
Some('b')
);
}
#[test]
fn duplicate_child_is_rejected() {
let mut doc = Document::new();
let child = doc.create_char('x').unwrap();
assert_eq!(
doc.create_command("dup", vec![ArgValue::math(child), ArgValue::math(child)]),
Err(EditError::DuplicateChild)
);
}
#[test]
fn set_arg_replaces_content() {
let mut doc = Document::new();
let old = doc.create_char('a').unwrap();
let cmd = doc
.create_command("sqrt", vec![ArgValue::math(old)])
.unwrap();
let new = doc.create_char('b').unwrap();
doc.set_arg(cmd, 0, ArgValue::math(new)).unwrap();
let node = doc.node(cmd).unwrap();
assert_eq!(
node.arg(0)
.and_then(|arg| arg.as_node())
.and_then(|node| node.char()),
Some('b')
);
}
#[test]
fn set_arg_preserves_optional_slot_kind() {
use texform_interface::syntax_node::{
Argument as SyntaxArgument, ArgumentKind as SyntaxArgumentKind,
ArgumentValue as SyntaxArgumentValue, ContentMode as M, SyntaxNode,
};
let syntax = SyntaxNode::Root {
mode: M::Math,
children: vec![SyntaxNode::Command {
name: "sqrt".to_string(),
args: vec![Some(SyntaxArgument {
kind: SyntaxArgumentKind::Optional,
value: SyntaxArgumentValue::MathContent(SyntaxNode::Char('a')),
})],
known: true,
}],
};
let mut doc = Document::from_syntax(&syntax).unwrap();
let command = doc.root().children().next().unwrap().id();
let replacement = doc.create_char('b').unwrap();
doc.set_arg(command, 0, ArgValue::math(replacement))
.unwrap();
assert_eq!(doc.to_latex().unwrap(), r"\sqrt [ b ]");
}
#[test]
fn set_arg_preserves_star_boolean_slot_kind() {
use texform_interface::syntax_node::{
Argument as SyntaxArgument, ArgumentKind as SyntaxArgumentKind,
ArgumentValue as SyntaxArgumentValue, ContentMode as M, SyntaxNode,
};
let syntax = SyntaxNode::Root {
mode: M::Math,
children: vec![SyntaxNode::Command {
name: "operatorname".to_string(),
args: vec![
Some(SyntaxArgument {
kind: SyntaxArgumentKind::Star,
value: SyntaxArgumentValue::Boolean(false),
}),
Some(SyntaxArgument {
kind: SyntaxArgumentKind::Mandatory,
value: SyntaxArgumentValue::MathContent(SyntaxNode::Char('x')),
}),
],
known: true,
}],
};
let mut doc = Document::from_syntax(&syntax).unwrap();
let command = doc.root().children().next().unwrap().id();
doc.set_arg(command, 0, ArgValue::boolean(true)).unwrap();
assert_eq!(doc.to_latex().unwrap(), r"\operatorname* { x }");
}
#[test]
fn read_only_editing_checks_precede_invalid_node_checks() {
let mut read_only = Document::from_ast_with_errors_for_test(|ast| {
let err = ast.new_node(Node::Error {
message: "bad".to_string(),
snippet: "x".to_string(),
});
ast.append_child(ast.root(), err);
});
let mut other = Document::new();
let foreign = other.create_char('x').unwrap();
assert_eq!(
read_only.append_child(foreign, foreign),
Err(EditError::ReadOnlyDocument)
);
assert_eq!(
read_only.insert_before(foreign, foreign),
Err(EditError::ReadOnlyDocument)
);
}
#[test]
fn wrap_moves_target_inside_wrapper() {
let mut doc = Document::new();
let a = doc.create_char('a').unwrap();
let wrapper = doc.create_group(ContentMode::Math).unwrap();
let root = doc.root().id();
doc.append_child(root, a).unwrap();
let wrapped = doc.wrap(a, wrapper).unwrap();
let group = doc.root().children().next().unwrap();
assert_eq!(group.id(), wrapped);
assert_eq!(
group.children().next().and_then(|node| node.char()),
Some('a')
);
}
#[test]
fn unwrap_splices_group_children_into_parent() {
let mut doc = Document::new();
let group = doc.create_group(ContentMode::Math).unwrap();
let a = doc.create_char('a').unwrap();
let b = doc.create_char('b').unwrap();
doc.append_child(group, a).unwrap();
doc.append_child(group, b).unwrap();
let root = doc.root().id();
doc.append_child(root, group).unwrap();
let children = doc.unwrap(group).unwrap();
assert_eq!(children.len(), 2);
let chars: Vec<_> = doc
.root()
.children()
.filter_map(|node| node.char())
.collect();
assert_eq!(chars, vec!['a', 'b']);
}
#[test]
fn error_tree_is_read_only() {
let doc = Document::from_ast_with_errors_for_test(|ast| {
let err = ast.new_node(Node::Error {
message: "bad".to_string(),
snippet: "x".to_string(),
});
ast.append_child(ast.root(), err);
});
assert!(doc.has_errors());
assert!(doc.is_read_only());
assert_eq!(doc.errors().count(), 1);
let mut doc = doc;
assert_eq!(doc.create_char('z'), Err(EditError::ReadOnlyDocument));
}
#[test]
fn from_syntax_round_trips_clean_tree() {
use texform_interface::syntax_node::{ContentMode as M, SyntaxNode};
let syntax = SyntaxNode::Root {
mode: M::Math,
children: vec![SyntaxNode::Char('a'), SyntaxNode::Char('b')],
};
let doc = Document::from_syntax(&syntax).unwrap();
assert!(!doc.has_errors());
assert_eq!(doc.to_syntax(), syntax);
}
#[test]
fn from_syntax_rejects_zero_count_prime() {
use texform_interface::syntax_node::{ContentMode as M, SyntaxNode};
let syntax = SyntaxNode::Root {
mode: M::Math,
children: vec![SyntaxNode::Prime { count: 0 }],
};
assert_eq!(
Document::from_syntax(&syntax).expect_err("expected invalid prime count"),
FromSyntaxError::InvalidPrimeCount
);
}
#[test]
fn from_syntax_rejects_text_mode_prime() {
use texform_interface::syntax_node::{ContentMode as M, SyntaxNode};
let syntax = SyntaxNode::Root {
mode: M::Text,
children: vec![SyntaxNode::Prime { count: 1 }],
};
assert_eq!(
Document::from_syntax(&syntax).expect_err("expected text-mode prime rejection"),
FromSyntaxError::PrimeInTextMode
);
}
#[test]
fn from_syntax_rejects_text_mode_scripted_prime() {
use texform_interface::syntax_node::{ContentMode as M, SyntaxNode};
let syntax = SyntaxNode::Root {
mode: M::Text,
children: vec![SyntaxNode::Scripted {
base: Box::new(SyntaxNode::Prime { count: 1 }),
subscript: None,
superscript: None,
}],
};
assert_eq!(
Document::from_syntax(&syntax)
.expect_err("expected text-mode scripted prime rejection"),
FromSyntaxError::PrimeInTextMode
);
}
#[test]
fn from_syntax_rejects_text_content_scripted_prime() {
use texform_interface::syntax_node::{
Argument, ArgumentKind, ArgumentValue, ContentMode as M, SyntaxNode,
};
let syntax = SyntaxNode::Root {
mode: M::Math,
children: vec![SyntaxNode::Command {
name: "text".to_string(),
args: vec![Some(Argument {
kind: ArgumentKind::Mandatory,
value: ArgumentValue::TextContent(SyntaxNode::Scripted {
base: Box::new(SyntaxNode::Prime { count: 1 }),
subscript: None,
superscript: None,
}),
})],
known: true,
}],
};
assert_eq!(
Document::from_syntax(&syntax)
.expect_err("expected text-content scripted prime rejection"),
FromSyntaxError::PrimeInTextMode
);
}
#[test]
fn from_syntax_marks_error_tree_read_only() {
use texform_interface::syntax_node::{ContentMode as M, SyntaxNode};
let syntax = SyntaxNode::Root {
mode: M::Math,
children: vec![SyntaxNode::Error {
message: "bad".to_string(),
snippet: "x".to_string(),
}],
};
let doc = Document::from_syntax(&syntax).unwrap();
assert!(doc.has_errors());
assert!(doc.is_read_only());
}
#[test]
fn from_syntax_alone_has_no_spans() {
use texform_interface::syntax_node::{ContentMode as M, SyntaxNode};
let syntax = SyntaxNode::Root {
mode: M::Math,
children: vec![SyntaxNode::Char('a')],
};
let doc = Document::from_syntax(&syntax).unwrap();
assert_eq!(doc.root().children().next().unwrap().span(), None);
}
#[test]
fn span_mapping_aligns_paths_to_node_ids() {
use texform_interface::syntax_node::{ContentMode as M, SyntaxNode};
let syntax = SyntaxNode::Root {
mode: M::Math,
children: vec![SyntaxNode::Char('a'), SyntaxNode::Char('b')],
};
let path_spans = vec![
("root".to_string(), Span { start: 0, end: 2 }),
("root.child.0".to_string(), Span { start: 0, end: 1 }),
("root.child.1".to_string(), Span { start: 1, end: 2 }),
];
let doc = Document::from_syntax_with_spans(&syntax, &path_spans).unwrap();
let mut kids = doc.root().children();
let a = kids.next().unwrap();
let b = kids.next().unwrap();
assert_eq!(a.span(), Some(Span { start: 0, end: 1 }));
assert_eq!(b.span(), Some(Span { start: 1, end: 2 }));
assert_eq!(doc.root().span(), Some(Span { start: 0, end: 2 }));
}
#[test]
fn to_latex_default_and_with_options() {
use crate::serialize::SerializeOptions;
let mut doc = Document::new();
let a = doc.create_char('a').unwrap();
let b = doc.create_char('b').unwrap();
let root = doc.root().id();
doc.append_child(root, a).unwrap();
doc.append_child(root, b).unwrap();
assert_eq!(doc.to_latex().unwrap(), "a b");
assert_eq!(format!("{doc}"), "a b");
let opts = SerializeOptions::default();
assert_eq!(doc.to_latex_with(&opts).unwrap(), "a b");
}
}