use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use crate::codes::{Op, Role};
use crate::error::{ParseError, Result};
use crate::tokenize::{quote_label, quote_value, split_indent, strip_quotes, Tokenizer};
use crate::tree::{Node, Ref, Tree};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeltaOp {
Add {
r: Ref,
parent: Ref,
pos: Option<u32>,
role: Role,
label: String,
ops: BTreeSet<Op>,
attrs: BTreeMap<String, String>,
},
Remove { r: Ref },
Update {
r: Ref,
attrs: BTreeMap<String, String>,
},
Move {
r: Ref,
parent: Ref,
pos: Option<u32>,
},
Replace { r: Ref, subtree: Vec<Node> },
}
impl DeltaOp {
#[must_use]
pub fn target(&self) -> Ref {
match self {
Self::Add { r, .. }
| Self::Remove { r }
| Self::Update { r, .. }
| Self::Move { r, .. }
| Self::Replace { r, .. } => *r,
}
}
}
#[must_use]
pub fn encode(ops: &[DeltaOp]) -> String {
let mut out = String::new();
for op in ops {
encode_one(op, &mut out);
}
out
}
fn encode_one(op: &DeltaOp, out: &mut String) {
match op {
DeltaOp::Add {
r,
parent,
pos,
role,
label,
ops,
attrs,
} => {
write!(out, "+{r}@{}", parent.0).expect("write");
if let Some(p) = pos {
write!(out, ":{p}").expect("write");
}
write!(out, " {role} {}", quote_label(label)).expect("write");
if !ops.is_empty() {
out.push(' ');
let mut first = true;
for op in ops {
if !first {
out.push(',');
}
out.push_str(op.as_str());
first = false;
}
}
for (k, v) in attrs {
write!(out, " {k}={}", quote_value(v)).expect("write");
}
out.push('\n');
}
DeltaOp::Remove { r } => {
writeln!(out, "-{r}").expect("write");
}
DeltaOp::Update { r, attrs } => {
write!(out, "~{r}").expect("write");
for (k, v) in attrs {
write!(out, " {k}={}", quote_value(v)).expect("write");
}
out.push('\n');
}
DeltaOp::Move { r, parent, pos } => {
write!(out, ">{r}@{}", parent.0).expect("write");
if let Some(p) = pos {
write!(out, ":{p}").expect("write");
}
out.push('\n');
}
DeltaOp::Replace { r, subtree } => {
writeln!(out, "*{r}").expect("write");
for node in subtree {
encode_subtree_line(node, 1, out);
}
}
}
}
fn encode_subtree_line(node: &Node, depth: usize, out: &mut String) {
for _ in 0..depth {
out.push_str(" ");
}
write!(out, "{} {} {}", node.r, node.role, quote_label(&node.label)).expect("write");
if !node.ops.is_empty() {
out.push(' ');
let mut first = true;
for op in &node.ops {
if !first {
out.push(',');
}
out.push_str(op.as_str());
first = false;
}
}
for (k, v) in &node.attrs {
write!(out, " {k}={}", quote_value(v)).expect("write");
}
out.push('\n');
for child in &node.children {
encode_subtree_line(child, depth + 1, out);
}
}
pub fn parse(input: &str) -> Result<Vec<DeltaOp>> {
let lines: Vec<&str> = input.lines().collect();
let mut idx = 0usize;
let mut ops = Vec::new();
while idx < lines.len() {
let raw = lines[idx];
if raw.trim().is_empty() {
idx += 1;
continue;
}
let (depth, body) = split_indent(raw);
if depth != 0 {
return Err(ParseError::IndentJump {
from: 0,
to: depth,
line: idx + 1,
});
}
let sigil = body.chars().next().ok_or(ParseError::MalformedLine {
line: idx + 1,
message: "empty delta line",
})?;
match sigil {
'+' => {
ops.push(parse_add(&body[1..], idx + 1)?);
idx += 1;
}
'-' => {
ops.push(parse_remove(&body[1..], idx + 1)?);
idx += 1;
}
'~' => {
ops.push(parse_update(&body[1..], idx + 1)?);
idx += 1;
}
'>' => {
ops.push(parse_move(&body[1..], idx + 1)?);
idx += 1;
}
'*' => {
let (op, consumed) = parse_replace(&lines[idx..], idx + 1)?;
ops.push(op);
idx += consumed;
}
_ => {
return Err(ParseError::MalformedLine {
line: idx + 1,
message: "unknown delta sigil",
});
}
}
}
Ok(ops)
}
fn parse_add(rest: &str, line_no: usize) -> Result<DeltaOp> {
let mut tokens = Tokenizer::new(rest);
let head = tokens.next().ok_or(ParseError::MalformedLine {
line: line_no,
message: "missing ref@parent",
})?;
let (r, parent, pos) = parse_ref_at_parent(head, line_no)?;
let role_tok = tokens.next().ok_or(ParseError::MalformedLine {
line: line_no,
message: "missing role",
})?;
let label_tok = tokens.next().ok_or(ParseError::MalformedLine {
line: line_no,
message: "missing label",
})?;
let role: Role = role_tok.parse()?;
let label = strip_quotes(label_tok).to_string();
let mut ops = BTreeSet::new();
let mut attrs = BTreeMap::new();
consume_ops_and_attrs(tokens, line_no, &mut ops, &mut attrs)?;
Ok(DeltaOp::Add {
r,
parent,
pos,
role,
label,
ops,
attrs,
})
}
fn parse_remove(rest: &str, line_no: usize) -> Result<DeltaOp> {
let trimmed = rest.trim();
if trimmed.is_empty() {
return Err(ParseError::MalformedLine {
line: line_no,
message: "missing ref",
});
}
let r: Ref = trimmed.parse()?;
Ok(DeltaOp::Remove { r })
}
fn parse_update(rest: &str, line_no: usize) -> Result<DeltaOp> {
let mut tokens = Tokenizer::new(rest);
let r_tok = tokens.next().ok_or(ParseError::MalformedLine {
line: line_no,
message: "missing ref",
})?;
let r: Ref = r_tok.parse()?;
let mut attrs = BTreeMap::new();
for tok in tokens {
let (k, v) = tok.split_once('=').ok_or(ParseError::InvalidAttribute {
raw: tok.to_string(),
})?;
if k.is_empty() {
return Err(ParseError::InvalidAttribute { raw: tok.into() });
}
attrs.insert(k.to_string(), strip_quotes(v).to_string());
}
Ok(DeltaOp::Update { r, attrs })
}
fn parse_move(rest: &str, line_no: usize) -> Result<DeltaOp> {
let trimmed = rest.trim();
if trimmed.is_empty() {
return Err(ParseError::MalformedLine {
line: line_no,
message: "missing ref@parent",
});
}
let (r, parent, pos) = parse_ref_at_parent(trimmed, line_no)?;
Ok(DeltaOp::Move { r, parent, pos })
}
fn parse_replace(lines: &[&str], header_line_no: usize) -> Result<(DeltaOp, usize)> {
let header = &lines[0][1..]; let r: Ref = header.trim().parse()?;
let mut consumed = 1;
let mut indented_lines: Vec<&str> = Vec::new();
while consumed < lines.len() {
let raw = lines[consumed];
if raw.trim().is_empty() {
consumed += 1;
continue;
}
let (depth, _) = split_indent(raw);
if depth == 0 {
break;
}
indented_lines.push(raw);
consumed += 1;
}
if indented_lines.is_empty() {
return Err(ParseError::MalformedLine {
line: header_line_no,
message: "Replace missing subtree",
});
}
let mut dedented = String::new();
for l in &indented_lines {
let trimmed = l.strip_prefix(" ").unwrap_or(l);
dedented.push_str(trimmed);
dedented.push('\n');
}
let subtree = Tree::parse(&dedented)?;
if subtree.roots.is_empty() {
return Err(ParseError::MalformedLine {
line: header_line_no,
message: "Replace subtree empty",
});
}
Ok((
DeltaOp::Replace {
r,
subtree: subtree.roots,
},
consumed,
))
}
fn parse_ref_at_parent(s: &str, line_no: usize) -> Result<(Ref, Ref, Option<u32>)> {
let (r_part, parent_part) = s.split_once('@').ok_or(ParseError::MalformedLine {
line: line_no,
message: "expected ref@parent",
})?;
let r: Ref = r_part.parse()?;
let (parent_part, pos_part) = match parent_part.split_once(':') {
Some((p, q)) => (p, Some(q)),
None => (parent_part, None),
};
let parent: Ref = parent_part.parse()?;
let pos = match pos_part {
Some(s) => Some(
s.parse::<u32>()
.map_err(|_| ParseError::InvalidPosition { raw: s.to_string() })?,
),
None => None,
};
Ok((r, parent, pos))
}
fn consume_ops_and_attrs(
tokens: Tokenizer<'_>,
_line_no: usize,
ops: &mut BTreeSet<Op>,
attrs: &mut BTreeMap<String, String>,
) -> Result<()> {
for tok in tokens {
if let Some((k, v)) = tok.split_once('=') {
if k.is_empty() {
return Err(ParseError::InvalidAttribute { raw: tok.into() });
}
attrs.insert(k.to_string(), strip_quotes(v).to_string());
} else {
for piece in tok.split(',') {
if piece.is_empty() {
return Err(ParseError::InvalidAttribute { raw: tok.into() });
}
ops.insert(piece.parse::<Op>()?);
}
}
}
Ok(())
}
mod apply;
mod diff;
#[cfg(test)]
mod tests;
pub use apply::apply;
pub use diff::diff;