1use sim_kernel::{Error, Expr, NumberLiteral, Result};
4use sim_value::build::{entry, list, sym, text, uint};
5
6use crate::{Inline, MarkupBlock, MarkupDoc, MarkupError, SpanState};
7
8#[derive(Clone, Debug, PartialEq)]
10pub enum MarkupEdit {
11 SetTitle {
13 old: Option<String>,
15 new: Option<String>,
17 },
18 InsertBlock {
20 index: usize,
22 block: MarkupBlock,
24 },
25 ReplaceBlock {
27 index: usize,
29 old: MarkupBlock,
31 new: MarkupBlock,
33 },
34 DeleteBlock {
36 index: usize,
38 old: MarkupBlock,
40 },
41 SetInlineText {
43 block: usize,
45 path: Vec<usize>,
47 old: String,
49 new: String,
51 },
52}
53
54impl MarkupEdit {
55 pub fn as_expr(&self) -> Expr {
57 match self {
58 Self::SetTitle { old, new } => {
59 let mut entries = edit_entries("set-title");
60 push_optional_string(&mut entries, "old", old);
61 push_optional_string(&mut entries, "new", new);
62 Expr::Map(entries)
63 }
64 Self::InsertBlock { index, block } => Expr::Map(vec![
65 entry("kind", sym("markup-edit")),
66 entry("op", sym("insert-block")),
67 entry("index", uint(*index as u64)),
68 entry("block", block.as_expr()),
69 ]),
70 Self::ReplaceBlock { index, old, new } => Expr::Map(vec![
71 entry("kind", sym("markup-edit")),
72 entry("op", sym("replace-block")),
73 entry("index", uint(*index as u64)),
74 entry("old", old.as_expr()),
75 entry("new", new.as_expr()),
76 ]),
77 Self::DeleteBlock { index, old } => Expr::Map(vec![
78 entry("kind", sym("markup-edit")),
79 entry("op", sym("delete-block")),
80 entry("index", uint(*index as u64)),
81 entry("old", old.as_expr()),
82 ]),
83 Self::SetInlineText {
84 block,
85 path,
86 old,
87 new,
88 } => Expr::Map(vec![
89 entry("kind", sym("markup-edit")),
90 entry("op", sym("set-inline-text")),
91 entry("block", uint(*block as u64)),
92 entry(
93 "path",
94 list(path.iter().map(|index| uint(*index as u64)).collect()),
95 ),
96 entry("old", text(old)),
97 entry("new", text(new)),
98 ]),
99 }
100 }
101
102 pub fn from_expr(expr: &Expr) -> Result<Self> {
104 let entries = map_entries(expr, "markup edit")?;
105 require_kind(entries)?;
106 match required_symbol(entries, "op", "markup edit")?.as_str() {
107 "set-title" => Ok(Self::SetTitle {
108 old: optional_string(entries, "old")?.map(str::to_owned),
109 new: optional_string(entries, "new")?.map(str::to_owned),
110 }),
111 "insert-block" => Ok(Self::InsertBlock {
112 index: required_usize(entries, "index", "insert block")?,
113 block: MarkupBlock::from_expr(required_field(entries, "block", "insert block")?)?,
114 }),
115 "replace-block" => Ok(Self::ReplaceBlock {
116 index: required_usize(entries, "index", "replace block")?,
117 old: MarkupBlock::from_expr(required_field(entries, "old", "replace block")?)?,
118 new: MarkupBlock::from_expr(required_field(entries, "new", "replace block")?)?,
119 }),
120 "delete-block" => Ok(Self::DeleteBlock {
121 index: required_usize(entries, "index", "delete block")?,
122 old: MarkupBlock::from_expr(required_field(entries, "old", "delete block")?)?,
123 }),
124 "set-inline-text" => Ok(Self::SetInlineText {
125 block: required_usize(entries, "block", "set inline text")?,
126 path: required_path(entries, "path", "set inline text")?,
127 old: required_string(entries, "old", "set inline text")?.to_owned(),
128 new: required_string(entries, "new", "set inline text")?.to_owned(),
129 }),
130 other => Err(Error::Eval(format!("unknown markup edit op {other}"))),
131 }
132 }
133}
134
135pub fn apply_edit(doc: &mut MarkupDoc, edit: &MarkupEdit) -> std::result::Result<(), MarkupError> {
137 match edit {
138 MarkupEdit::SetTitle { old, new } => {
139 if &doc.title != old {
140 return invalid_edit("title precondition failed");
141 }
142 doc.title = new.clone();
143 }
144 MarkupEdit::InsertBlock { index, block } => {
145 if *index > doc.blocks.len() {
146 return invalid_edit("insert block index out of range");
147 }
148 doc.blocks.insert(*index, block.clone());
149 }
150 MarkupEdit::ReplaceBlock { index, old, new } => {
151 let current = doc.blocks.get_mut(*index).ok_or_else(|| {
152 MarkupError::InvalidDocument("replace block index out of range".to_owned())
153 })?;
154 if current != old {
155 return invalid_edit("replace block precondition failed");
156 }
157 *current = new.clone();
158 }
159 MarkupEdit::DeleteBlock { index, old } => {
160 let current = doc.blocks.get(*index).ok_or_else(|| {
161 MarkupError::InvalidDocument("delete block index out of range".to_owned())
162 })?;
163 if current != old {
164 return invalid_edit("delete block precondition failed");
165 }
166 doc.blocks.remove(*index);
167 }
168 MarkupEdit::SetInlineText {
169 block,
170 path,
171 old,
172 new,
173 } => {
174 let block = doc.blocks.get_mut(*block).ok_or_else(|| {
175 MarkupError::InvalidDocument("inline block index out of range".to_owned())
176 })?;
177 match block {
178 MarkupBlock::Heading { text, .. } => apply_inline_text(text, path, old, new)?,
179 MarkupBlock::Paragraph { content, .. } => {
180 apply_inline_text(content, path, old, new)?
181 }
182 MarkupBlock::Figure { caption, .. } => apply_inline_text(caption, path, old, new)?,
183 _ => return invalid_edit("block has no editable inline text"),
184 }
185 mark_block_dirty(block);
186 }
187 }
188 doc.source = None;
189 Ok(())
190}
191
192pub fn invert_edit(edit: &MarkupEdit) -> MarkupEdit {
194 match edit {
195 MarkupEdit::SetTitle { old, new } => MarkupEdit::SetTitle {
196 old: new.clone(),
197 new: old.clone(),
198 },
199 MarkupEdit::InsertBlock { index, block } => MarkupEdit::DeleteBlock {
200 index: *index,
201 old: block.clone(),
202 },
203 MarkupEdit::ReplaceBlock { index, old, new } => MarkupEdit::ReplaceBlock {
204 index: *index,
205 old: new.clone(),
206 new: old.clone(),
207 },
208 MarkupEdit::DeleteBlock { index, old } => MarkupEdit::InsertBlock {
209 index: *index,
210 block: old.clone(),
211 },
212 MarkupEdit::SetInlineText {
213 block,
214 path,
215 old,
216 new,
217 } => MarkupEdit::SetInlineText {
218 block: *block,
219 path: path.clone(),
220 old: new.clone(),
221 new: old.clone(),
222 },
223 }
224}
225
226fn edit_entries(op: &str) -> Vec<(Expr, Expr)> {
227 vec![entry("kind", sym("markup-edit")), entry("op", sym(op))]
228}
229
230fn push_optional_string(entries: &mut Vec<(Expr, Expr)>, name: &str, value: &Option<String>) {
231 if let Some(value) = value {
232 entries.push(entry(name, text(value)));
233 }
234}
235
236fn apply_inline_text(
237 items: &mut [Inline],
238 path: &[usize],
239 old: &str,
240 new: &str,
241) -> std::result::Result<(), MarkupError> {
242 let inline = inline_at_path_mut(items, path)?;
243 match inline {
244 Inline::Text(value) if value == old => {
245 *value = new.to_owned();
246 Ok(())
247 }
248 Inline::Text(_) => invalid_edit("inline text precondition failed"),
249 _ => invalid_edit("inline path does not point at text"),
250 }
251}
252
253fn inline_at_path_mut<'a>(
254 items: &'a mut [Inline],
255 path: &[usize],
256) -> std::result::Result<&'a mut Inline, MarkupError> {
257 let (index, rest) = path
258 .split_first()
259 .ok_or_else(|| MarkupError::InvalidDocument("inline path must not be empty".to_owned()))?;
260 let inline = items
261 .get_mut(*index)
262 .ok_or_else(|| MarkupError::InvalidDocument("inline path index out of range".to_owned()))?;
263 if rest.is_empty() {
264 return Ok(inline);
265 }
266 match inline {
267 Inline::Emph(children) | Inline::Strong(children) => inline_at_path_mut(children, rest),
268 Inline::Link { label, .. } => inline_at_path_mut(label, rest),
269 _ => invalid_edit("inline path cannot descend through this node"),
270 }
271}
272
273fn mark_block_dirty(block: &mut MarkupBlock) {
274 match block {
275 MarkupBlock::Heading { span, .. }
276 | MarkupBlock::Paragraph { span, .. }
277 | MarkupBlock::CodeBlock { span, .. }
278 | MarkupBlock::MathBlock { span, .. }
279 | MarkupBlock::Table { span, .. }
280 | MarkupBlock::Figure { span, .. }
281 | MarkupBlock::Raw { span, .. } => mark_span_dirty(span),
282 MarkupBlock::Quote { blocks, span } => {
283 mark_span_dirty(span);
284 for block in blocks {
285 mark_block_dirty(block);
286 }
287 }
288 MarkupBlock::List { items, span, .. } => {
289 mark_span_dirty(span);
290 for item in items {
291 for block in item {
292 mark_block_dirty(block);
293 }
294 }
295 }
296 }
297}
298
299fn mark_span_dirty(span: &mut Option<crate::Span>) {
300 if let Some(span) = span {
301 span.state = SpanState::Dirty;
302 }
303}
304
305fn invalid_edit<T>(message: &str) -> std::result::Result<T, MarkupError> {
306 Err(MarkupError::InvalidDocument(message.to_owned()))
307}
308
309fn map_entries<'a>(expr: &'a Expr, expected: &str) -> Result<&'a [(Expr, Expr)]> {
310 match expr {
311 Expr::Map(entries) => Ok(entries),
312 _ => Err(Error::Eval(format!("{expected} must be a map"))),
313 }
314}
315
316fn require_kind(entries: &[(Expr, Expr)]) -> Result<()> {
317 let actual = required_symbol(entries, "kind", "markup edit")?;
318 if actual == "markup-edit" {
319 Ok(())
320 } else {
321 Err(Error::Eval(
322 "markup edit kind must be markup-edit".to_owned(),
323 ))
324 }
325}
326
327fn required_field<'a>(entries: &'a [(Expr, Expr)], name: &str, context: &str) -> Result<&'a Expr> {
328 field(entries, name).ok_or_else(|| Error::Eval(format!("{context} requires {name} field")))
329}
330
331fn required_symbol(entries: &[(Expr, Expr)], name: &str, context: &str) -> Result<String> {
332 match required_field(entries, name, context)? {
333 Expr::Symbol(symbol) if symbol.namespace.is_none() => Ok(symbol.name.to_string()),
334 Expr::String(value) => Ok(value.clone()),
335 _ => Err(Error::Eval(format!(
336 "{context} field {name} must be a symbol"
337 ))),
338 }
339}
340
341fn required_string<'a>(entries: &'a [(Expr, Expr)], name: &str, context: &str) -> Result<&'a str> {
342 match required_field(entries, name, context)? {
343 Expr::String(value) => Ok(value),
344 _ => Err(Error::Eval(format!(
345 "{context} field {name} must be a string"
346 ))),
347 }
348}
349
350fn optional_string<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<Option<&'a str>> {
351 match field(entries, name) {
352 Some(Expr::String(value)) => Ok(Some(value)),
353 Some(_) => Err(Error::Eval(format!("{name} field must be a string"))),
354 None => Ok(None),
355 }
356}
357
358fn required_usize(entries: &[(Expr, Expr)], name: &str, context: &str) -> Result<usize> {
359 match required_field(entries, name, context)? {
360 Expr::Number(NumberLiteral { canonical, .. }) => canonical
361 .parse()
362 .map_err(|_| Error::Eval(format!("{context} field {name} must be an integer"))),
363 _ => Err(Error::Eval(format!(
364 "{context} field {name} must be a number"
365 ))),
366 }
367}
368
369fn required_path(entries: &[(Expr, Expr)], name: &str, context: &str) -> Result<Vec<usize>> {
370 match required_field(entries, name, context)? {
371 Expr::List(items) => items
372 .iter()
373 .map(|item| match item {
374 Expr::Number(NumberLiteral { canonical, .. }) => canonical.parse().map_err(|_| {
375 Error::Eval(format!("{context} field {name} must contain integers"))
376 }),
377 _ => Err(Error::Eval(format!(
378 "{context} field {name} must contain numbers"
379 ))),
380 })
381 .collect(),
382 _ => Err(Error::Eval(format!(
383 "{context} field {name} must be a list"
384 ))),
385 }
386}
387
388fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
389 entries
390 .iter()
391 .find_map(|(key, value)| (key_name(key).as_deref() == Some(name)).then_some(value))
392}
393
394fn key_name(key: &Expr) -> Option<String> {
395 match key {
396 Expr::Symbol(symbol) if symbol.namespace.is_none() => Some(symbol.name.to_string()),
397 Expr::String(value) => Some(value.clone()),
398 _ => None,
399 }
400}