1use headless_lms_utils::document_schema_processor::GutenbergBlock;
4use serde_json::json;
5use uuid::Uuid;
6
7use super::Editor;
8use crate::engine::blocks;
9use crate::engine::error::{EditError, EditErrorCode};
10use crate::engine::op::Position;
11use crate::engine::path::{BlockPart, BlockPath, parse_block_path};
12
13pub(super) type Anchor = crate::engine::Anchor<ListRef>;
14
15#[derive(Debug, Clone)]
16pub(super) enum Address {
17 Root(usize),
19 Block(Uuid),
20 Children(Uuid),
21 Name(Uuid),
22 Attribute(Uuid, String),
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub(super) struct ListRef {
29 pub(super) page: usize,
30 pub(super) parent: Option<Uuid>,
31}
32
33#[derive(Debug, Clone)]
35pub(super) struct Loc {
36 pub(super) page: usize,
37 pub(super) path: Vec<usize>,
38}
39
40impl Loc {
41 pub(super) fn parent_path(&self) -> &[usize] {
42 &self.path[..self.path.len() - 1]
43 }
44
45 pub(super) fn index(&self) -> usize {
46 self.path[self.path.len() - 1]
47 }
48}
49
50pub(super) struct Range {
52 pub(super) page: usize,
53 pub(super) parent_path: Vec<usize>,
54 pub(super) start: usize,
55 pub(super) end: usize,
56}
57
58impl Range {
59 pub(super) fn first(&self) -> Loc {
60 Loc {
61 page: self.page,
62 path: [self.parent_path.as_slice(), &[self.start]].concat(),
63 }
64 }
65}
66
67impl Editor<'_> {
68 pub(super) fn locate(&self, id: Uuid) -> Option<Loc> {
70 let reachable = if self.can_reach_other_pages {
71 self.pages.len()
72 } else {
73 1
74 };
75 self.pages
76 .iter()
77 .take(reachable)
78 .enumerate()
79 .find_map(|(page, doc)| {
80 blocks::find_path(&doc.blocks, id).map(|path| Loc { page, path })
81 })
82 }
83
84 pub(super) fn require(&self, id: Uuid) -> Result<Loc, EditError> {
85 self.locate(id).ok_or_else(|| self.not_found(id))
86 }
87
88 pub(super) fn not_found(&self, id: Uuid) -> EditError {
89 let other_page = self
90 .pages
91 .iter()
92 .skip(1)
93 .find(|doc| blocks::find_path(&doc.blocks, id).is_some());
94 match (self.removed.get(&id), other_page) {
95 (Some(op), _) => EditError::new(
96 EditErrorCode::NotFound,
97 format!("block {id} was removed by op {op} of this call"),
98 ),
99 (None, Some(doc)) => EditError::new(
100 EditErrorCode::NotFound,
101 format!(
102 "block {id} is on page {}; this call edits only its own page, which move and copy can reach other pages from",
103 doc.page_id
104 ),
105 )
106 .with_fix("edit the other page with its own call"),
107 (None, None) => EditError::new(
108 EditErrorCode::NotFound,
109 format!("no block {id} on this page"),
110 )
111 .with_fix("read the page outline with get_page_content to get current ids"),
112 }
113 }
114
115 pub(super) fn address(&self, path: &str, root_page: usize) -> Result<Address, EditError> {
117 let parsed = parse_block_path(path).ok_or_else(|| {
118 EditError::new(
119 EditErrorCode::InvalidPath,
120 format!("path \"{path}\" must start with / (or be \"\" for the page's top level)"),
121 )
122 .with_fix(format!("/{path}"))
123 })?;
124 let (root, part) = match parsed {
125 BlockPath::TopLevel => return Ok(Address::Root(root_page)),
126 BlockPath::Block { root, part } => (root, part),
127 };
128 if root.parse::<usize>().is_ok() {
129 return Err(numeric_index_error(
130 path,
131 &self.pages[root_page].blocks,
132 &root,
133 ));
134 }
135 let id = self.labels.resolve_segment(&root, path, self.ids.op())?;
136 let loc = self.require(id)?;
137 match part {
138 BlockPart::Whole => Ok(Address::Block(id)),
139 BlockPart::Children => Ok(Address::Children(id)),
140 BlockPart::ChildIndex(index) => Err(numeric_index_error(
141 path,
142 &self.block(&loc)?.inner_blocks,
143 &index,
144 )),
145 BlockPart::Attributes(pointer) => Ok(Address::Attribute(id, pointer.to_string())),
146 BlockPart::Name => Ok(Address::Name(id)),
147 BlockPart::Other { field, .. } if matches!(field.as_str(), "clientId" | "isValid") => {
148 Err(EditError::new(
149 EditErrorCode::ReadOnlyField,
150 format!("{field} is managed by the server"),
151 ))
152 }
153 BlockPart::Other { field, .. } => Err(EditError::new(
154 EditErrorCode::InvalidPath,
155 format!("blocks have no \"{field}\"; a block path continues with /innerBlocks, /attributes/<name> or /name"),
156 )
157 .with_fix(format!("/{id}/attributes/{field}"))),
158 }
159 }
160
161 pub(super) fn anchor(
162 &self,
163 address: &Address,
164 position: Option<Position>,
165 ) -> Result<Anchor, EditError> {
166 let Some(position) = position else {
167 return Err(EditError::position_required(
168 "position is before/after a sibling (\"path\":\"/p2\") or start/end of a children list (\"path\":\"/box1/innerBlocks\" or \"\" for the page)",
169 ));
170 };
171 match (address, position) {
172 (Address::Root(page), Position::Start) => Ok(Anchor::Start(ListRef { page: *page, parent: None })),
173 (Address::Root(page), Position::End) => Ok(Anchor::End(ListRef { page: *page, parent: None })),
174 (Address::Children(id), Position::Start | Position::End) => {
175 let loc = self.require(*id)?;
176 let list = ListRef { page: loc.page, parent: Some(*id) };
177 self.check_not_in_exercise(&loc, true, "add blocks inside it")?;
178 Ok(if position == Position::Start { Anchor::Start(list) } else { Anchor::End(list) })
179 }
180 (Address::Block(id), Position::Before | Position::After) => {
181 let loc = self.require(*id)?;
182 self.check_not_in_exercise(&loc, false, "add blocks inside it")?;
183 Ok(if position == Position::Before { Anchor::Before(*id) } else { Anchor::After(*id) })
184 }
185 (Address::Block(id), Position::Start | Position::End) => Err(EditError::new(
186 EditErrorCode::PositionNeedsChildren,
187 format!("position \"{position}\" needs a children list, not a block"),
188 )
189 .with_fix(format!("\"path\":\"/{id}/innerBlocks\" to place inside the block, or position before/after to place next to it"))),
190 (Address::Root(_), Position::Before | Position::After) => Err(EditError::new(
191 EditErrorCode::PositionNeedsSibling,
192 "\"\" is the page's top-level list; before/after need a sibling block",
193 )
194 .with_fix("use position \"start\" or \"end\" with path \"\", or before/after with \"path\":\"/<sibling id>\"")),
195 (Address::Children(id), Position::Before | Position::After) => Err(EditError::new(
196 EditErrorCode::PositionNeedsSibling,
197 format!("/{id}/innerBlocks is a children list; before/after need a sibling block"),
198 )
199 .with_fix(format!("use position \"start\" or \"end\" with \"/{id}/innerBlocks\", or before/after with the sibling's path"))),
200 (Address::Name(_) | Address::Attribute(..), _) => Err(EditError::new(
201 EditErrorCode::InvalidPath,
202 "blocks are placed next to a block or into a children list, not at an attribute",
203 )),
204 }
205 }
206
207 pub(super) fn anchor_page(&self, anchor: Anchor) -> Result<usize, EditError> {
208 match anchor {
209 Anchor::Before(id) | Anchor::After(id) => Ok(self.require(id)?.page),
210 Anchor::Start(list) | Anchor::End(list) => Ok(list.page),
211 }
212 }
213
214 pub(super) fn resolve_anchor(
216 &self,
217 anchor: Anchor,
218 ) -> Result<(usize, Vec<usize>, usize), EditError> {
219 let ((page, parent_path), index) = anchor.insertion_point(
220 |id| {
221 let loc = self.require(id)?;
222 Ok(((loc.page, loc.parent_path().to_vec()), loc.index()))
223 },
224 |list| {
225 let parent_path = self.list_parent_path(list)?;
226 let len = blocks::list(&self.pages[list.page].blocks, &parent_path)?.len();
227 Ok(((list.page, parent_path), len))
228 },
229 )?;
230 Ok((page, parent_path, index))
231 }
232
233 pub(super) fn list_parent_path(&self, list: ListRef) -> Result<Vec<usize>, EditError> {
235 Ok(match list.parent {
236 Some(parent) => self.require(parent)?.path,
237 None => Vec::new(),
238 })
239 }
240
241 pub(super) fn range(&self, first: Uuid, through: Option<Uuid>) -> Result<Range, EditError> {
242 let start = self.require(first)?;
243 let Some(last_id) = through else {
244 return Ok(Range {
245 page: start.page,
246 parent_path: start.parent_path().to_vec(),
247 start: start.index(),
248 end: start.index(),
249 });
250 };
251 let end = self.require(last_id)?;
252 if start.page != end.page || start.parent_path() != end.parent_path() {
253 let parent_of = |loc: &Loc| -> Result<String, EditError> {
254 Ok(if loc.path.len() == 1 {
255 "the page's top level (\"\")".to_string()
256 } else {
257 format!(
258 "/{}/innerBlocks",
259 blocks::get(&self.pages[loc.page].blocks, loc.parent_path())?.client_id
260 )
261 })
262 };
263 return Err(EditError::new(
264 EditErrorCode::InvalidRange,
265 format!(
266 "a range must be siblings under one parent; /{first} is in {} and /{last_id} is in {}",
267 parent_of(&start)?,
268 parent_of(&end)?
269 ),
270 )
271 .with_details(json!({
272 "from_siblings": self.siblings(start.page, start.parent_path()),
273 "through_siblings": self.siblings(end.page, end.parent_path()),
274 })));
275 }
276 if end.index() < start.index() {
277 return Err(EditError::reversed_range(
278 first,
279 last_id,
280 self.siblings(start.page, start.parent_path()),
281 ));
282 }
283 Ok(Range {
284 page: start.page,
285 parent_path: start.parent_path().to_vec(),
286 start: start.index(),
287 end: end.index(),
288 })
289 }
290
291 pub(super) fn range_blocks(&self, range: &Range) -> Result<&[GutenbergBlock], EditError> {
292 blocks::list(&self.pages[range.page].blocks, &range.parent_path)?
293 .get(range.start..=range.end)
294 .ok_or_else(blocks::stale_location)
295 }
296}
297
298fn numeric_index_error(path: &str, list: &[GutenbergBlock], index: &str) -> EditError {
299 let current = index
300 .parse::<usize>()
301 .ok()
302 .and_then(|i| list.get(i))
303 .map(|b| format!("; that position currently holds /{}", b.client_id))
304 .unwrap_or_default();
305 let append_hint = if index == "-" {
306 "; to append, use position \"end\" on the children list"
307 } else {
308 ""
309 };
310 let mut error = EditError::numeric_index(path, "blocks");
311 error.message.push_str(¤t);
312 error.message.push_str(append_hint);
313 error.with_fix("use /<id> for a block, or the children list with a position, e.g. {\"path\":\"/box1/innerBlocks\",\"position\":\"end\"}")
314}