| 96 |
96 |
|
);
|
| 97 |
97 |
|
|
| 98 |
98 |
|
/// A span of the base render, and what decides it.
|
|
99 |
+ |
///
|
|
100 |
+ |
/// `at` is where the span is finally placed; `first` is the earliest placement
|
|
101 |
+ |
/// that is equally consistent with the two renders it was read from. See
|
|
102 |
+ |
/// [`between`] for why a span has a range at all, and [`placed`] for what picks
|
|
103 |
+ |
/// one point in it.
|
| 99 |
104 |
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
| 100 |
105 |
|
struct Span {
|
| 101 |
106 |
|
at: usize,
|
| 102 |
107 |
|
to: usize,
|
| 103 |
108 |
|
repeats: bool,
|
|
109 |
+ |
/// The earliest `at` that rebuilds the same string. `at` is the latest.
|
|
110 |
+ |
first: usize,
|
| 104 |
111 |
|
}
|
| 105 |
112 |
|
|
| 106 |
113 |
|
/// The residual of one staged shape.
|
| 185 |
192 |
|
|
| 186 |
193 |
|
let at = found.at.checked_sub(length)?;
|
| 187 |
194 |
|
let to = found.at;
|
| 188 |
|
- |
(narrow.is_char_boundary(at) && narrow.is_char_boundary(to)).then_some(Span {
|
|
195 |
+ |
// The window shifts with the placement it came from: the body in `narrow`
|
|
196 |
+ |
// sits one copy earlier than the extra copy found in `wide`.
|
|
197 |
+ |
let first = found.first.checked_sub(length)?;
|
|
198 |
+ |
(first <= at && narrow.is_char_boundary(at) && narrow.is_char_boundary(to)).then_some(Span {
|
| 189 |
199 |
|
at,
|
| 190 |
200 |
|
to,
|
| 191 |
201 |
|
repeats: true,
|
|
202 |
+ |
first,
|
| 192 |
203 |
|
})
|
| 193 |
204 |
|
}
|
| 194 |
205 |
|
|
| 215 |
226 |
|
/// alone cannot tell a body from the text that follows it when the two begin
|
| 216 |
227 |
|
/// the same way, which is the ambiguity that makes a search necessary and a
|
| 217 |
228 |
|
/// named site unnecessary.
|
|
229 |
+ |
///
|
|
230 |
+ |
/// # The deletion is a range, not a point
|
|
231 |
+ |
///
|
|
232 |
+ |
/// When what is deleted shares a boundary with what follows it, several
|
|
233 |
+ |
/// placements rebuild `short` from `long` **exactly**, so no check on one
|
|
234 |
+ |
/// placement can tell them apart. Two adjacent described regions are the site,
|
|
235 |
+ |
/// and they are not an edge case: every region opens `<section id="` and closes
|
|
236 |
+ |
/// `</section>`, so a pair of guarded siblings always shares a prefix, and MNW's
|
|
237 |
+ |
/// `/fan-plus` shares 22 bytes of it.
|
|
238 |
+ |
///
|
|
239 |
+ |
/// So this answers with the whole range. `first` is the earliest placement, from
|
|
240 |
+ |
/// the maximal common suffix, and `at` the latest, from the maximal common
|
|
241 |
+ |
/// prefix. Both rebuild `short`, and [`placed`] picks between them using the one
|
|
242 |
+ |
/// fact a single span does not carry: where the other spans went.
|
|
243 |
+ |
///
|
|
244 |
+ |
/// Reading only the latest is what put a branch on top of its neighbour. It is
|
|
245 |
+ |
/// tempting to read only the earliest instead, and that is just as wrong the
|
|
246 |
+ |
/// other way: of two adjacent guarded regions, the first wants its earliest
|
|
247 |
+ |
/// placement and the second wants its latest.
|
| 218 |
248 |
|
fn between(short: &str, long: &str, length: usize) -> Option<Span> {
|
| 219 |
249 |
|
let (s, l) = (short.as_bytes(), long.as_bytes());
|
| 220 |
250 |
|
|
| 229 |
259 |
|
if head + tail + length != long.len() {
|
| 230 |
260 |
|
return None;
|
| 231 |
261 |
|
}
|
|
262 |
+ |
|
|
263 |
+ |
// The maximal common suffix, measured on its own rather than as whatever is
|
|
264 |
+ |
// left after the prefix has taken what it can. That is what makes `first`
|
|
265 |
+ |
// the earliest placement instead of the same point twice.
|
|
266 |
+ |
let mut back = 0;
|
|
267 |
+ |
while back < s.len() && s[s.len() - 1 - back] == l[l.len() - 1 - back] {
|
|
268 |
+ |
back += 1;
|
|
269 |
+ |
}
|
|
270 |
+ |
let first = long.len().checked_sub(length + back)?;
|
|
271 |
+ |
|
| 232 |
272 |
|
let (at, to) = (head, head + length);
|
| 233 |
|
- |
(long.is_char_boundary(at) && long.is_char_boundary(to)).then_some(Span {
|
|
273 |
+ |
(first <= at
|
|
274 |
+ |
&& long.is_char_boundary(at)
|
|
275 |
+ |
&& long.is_char_boundary(to)
|
|
276 |
+ |
&& long.is_char_boundary(first))
|
|
277 |
+ |
.then_some(Span {
|
| 234 |
278 |
|
at,
|
| 235 |
279 |
|
to,
|
| 236 |
280 |
|
repeats: false,
|
|
281 |
+ |
first,
|
| 237 |
282 |
|
})
|
| 238 |
283 |
|
}
|
| 239 |
284 |
|
|
|
285 |
+ |
/// One point in each span's range, chosen so the spans nest.
|
|
286 |
+ |
///
|
|
287 |
+ |
/// [`between`] answers with a range because a deletion that shares a boundary
|
|
288 |
+ |
/// with its neighbour has several placements that rebuild the render exactly.
|
|
289 |
+ |
/// Picking one is not a property of that span: it is a property of the set, and
|
|
290 |
+ |
/// this is where the set is known.
|
|
291 |
+ |
///
|
|
292 |
+ |
/// The spans arrive in the order the trace recorded them, which is the order the
|
|
293 |
+ |
/// renderer reached the sites: a container before the members inside it, and
|
|
294 |
+ |
/// siblings left to right. So a span is placed as early as its own range allows
|
|
295 |
+ |
/// but never before the floor, and the floor is the start of the span that
|
|
296 |
+ |
/// encloses it or the end of the sibling before it. A span that will not fit
|
|
297 |
+ |
/// inside the one above it closes that one and is retried against its end.
|
|
298 |
+ |
///
|
|
299 |
+ |
/// A span whose range cannot satisfy the floor is a genuine overlap rather than
|
|
300 |
+ |
/// an ambiguity, and [`build`] is where that is refused: this narrows the
|
|
301 |
+ |
/// choices and never widens them, so nothing it does can turn a real overlap
|
|
302 |
+ |
/// into a tree.
|
|
303 |
+ |
fn placed(spans: &mut [Span]) {
|
|
304 |
+ |
// Document order, and a container before what is inside it. `derive`
|
|
305 |
+ |
// collects every loop and then every guard, so the order they arrive in is
|
|
306 |
+ |
// the order of two lists rather than the order of the screen; sorting by the
|
|
307 |
+ |
// earliest each could sit at, longest first, is that order recovered from
|
|
308 |
+ |
// the spans themselves.
|
|
309 |
+ |
spans.sort_by_key(|span| (span.first, std::cmp::Reverse(span.to - span.at)));
|
|
310 |
+ |
|
|
311 |
+ |
// The `to` of every span still open, outermost last.
|
|
312 |
+ |
let mut open: Vec<usize> = Vec::new();
|
|
313 |
+ |
let mut floor = 0;
|
|
314 |
+ |
|
|
315 |
+ |
for span in spans.iter_mut() {
|
|
316 |
+ |
let length = span.to - span.at;
|
|
317 |
+ |
loop {
|
|
318 |
+ |
let at = span.first.max(floor).min(span.at);
|
|
319 |
+ |
let to = at + length;
|
|
320 |
+ |
match open.last() {
|
|
321 |
+ |
// It does not fit inside the span above it, so that one ends
|
|
322 |
+ |
// here and this is a sibling of it rather than a member.
|
|
323 |
+ |
Some(&bound) if to > bound => {
|
|
324 |
+ |
floor = bound;
|
|
325 |
+ |
open.pop();
|
|
326 |
+ |
}
|
|
327 |
+ |
_ => {
|
|
328 |
+ |
span.at = at;
|
|
329 |
+ |
span.to = to;
|
|
330 |
+ |
floor = at;
|
|
331 |
+ |
open.push(to);
|
|
332 |
+ |
break;
|
|
333 |
+ |
}
|
|
334 |
+ |
}
|
|
335 |
+ |
}
|
|
336 |
+ |
}
|
|
337 |
+ |
}
|
|
338 |
+ |
|
| 240 |
339 |
|
/// The spans, as a tree over the base render.
|
| 241 |
340 |
|
///
|
| 242 |
341 |
|
/// Outermost first and then by position, so a span is placed inside the last
|
| 243 |
342 |
|
/// one still open. Anything that leaves an open span without closing it inside
|
| 244 |
343 |
|
/// is an overlap rather than a nesting, and there is no tree for that.
|
| 245 |
344 |
|
fn tree(base: &str, spans: &mut Vec<Span>) -> Vec<Op> {
|
|
345 |
+ |
placed(spans);
|
| 246 |
346 |
|
spans.sort_by_key(|span| (span.at, std::cmp::Reverse(span.to)));
|
| 247 |
347 |
|
spans.dedup();
|
| 248 |
348 |
|
build(base, 0, base.len(), spans, &mut 0)
|
| 304 |
404 |
|
}
|
| 305 |
405 |
|
ops
|
| 306 |
406 |
|
}
|
|
407 |
+ |
|
|
408 |
+ |
#[cfg(test)]
|
|
409 |
+ |
mod tests {
|
|
410 |
+ |
use super::{Span, between, placed, shrunk};
|
|
411 |
+ |
|
|
412 |
+ |
/// Two described regions, which is what a pair of guarded siblings renders
|
|
413 |
+ |
/// as. They share `<section id="` and `</section>`, and that shared boundary
|
|
414 |
+ |
/// is the whole of the ambiguity.
|
|
415 |
+ |
const FIRST: &str =
|
|
416 |
+ |
"<section id=\"fan-plus-membership\" class=\"region group\">MEMBER</section>";
|
|
417 |
+ |
const SECOND: &str = "<section id=\"fan-plus-pitch\" class=\"region group\">PITCH</section>";
|
|
418 |
+ |
|
|
419 |
+ |
fn both() -> String {
|
|
420 |
+ |
format!("{FIRST}{SECOND}")
|
|
421 |
+ |
}
|
|
422 |
+ |
|
|
423 |
+ |
/// The wrong placement rebuilds the render exactly, so no check on one span
|
|
424 |
+ |
/// can reject it.
|
|
425 |
+ |
///
|
|
426 |
+ |
/// This is why the span carries a range rather than a point. Removing the
|
|
427 |
+ |
/// bytes at the *latest* placement gives back the same string as removing
|
|
428 |
+ |
/// the ones the screen actually has, because the two regions begin the same
|
|
429 |
+ |
/// way; a residual built on it would put a branch across two siblings.
|
|
430 |
+ |
#[test]
|
|
431 |
+ |
fn a_deletion_sharing_a_boundary_has_more_than_one_placement() {
|
|
432 |
+ |
let full = both();
|
|
433 |
+ |
let span = shrunk(&full, SECOND).expect("the first region is a deletion");
|
|
434 |
+ |
|
|
435 |
+ |
assert_eq!(span.first, 0, "the region does start at the front");
|
|
436 |
+ |
assert!(
|
|
437 |
+ |
span.at > span.first,
|
|
438 |
+ |
"the latest placement should differ, or there is no ambiguity to fix: {span:?}"
|
|
439 |
+ |
);
|
|
440 |
+ |
|
|
441 |
+ |
// Both ends of the range put the same string back, which is the reason
|
|
442 |
+ |
// the choice cannot be made here.
|
|
443 |
+ |
for at in [span.first, span.at] {
|
|
444 |
+ |
let length = span.to - span.at;
|
|
445 |
+ |
let rebuilt = format!("{}{}", &full[..at], &full[at + length..]);
|
|
446 |
+ |
assert_eq!(rebuilt, SECOND, "placement {at} is genuinely valid");
|
|
447 |
+ |
}
|
|
448 |
+ |
}
|
|
449 |
+ |
|
|
450 |
+ |
/// Placed against each other, two siblings land where they belong.
|
|
451 |
+ |
///
|
|
452 |
+ |
/// The case that panicked while `/fan-plus` was being staged: MNW
|
|
453 |
+ |
/// `b4a83103`, quasicoherent `14d42beb`. The first region wants the earliest
|
|
454 |
+ |
/// placement in its range and the second wants the latest, so neither
|
|
455 |
+ |
/// "always earliest" nor "always latest" is the rule, and reading only the
|
|
456 |
+ |
/// latest is what overlapped them.
|
|
457 |
+ |
#[test]
|
|
458 |
+ |
fn two_guarded_siblings_are_placed_where_they_are() {
|
|
459 |
+ |
let full = both();
|
|
460 |
+ |
let mut spans = vec![
|
|
461 |
+ |
shrunk(&full, SECOND).expect("the first region"),
|
|
462 |
+ |
shrunk(&full, FIRST).expect("the second region"),
|
|
463 |
+ |
];
|
|
464 |
+ |
|
|
465 |
+ |
placed(&mut spans);
|
|
466 |
+ |
|
|
467 |
+ |
assert_eq!((spans[0].at, spans[0].to), (0, FIRST.len()), "{spans:?}");
|
|
468 |
+ |
assert_eq!(
|
|
469 |
+ |
(spans[1].at, spans[1].to),
|
|
470 |
+ |
(FIRST.len(), full.len()),
|
|
471 |
+ |
"{spans:?}"
|
|
472 |
+ |
);
|
|
473 |
+ |
assert!(
|
|
474 |
+ |
spans[0].to <= spans[1].at,
|
|
475 |
+ |
"they must not overlap: {spans:?}"
|
|
476 |
+ |
);
|
|
477 |
+ |
}
|
|
478 |
+ |
|
|
479 |
+ |
/// A span inside another stays inside it.
|
|
480 |
+ |
///
|
|
481 |
+ |
/// The other half of what `placed` has to get right: the floor pushes a
|
|
482 |
+ |
/// span rightwards, and a member pushed past the end of its container would
|
|
483 |
+ |
/// be a worse answer than the overlap. A span that still fits is a member,
|
|
484 |
+ |
/// and only one that cannot fit closes the container and becomes a sibling.
|
|
485 |
+ |
#[test]
|
|
486 |
+ |
fn a_span_that_fits_inside_another_stays_inside_it() {
|
|
487 |
+ |
let mut spans = vec![
|
|
488 |
+ |
Span {
|
|
489 |
+ |
at: 0,
|
|
490 |
+ |
to: 100,
|
|
491 |
+ |
repeats: false,
|
|
492 |
+ |
first: 0,
|
|
493 |
+ |
},
|
|
494 |
+ |
Span {
|
|
495 |
+ |
at: 30,
|
|
496 |
+ |
to: 60,
|
|
497 |
+ |
repeats: false,
|
|
498 |
+ |
first: 10,
|
|
499 |
+ |
},
|
|
500 |
+ |
];
|
|
501 |
+ |
|
|
502 |
+ |
placed(&mut spans);
|
|
503 |
+ |
|
|
504 |
+ |
assert!(
|
|
505 |
+ |
spans[1].at >= spans[0].at && spans[1].to <= spans[0].to,
|
|
506 |
+ |
"the inner span left its container: {spans:?}"
|
|
507 |
+ |
);
|
|
508 |
+ |
}
|
|
509 |
+ |
|
|
510 |
+ |
/// One shared byte is enough to make a range, and markup always shares one.
|
|
511 |
+ |
///
|
|
512 |
+ |
/// Two elements with nothing in common but the `<` they open with still
|
|
513 |
+ |
/// admit two placements. That is the argument for resolving the range rather
|
|
514 |
+ |
/// than for detecting a special case: there is no markup where a deletion
|
|
515 |
+ |
/// followed by a sibling is unambiguous, so the narrow reading was never
|
|
516 |
+ |
/// right, and the pair that panicked was only the first to overlap by enough
|
|
517 |
+ |
/// to notice.
|
|
518 |
+ |
#[test]
|
|
519 |
+ |
fn even_one_shared_byte_makes_a_range() {
|
|
520 |
+ |
let full = "<p>alpha</p><div>beta</div>";
|
|
521 |
+ |
let span = shrunk(full, "<div>beta</div>").expect("a deletion");
|
|
522 |
+ |
|
|
523 |
+ |
assert_eq!(span.first, 0, "the paragraph does start at the front");
|
|
524 |
+ |
assert_eq!(
|
|
525 |
+ |
span.at, 1,
|
|
526 |
+ |
"and the `<` is shared, so it may also start later"
|
|
527 |
+ |
);
|
|
528 |
+ |
|
|
529 |
+ |
let mut spans = vec![span];
|
|
530 |
+ |
placed(&mut spans);
|
|
531 |
+ |
assert_eq!(
|
|
532 |
+ |
(spans[0].at, spans[0].to),
|
|
533 |
+ |
(0, "<p>alpha</p>".len()),
|
|
534 |
+ |
"resolved against nothing else, a span takes the earliest it may"
|
|
535 |
+ |
);
|
|
536 |
+ |
}
|
|
537 |
+ |
|
|
538 |
+ |
/// A deletion with genuinely nothing in common is a point.
|
|
539 |
+ |
#[test]
|
|
540 |
+ |
fn a_deletion_with_no_shared_boundary_has_one_placement() {
|
|
541 |
+ |
let span = shrunk("ALPHAbeta", "beta").expect("a deletion");
|
|
542 |
+ |
|
|
543 |
+ |
assert_eq!((span.first, span.at, span.to), (0, 0, "ALPHA".len()));
|
|
544 |
+ |
}
|
|
545 |
+ |
|
|
546 |
+ |
/// `between` refuses when the lengths do not add up, rather than guessing.
|
|
547 |
+ |
#[test]
|
|
548 |
+ |
fn a_pair_that_is_not_a_deletion_is_refused() {
|
|
549 |
+ |
assert!(between("abc", "axbxc", 2).is_none());
|
|
550 |
+ |
}
|
|
551 |
+ |
}
|