use serde::{de, Deserialize, Deserializer, Serialize};
use crate::model::{Deco, FracStyle, Kind, Mark, MatrixEnv, Node, NodeId, SeqId, Symbol, Tree, Variant};
pub const DOCUMENT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Document {
#[serde(deserialize_with = "deserialize_version")]
version: u32,
pub root: Vec<NodeDoc>,
}
impl Document {
pub fn new(root: Vec<NodeDoc>) -> Self {
Self {
version: DOCUMENT_VERSION,
root,
}
}
pub fn version(&self) -> u32 {
self.version
}
pub fn root(&self) -> &[NodeDoc] {
&self.root
}
pub fn len(&self) -> usize {
self.root.len()
}
pub fn is_empty(&self) -> bool {
self.root.is_empty()
}
pub fn into_root(self) -> Vec<NodeDoc> {
self.root
}
}
impl Default for Document {
fn default() -> Self {
Self::new(Vec::new())
}
}
fn deserialize_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
let version = u32::deserialize(deserializer)?;
if version == DOCUMENT_VERSION {
Ok(version)
} else {
Err(de::Error::custom(format_args!(
"unsupported document version {version}; expected {DOCUMENT_VERSION}"
)))
}
}
pub type DocFragment = Vec<NodeDoc>;
macro_rules! node_kinds {
(
$(
$variant:ident {
$( $field:ident : $mode:ident $ty:ty ),* $(,)?
}
),* $(,)?
) => {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum NodeDoc {
Atom(Symbol),
$(
/// A serialized generated node variant.
$variant {
$( /// A serialized generated node field.
$field : node_kinds!(@doc_ty $mode $ty) ),*
},
)*
Matrix {
env: MatrixEnv,
rows: Vec<Vec<Vec<NodeDoc>>>,
},
}
impl Tree {
fn node_to_doc(&self, node: NodeId) -> NodeDoc {
match self.kind(node).expect("live node") {
Kind::Atom(s) => NodeDoc::Atom(s.clone()),
$(
Kind::$variant { $( $field ),* } => NodeDoc::$variant {
$( $field : node_kinds!(@to_doc self $mode $field) ),*
},
)*
Kind::Matrix { env, rows } => NodeDoc::Matrix {
env: *env,
rows: rows
.iter()
.map(|row| row.iter().map(|&c| self.seq_to_doc(c)).collect())
.collect(),
},
}
}
fn doc_kind(&mut self, d: &NodeDoc) -> Kind {
match d {
NodeDoc::Atom(s) => Kind::Atom(s.clone()),
$(
NodeDoc::$variant { $( $field ),* } => Kind::$variant {
$( $field : node_kinds!(@from_doc self $mode $field) ),*
},
)*
NodeDoc::Matrix { env, rows } => {
let mut grid = Vec::with_capacity(rows.len());
for row in rows {
let mut r = Vec::with_capacity(row.len());
for cell in row {
r.push(self.doc_seq(cell));
}
grid.push(r);
}
Kind::Matrix { env: *env, rows: grid }
}
}
}
}
};
(@doc_ty seq $ty:ty) => { Vec<NodeDoc> };
(@doc_ty opt_seq $ty:ty) => { Option<Vec<NodeDoc>> };
(@doc_ty copy $ty:ty) => { $ty };
(@doc_ty clone $ty:ty) => { $ty };
(@to_doc $self:ident seq $f:ident) => { $self.seq_to_doc(*$f) };
(@to_doc $self:ident opt_seq $f:ident) => { $self.opt_seq_to_doc(*$f) };
(@to_doc $self:ident copy $f:ident) => { *$f };
(@to_doc $self:ident clone $f:ident) => { $f.clone() };
(@from_doc $self:ident seq $f:ident) => { $self.doc_seq($f) };
(@from_doc $self:ident opt_seq $f:ident) => { $self.doc_opt_seq($f) };
(@from_doc $self:ident copy $f:ident) => { *$f };
(@from_doc $self:ident clone $f:ident) => { $f.clone() };
}
node_kinds! {
Frac {
num: seq SeqId,
den: seq SeqId,
style: copy FracStyle,
},
Script {
base: seq SeqId,
sub: opt_seq SeqId,
sup: opt_seq SeqId,
},
BigOp {
op: clone Symbol,
lower: seq SeqId,
upper: seq SeqId,
},
Sqrt {
index: seq SeqId,
radicand: seq SeqId,
},
Delim {
open: copy char,
close: copy char,
body: seq SeqId,
},
Accent {
mark: copy Mark,
base: seq SeqId,
},
UnderOver {
base: seq SeqId,
over: opt_seq SeqId,
under: opt_seq SeqId,
over_deco: copy Deco,
under_deco: copy Deco,
},
Styled {
variant: copy Variant,
content: seq SeqId,
},
}
impl Tree {
pub fn to_doc(&self) -> Document {
Document::new(self.seq_to_doc(self.root()))
}
fn seq_to_doc(&self, seq: SeqId) -> Vec<NodeDoc> {
self.items(seq).iter().map(|&n| self.node_to_doc(n)).collect()
}
fn opt_seq_to_doc(&self, seq: Option<SeqId>) -> Option<Vec<NodeDoc>> {
seq.map(|s| self.seq_to_doc(s))
}
pub fn from_doc(doc: &Document) -> Self {
let mut t = Tree::new();
let root = t.root();
for d in doc.root() {
t.push_doc_node(root, d);
}
t
}
pub fn insert_fragment(&mut self, at: crate::model::Cursor, frag: &DocFragment) -> crate::model::Cursor {
let mut idx = at.index;
for d in frag {
let kind = self.doc_kind(d);
let node = self.nodes.insert(Node {
parent: at.seq,
kind,
});
for s in self.child_seqs(node) {
if let Some(sq) = self.seqs.get_mut(s) {
sq.parent = Some(node);
}
}
if let Some(sq) = self.seqs.get_mut(at.seq) {
let i = idx.min(sq.items.len());
sq.items.insert(i, node);
}
idx += 1;
}
crate::model::Cursor {
seq: at.seq,
index: idx,
}
}
fn push_doc_node(&mut self, seq: SeqId, d: &NodeDoc) {
let kind = self.doc_kind(d);
let node = self.nodes.insert(Node { parent: seq, kind });
for s in self.child_seqs(node) {
if let Some(sq) = self.seqs.get_mut(s) {
sq.parent = Some(node);
}
}
if let Some(sq) = self.seqs.get_mut(seq) {
sq.items.push(node);
}
}
fn doc_seq(&mut self, docs: &[NodeDoc]) -> SeqId {
let seq = self.alloc_seq(None);
for d in docs {
self.push_doc_node(seq, d);
}
seq
}
fn doc_opt_seq(&mut self, docs: &Option<Vec<NodeDoc>>) -> Option<SeqId> {
match docs {
Some(d) => Some(self.doc_seq(d)),
None => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Cursor, MathClass};
fn atom(c: &str) -> Symbol {
Symbol {
latex: c.into(),
class: MathClass::Ord,
}
}
#[test]
fn doc_roundtrip() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
let c = t.insert_atom(c, atom("a")); let _ = c;
let frac = t.items(root)[0];
let den = t.child_seqs(frac)[1];
t.insert_atom(Cursor { seq: den, index: 0 }, atom("b"));
let d1 = t.to_doc();
let json = serde_json::to_string(&d1).unwrap();
let decoded = serde_json::from_str(&json).unwrap();
let t2 = Tree::from_doc(&decoded);
let d2 = t2.to_doc();
assert_eq!(d1, d2);
}
#[test]
fn doc_roundtrip_all_kinds() {
use crate::model::{Mark, MatrixEnv, ScriptSlot, UnderOverSpec, Variant};
let mut t = Tree::new();
let root = t.root();
let end = |t: &Tree| Cursor { seq: root, index: t.len(root) };
t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
t.insert_atom(c, atom("2"));
let c = t.insert_fraction(end(&t), FracStyle::Bar, None);
t.insert_atom(c, atom("x"));
let c = t.insert_sqrt(end(&t), None);
t.insert_atom(c, atom("y"));
t.insert_big_op(end(&t), Symbol { latex: "\\sum".into(), class: MathClass::Op });
let c = t.insert_delimiters(end(&t), '(', ')', None);
t.insert_atom(c, atom("z"));
let c = t.insert_accent(end(&t), Mark::Hat, None);
t.insert_atom(c, atom("b"));
let c = t.insert_styled(end(&t), Variant::Bold, None);
t.insert_atom(c, atom("c"));
let spec = UnderOverSpec { over: true, under: false, over_deco: Deco::Brace, under_deco: Deco::None };
let c = t.insert_under_over(end(&t), spec, None);
t.insert_atom(c, atom("d"));
let c = t.insert_matrix(end(&t), MatrixEnv::Pmatrix, 2, 2);
t.insert_atom(c, atom("e"));
let d1 = t.to_doc();
let json = serde_json::to_string(&d1).unwrap();
let decoded = serde_json::from_str(&json).unwrap();
let t2 = Tree::from_doc(&decoded);
let d2 = t2.to_doc();
assert_eq!(d1, d2);
}
#[test]
fn doc_serde_field_order_is_canonical() {
let pos = |s: &str, key: &str| s.find(&format!("\"{key}\"")).unwrap_or_else(|| panic!("missing {key} in {s}"));
let bigop = NodeDoc::BigOp {
op: Symbol { latex: "\\sum".into(), class: MathClass::Op },
lower: vec![],
upper: vec![],
};
let s = serde_json::to_string(&bigop).unwrap();
assert!(pos(&s, "op") < pos(&s, "lower") && pos(&s, "lower") < pos(&s, "upper"), "BigOp order: {s}");
let uo = NodeDoc::UnderOver {
base: vec![],
over: Some(vec![]), under: None,
over_deco: Deco::Brace,
under_deco: Deco::None,
};
let s = serde_json::to_string(&uo).unwrap();
assert!(
pos(&s, "base") < pos(&s, "over")
&& pos(&s, "over") < pos(&s, "under")
&& pos(&s, "under") < pos(&s, "over_deco")
&& pos(&s, "over_deco") < pos(&s, "under_deco"),
"UnderOver order: {s}"
);
let back: NodeDoc = serde_json::from_str(&s).unwrap();
assert_eq!(back, uo);
}
#[test]
fn document_json_has_a_stable_shape() {
let doc = Document::new(vec![NodeDoc::Atom(atom("x"))]);
let json = serde_json::to_string(&doc).unwrap();
assert_eq!(
json,
r#"{"version":1,"root":[{"type":"atom","data":{"latex":"x","class":"ord"}}]}"#
);
}
#[test]
fn unknown_document_version_is_rejected() {
let error = serde_json::from_str::<Document>(r#"{"version":2,"root":[]}"#)
.unwrap_err()
.to_string();
assert!(error.contains("unsupported document version 2"), "{error}");
}
}