1use std::collections::{BTreeMap, BTreeSet};
17use std::fmt::Write as _;
18
19use crate::codes::{Op, Role};
20use crate::error::{ParseError, Result};
21use crate::tokenize::{quote_label, quote_value, split_indent, strip_quotes, Tokenizer};
22use crate::tree::{Node, Ref, Tree};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum DeltaOp {
27 Add {
30 r: Ref,
31 parent: Ref,
32 pos: Option<u32>,
33 role: Role,
34 label: String,
35 ops: BTreeSet<Op>,
36 attrs: BTreeMap<String, String>,
37 },
38 Remove { r: Ref },
40 Update {
43 r: Ref,
44 attrs: BTreeMap<String, String>,
45 },
46 Move {
48 r: Ref,
49 parent: Ref,
50 pos: Option<u32>,
51 },
52 Replace { r: Ref, subtree: Vec<Node> },
56}
57
58impl DeltaOp {
59 #[must_use]
61 pub fn target(&self) -> Ref {
62 match self {
63 Self::Add { r, .. }
64 | Self::Remove { r }
65 | Self::Update { r, .. }
66 | Self::Move { r, .. }
67 | Self::Replace { r, .. } => *r,
68 }
69 }
70}
71
72#[must_use]
74pub fn encode(ops: &[DeltaOp]) -> String {
75 let mut out = String::new();
76 for op in ops {
77 encode_one(op, &mut out);
78 }
79 out
80}
81
82fn encode_one(op: &DeltaOp, out: &mut String) {
83 match op {
84 DeltaOp::Add {
85 r,
86 parent,
87 pos,
88 role,
89 label,
90 ops,
91 attrs,
92 } => {
93 write!(out, "+{r}@{}", parent.0).expect("write");
94 if let Some(p) = pos {
95 write!(out, ":{p}").expect("write");
96 }
97 write!(out, " {role} {}", quote_label(label)).expect("write");
98 if !ops.is_empty() {
99 out.push(' ');
100 let mut first = true;
101 for op in ops {
102 if !first {
103 out.push(',');
104 }
105 out.push_str(op.as_str());
106 first = false;
107 }
108 }
109 for (k, v) in attrs {
110 write!(out, " {k}={}", quote_value(v)).expect("write");
111 }
112 out.push('\n');
113 }
114 DeltaOp::Remove { r } => {
115 writeln!(out, "-{r}").expect("write");
116 }
117 DeltaOp::Update { r, attrs } => {
118 write!(out, "~{r}").expect("write");
119 for (k, v) in attrs {
120 write!(out, " {k}={}", quote_value(v)).expect("write");
121 }
122 out.push('\n');
123 }
124 DeltaOp::Move { r, parent, pos } => {
125 write!(out, ">{r}@{}", parent.0).expect("write");
126 if let Some(p) = pos {
127 write!(out, ":{p}").expect("write");
128 }
129 out.push('\n');
130 }
131 DeltaOp::Replace { r, subtree } => {
132 writeln!(out, "*{r}").expect("write");
133 for node in subtree {
134 encode_subtree_line(node, 1, out);
135 }
136 }
137 }
138}
139
140fn encode_subtree_line(node: &Node, depth: usize, out: &mut String) {
141 for _ in 0..depth {
142 out.push_str(" ");
143 }
144 write!(out, "{} {} {}", node.r, node.role, quote_label(&node.label)).expect("write");
145 if !node.ops.is_empty() {
146 out.push(' ');
147 let mut first = true;
148 for op in &node.ops {
149 if !first {
150 out.push(',');
151 }
152 out.push_str(op.as_str());
153 first = false;
154 }
155 }
156 for (k, v) in &node.attrs {
157 write!(out, " {k}={}", quote_value(v)).expect("write");
158 }
159 out.push('\n');
160 for child in &node.children {
161 encode_subtree_line(child, depth + 1, out);
162 }
163}
164
165pub fn parse(input: &str) -> Result<Vec<DeltaOp>> {
167 let lines: Vec<&str> = input.lines().collect();
168 let mut idx = 0usize;
169 let mut ops = Vec::new();
170 while idx < lines.len() {
171 let raw = lines[idx];
172 if raw.trim().is_empty() {
173 idx += 1;
174 continue;
175 }
176 let (depth, body) = split_indent(raw);
177 if depth != 0 {
178 return Err(ParseError::IndentJump {
179 from: 0,
180 to: depth,
181 line: idx + 1,
182 });
183 }
184 let sigil = body.chars().next().ok_or(ParseError::MalformedLine {
185 line: idx + 1,
186 message: "empty delta line",
187 })?;
188 match sigil {
189 '+' => {
190 ops.push(parse_add(&body[1..], idx + 1)?);
191 idx += 1;
192 }
193 '-' => {
194 ops.push(parse_remove(&body[1..], idx + 1)?);
195 idx += 1;
196 }
197 '~' => {
198 ops.push(parse_update(&body[1..], idx + 1)?);
199 idx += 1;
200 }
201 '>' => {
202 ops.push(parse_move(&body[1..], idx + 1)?);
203 idx += 1;
204 }
205 '*' => {
206 let (op, consumed) = parse_replace(&lines[idx..], idx + 1)?;
207 ops.push(op);
208 idx += consumed;
209 }
210 _ => {
211 return Err(ParseError::MalformedLine {
212 line: idx + 1,
213 message: "unknown delta sigil",
214 });
215 }
216 }
217 }
218 Ok(ops)
219}
220
221fn parse_add(rest: &str, line_no: usize) -> Result<DeltaOp> {
222 let mut tokens = Tokenizer::new(rest);
223 let head = tokens.next().ok_or(ParseError::MalformedLine {
224 line: line_no,
225 message: "missing ref@parent",
226 })?;
227 let (r, parent, pos) = parse_ref_at_parent(head, line_no)?;
228 let role_tok = tokens.next().ok_or(ParseError::MalformedLine {
229 line: line_no,
230 message: "missing role",
231 })?;
232 let label_tok = tokens.next().ok_or(ParseError::MalformedLine {
233 line: line_no,
234 message: "missing label",
235 })?;
236 let role: Role = role_tok.parse()?;
237 let label = strip_quotes(label_tok).to_string();
238
239 let mut ops = BTreeSet::new();
240 let mut attrs = BTreeMap::new();
241 consume_ops_and_attrs(tokens, line_no, &mut ops, &mut attrs)?;
242
243 Ok(DeltaOp::Add {
244 r,
245 parent,
246 pos,
247 role,
248 label,
249 ops,
250 attrs,
251 })
252}
253
254fn parse_remove(rest: &str, line_no: usize) -> Result<DeltaOp> {
255 let trimmed = rest.trim();
256 if trimmed.is_empty() {
257 return Err(ParseError::MalformedLine {
258 line: line_no,
259 message: "missing ref",
260 });
261 }
262 let r: Ref = trimmed.parse()?;
263 Ok(DeltaOp::Remove { r })
264}
265
266fn parse_update(rest: &str, line_no: usize) -> Result<DeltaOp> {
267 let mut tokens = Tokenizer::new(rest);
268 let r_tok = tokens.next().ok_or(ParseError::MalformedLine {
269 line: line_no,
270 message: "missing ref",
271 })?;
272 let r: Ref = r_tok.parse()?;
273 let mut attrs = BTreeMap::new();
274 for tok in tokens {
275 let (k, v) = tok.split_once('=').ok_or(ParseError::InvalidAttribute {
276 raw: tok.to_string(),
277 })?;
278 if k.is_empty() {
279 return Err(ParseError::InvalidAttribute { raw: tok.into() });
280 }
281 attrs.insert(k.to_string(), strip_quotes(v).to_string());
282 }
283 Ok(DeltaOp::Update { r, attrs })
284}
285
286fn parse_move(rest: &str, line_no: usize) -> Result<DeltaOp> {
287 let trimmed = rest.trim();
288 if trimmed.is_empty() {
289 return Err(ParseError::MalformedLine {
290 line: line_no,
291 message: "missing ref@parent",
292 });
293 }
294 let (r, parent, pos) = parse_ref_at_parent(trimmed, line_no)?;
295 Ok(DeltaOp::Move { r, parent, pos })
296}
297
298fn parse_replace(lines: &[&str], header_line_no: usize) -> Result<(DeltaOp, usize)> {
299 let header = &lines[0][1..]; let r: Ref = header.trim().parse()?;
301 let mut consumed = 1;
302 let mut indented_lines: Vec<&str> = Vec::new();
303 while consumed < lines.len() {
304 let raw = lines[consumed];
305 if raw.trim().is_empty() {
306 consumed += 1;
307 continue;
308 }
309 let (depth, _) = split_indent(raw);
310 if depth == 0 {
311 break;
312 }
313 indented_lines.push(raw);
314 consumed += 1;
315 }
316 if indented_lines.is_empty() {
317 return Err(ParseError::MalformedLine {
318 line: header_line_no,
319 message: "Replace missing subtree",
320 });
321 }
322 let mut dedented = String::new();
324 for l in &indented_lines {
325 let trimmed = l.strip_prefix(" ").unwrap_or(l);
326 dedented.push_str(trimmed);
327 dedented.push('\n');
328 }
329 let subtree = Tree::parse(&dedented)?;
330 if subtree.roots.is_empty() {
331 return Err(ParseError::MalformedLine {
332 line: header_line_no,
333 message: "Replace subtree empty",
334 });
335 }
336 Ok((
337 DeltaOp::Replace {
338 r,
339 subtree: subtree.roots,
340 },
341 consumed,
342 ))
343}
344
345fn parse_ref_at_parent(s: &str, line_no: usize) -> Result<(Ref, Ref, Option<u32>)> {
346 let (r_part, parent_part) = s.split_once('@').ok_or(ParseError::MalformedLine {
347 line: line_no,
348 message: "expected ref@parent",
349 })?;
350 let r: Ref = r_part.parse()?;
351 let (parent_part, pos_part) = match parent_part.split_once(':') {
352 Some((p, q)) => (p, Some(q)),
353 None => (parent_part, None),
354 };
355 let parent: Ref = parent_part.parse()?;
356 let pos = match pos_part {
357 Some(s) => Some(
358 s.parse::<u32>()
359 .map_err(|_| ParseError::InvalidPosition { raw: s.to_string() })?,
360 ),
361 None => None,
362 };
363 Ok((r, parent, pos))
364}
365
366fn consume_ops_and_attrs(
367 tokens: Tokenizer<'_>,
368 _line_no: usize,
369 ops: &mut BTreeSet<Op>,
370 attrs: &mut BTreeMap<String, String>,
371) -> Result<()> {
372 for tok in tokens {
373 if let Some((k, v)) = tok.split_once('=') {
374 if k.is_empty() {
375 return Err(ParseError::InvalidAttribute { raw: tok.into() });
376 }
377 attrs.insert(k.to_string(), strip_quotes(v).to_string());
378 } else {
379 for piece in tok.split(',') {
380 if piece.is_empty() {
381 return Err(ParseError::InvalidAttribute { raw: tok.into() });
382 }
383 ops.insert(piece.parse::<Op>()?);
384 }
385 }
386 }
387 Ok(())
388}
389
390mod apply;
391mod diff;
392
393#[cfg(test)]
394mod tests;
395
396pub use apply::apply;
397pub use diff::diff;