add extensions

This commit is contained in:
appflowy 2021-08-11 08:40:58 +08:00
parent 1490d5c1b0
commit 15c3a821ec
14 changed files with 360 additions and 264 deletions

View File

@ -53,10 +53,7 @@ class History {
if (stack.undo.isNotEmpty) {
final lastDelta = stack.undo.removeLast();
print("undoDelta: $undoDelta");
print("lastDelta: $lastDelta");
undoDelta = undoDelta.compose(lastDelta);
print("compose result: $undoDelta");
} else {
lastRecorded = timestamp;
}

View File

@ -40,21 +40,21 @@ class Rules {
final List<Rule> _rules;
static final Rules _instance = Rules([
const FormatLinkAtCaretPositionRule(),
const ResolveLineFormatRule(),
const ResolveInlineFormatRule(),
const InsertEmbedsRule(),
const ForceNewlineForInsertsAroundEmbedRule(),
const AutoExitBlockRule(),
const PreserveBlockStyleOnInsertRule(),
const PreserveLineStyleOnSplitRule(),
const ResetLineFormatOnNewLineRule(),
const AutoFormatLinksRule(),
const PreserveInlineStylesRule(),
const CatchAllInsertRule(),
const EnsureEmbedLineRule(),
const PreserveLineStyleOnMergeRule(),
const CatchAllDeleteRule(),
// const FormatLinkAtCaretPositionRule(),
// const ResolveLineFormatRule(),
// const ResolveInlineFormatRule(),
// const InsertEmbedsRule(),
// const ForceNewlineForInsertsAroundEmbedRule(),
// const AutoExitBlockRule(),
// const PreserveBlockStyleOnInsertRule(),
// const PreserveLineStyleOnSplitRule(),
// const ResetLineFormatOnNewLineRule(),
// const AutoFormatLinksRule(),
// const PreserveInlineStylesRule(),
// const CatchAllInsertRule(),
// const EnsureEmbedLineRule(),
// const PreserveLineStyleOnMergeRule(),
// const CatchAllDeleteRule(),
]);
static Rules getInstance() => _instance;

View File

@ -13,6 +13,7 @@ derive_more = {version = "0.99", features = ["display"]}
log = "0.4"
color-eyre = { version = "0.5", default-features = false }
chrono = "0.4.19"
lazy_static = "1.4.0"
[dev-dependencies]
criterion = "0.3"

View File

@ -24,7 +24,7 @@ impl Document {
}
}
pub fn edit(&mut self, index: usize, text: &str) -> Result<(), OTError> {
pub fn insert(&mut self, index: usize, text: &str) -> Result<(), OTError> {
if self.data.target_len < index {
log::error!(
"{} out of bounds. should 0..{}",
@ -59,6 +59,21 @@ impl Document {
self.update_with_attribute(attributes, interval)
}
pub fn replace(&mut self, interval: Interval, s: &str) -> Result<(), OTError> {
let mut delta = Delta::default();
if !s.is_empty() {
let insert = Builder::insert(s).attributes(Attributes::Follow).build();
delta.add(insert);
}
if !interval.is_empty() {
let delete = Builder::delete(interval.size()).build();
delta.add(delete);
}
self.update_with_op(&delta, interval)
}
pub fn can_undo(&self) -> bool { self.history.can_undo() }
pub fn can_redo(&self) -> bool { self.history.can_redo() }
@ -93,21 +108,6 @@ impl Document {
}
}
pub fn replace(&mut self, interval: Interval, s: &str) -> Result<(), OTError> {
let mut delta = Delta::default();
if !s.is_empty() {
let insert = Builder::insert(s).attributes(Attributes::Follow).build();
delta.add(insert);
}
if !interval.is_empty() {
let delete = Builder::delete(interval.size()).build();
delta.add(delete);
}
self.update_with_op(&delta, interval)
}
pub fn to_json(&self) -> String { self.data.to_json() }
pub fn to_string(&self) -> String { self.data.apply("").unwrap() }

View File

@ -1,5 +1,6 @@
mod document;
mod history;
mod view;
pub use document::*;
pub use history::*;

View File

@ -0,0 +1,23 @@
use crate::{
client::{view::insert_ext::*, Document},
core::{Attributes, Interval},
};
use lazy_static::lazy_static;
pub trait InsertExt {
fn apply(document: &Document, s: &str, interval: Interval);
}
pub trait FormatExt {
fn apply(document: &Document, interval: Interval, attributes: Attributes);
}
pub trait DeleteExt {
fn apply(document: &Document, interval: Interval);
}
lazy_static! {
static ref INSERT_EXT: Vec<Box<InsertExt>> = vec![PreserveInlineStyleExt::new(),];
static ref FORMAT_EXT: Vec<Box<FormatExt>> = vec![];
static ref DELETE_EXT: Vec<Box<DeleteExt>> = vec![];
}

View File

@ -0,0 +1,14 @@
use crate::{
client::{view::InsertExt, Document},
core::Interval,
};
pub struct PreserveInlineStyleExt {}
impl PreserveInlineStyleExt {
pub fn new() -> Self {}
}
impl InsertExt for PreserveInlineStyleExt {
fn apply(document: &Document, s: &str, interval: Interval) { unimplemented!() }
}

View File

@ -0,0 +1,6 @@
mod extension;
mod insert_ext;
pub use extension::*;
pub use insert_ext::*;

View File

@ -0,0 +1,98 @@
use crate::core::{Delta, Interval, Operation};
use std::{cmp::min, slice::Iter};
pub struct Cursor<'a> {
delta: &'a Delta,
interval: Interval,
iterator: Iter<'a, Operation>,
offset: usize,
}
impl<'a> Cursor<'a> {
pub fn new(delta: &'a Delta, interval: Interval) -> Cursor<'a> {
let mut cursor = Self {
delta,
interval,
iterator: delta.ops.iter(),
offset: 0,
};
cursor
}
pub fn next_op(&mut self) -> Option<Operation> {
let mut next_op = self.iterator.next();
let mut find_op = None;
while find_op.is_none() && next_op.is_some() {
let op = next_op.unwrap();
if self.offset < self.interval.start {
let intersect =
Interval::new(self.offset, self.offset + op.length()).intersect(self.interval);
if intersect.is_empty() {
self.offset += op.length();
} else {
if let Some(new_op) = op.shrink(intersect.translate_neg(self.offset)) {
// shrink the op to fit the intersect range
// ┌──────────────┐
// │ 1 2 3 4 5 6 │
// └───────▲───▲──┘
// │ │
// [3, 5)
// op = "45"
find_op = Some(new_op);
}
self.offset = intersect.end;
}
} else {
// the interval passed in the shrink function is base on the op not the delta.
if let Some(new_op) = op.shrink(self.interval.translate_neg(self.offset)) {
find_op = Some(new_op);
}
// for example: extract the ops from three insert ops with interval [2,5). the
// interval size is larger than the op. Moving the offset to extract each part.
// Each step would be the small value between interval.size() and
// next_op.length(). Checkout the delta_get_ops_in_interval_4 for more details.
//
// ┌──────┐ ┌──────┐ ┌──────┐
// │ 1 2 │ │ 3 4 │ │ 5 6 │
// └──────┘ └─▲────┘ └───▲──┘
// │ [2, 5) │
//
self.offset += min(self.interval.size(), op.length());
}
match find_op {
None => next_op = self.iterator.next(),
Some(_) => self.interval.start = self.offset,
}
}
find_op
}
}
pub struct DeltaIter<'a> {
cursor: Cursor<'a>,
interval: Interval,
}
impl<'a> DeltaIter<'a> {
pub fn new(delta: &'a Delta, interval: Interval) -> Self {
let cursor = Cursor::new(delta, interval);
Self { cursor, interval }
}
pub fn ops(&mut self) -> Vec<Operation> { self.collect::<Vec<_>>() }
}
impl<'a> Iterator for DeltaIter<'a> {
type Item = Operation;
fn next(&mut self) -> Option<Self::Item> { self.cursor.next_op() }
}
#[cfg(test)]
mod tests {
#[test]
fn test() {}
}

View File

@ -1,5 +1,5 @@
use crate::{
core::{attributes::*, operation::*, Interval},
core::{attributes::*, operation::*, DeltaIter, Interval},
errors::{ErrorBuilder, OTError, OTErrorCode},
};
use bytecount::num_chars;
@ -530,59 +530,6 @@ impl Delta {
pub fn is_empty(&self) -> bool { self.ops.is_empty() }
pub fn ops_in_interval(&self, mut interval: Interval) -> Vec<Operation> {
log::debug!("try get ops in delta: {} at {}", self, interval);
let mut ops: Vec<Operation> = Vec::with_capacity(self.ops.len());
let mut ops_iter = self.ops.iter();
let mut maybe_next_op = ops_iter.next();
let mut offset: usize = 0;
while maybe_next_op.is_some() {
let next_op = maybe_next_op.take().unwrap();
if offset < interval.start {
let next_op_i = Interval::new(offset, offset + next_op.length());
let intersect = next_op_i.intersect(interval);
if intersect.is_empty() {
offset += next_op_i.size();
} else {
if let Some(new_op) = next_op.shrink(intersect.translate_neg(offset)) {
// shrink the op to fit the intersect range
// ┌──────────────┐
// │ 1 2 3 4 5 6 │
// └───────▲───▲──┘
// │ │
// [3, 5)
// op = "45"
ops.push(new_op);
}
offset = intersect.end;
interval = Interval::new(offset, interval.end);
}
} else {
// the interval passed in the shrink function is base on the op not the delta.
if let Some(new_op) = next_op.shrink(interval.translate_neg(offset)) {
ops.push(new_op);
}
// for example: extract the ops from three insert ops with interval [2,5). the
// interval size is larger than the op. Moving the offset to extract each part.
// Each step would be the small value between interval.size() and
// next_op.length(). Checkout the delta_get_ops_in_interval_4 for more details.
//
// ┌──────┐ ┌──────┐ ┌──────┐
// │ 1 2 │ │ 3 4 │ │ 5 6 │
// └──────┘ └─▲────┘ └───▲──┘
// │ [2, 5) │
//
offset += min(interval.size(), next_op.length());
interval = Interval::new(offset, interval.end);
}
maybe_next_op = ops_iter.next();
}
log::debug!("did get ops : {:?}", ops);
ops
}
pub fn get_attributes(&self, interval: Interval) -> Attributes {
let mut attributes_data = AttributesData::new();
let mut offset: usize = 0;
@ -626,7 +573,7 @@ fn invert_from_other(
end: usize,
) {
log::debug!("invert op: {} [{}:{}]", operation, start, end);
let other_ops = other.ops_in_interval(Interval::new(start, end));
let other_ops = DeltaIter::new(other, Interval::new(start, end)).ops();
other_ops.into_iter().for_each(|other_op| match operation {
Operation::Delete(n) => {
log::debug!("invert delete: {} by add {}", n, other_op);

View File

@ -0,0 +1,5 @@
mod cursor;
mod delta;
pub use cursor::*;
pub use delta::*;

View File

@ -73,7 +73,7 @@ impl OpTester {
match op {
TestOp::Insert(delta_i, s, index) => {
let document = &mut self.documents[*delta_i];
document.edit(*index, s).unwrap();
document.insert(*index, s).unwrap();
},
TestOp::Delete(delta_i, interval) => {
let document = &mut self.documents[*delta_i];
@ -85,7 +85,7 @@ impl OpTester {
},
TestOp::InsertBold(delta_i, s, interval) => {
let document = &mut self.documents[*delta_i];
document.edit(interval.start, s).unwrap();
document.insert(interval.start, s).unwrap();
document.format(*interval, Attribute::Bold, true).unwrap();
},
TestOp::Bold(delta_i, interval, enable) => {

View File

@ -1,156 +0,0 @@
pub mod helper;
use crate::helper::{TestOp::*, *};
use flowy_ot::core::{Builder, Delta, Interval};
#[test]
fn delta_invert_no_attribute_delta() {
let mut delta = Delta::default();
delta.add(Builder::insert("123").build());
let mut change = Delta::default();
change.add(Builder::retain(3).build());
change.add(Builder::insert("456").build());
let undo = change.invert(&delta);
let new_delta = delta.compose(&change).unwrap();
let delta_after_undo = new_delta.compose(&undo).unwrap();
assert_eq!(delta_after_undo, delta);
}
#[test]
fn delta_invert_no_attribute_delta2() {
let ops = vec![
Insert(0, "123", 0),
Insert(1, "4567", 0),
Invert(0, 1),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_attribute_delta_with_no_attribute_delta() {
let ops = vec![
Insert(0, "123", 0),
Bold(0, Interval::new(0, 3), true),
AssertOpsJson(0, r#"[{"insert":"123","attributes":{"bold":"true"}}]"#),
Insert(1, "4567", 0),
Invert(0, 1),
AssertOpsJson(0, r#"[{"insert":"123","attributes":{"bold":"true"}}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_attribute_delta_with_no_attribute_delta2() {
let ops = vec![
Insert(0, "123", 0),
Bold(0, Interval::new(0, 3), true),
Insert(0, "456", 3),
AssertOpsJson(
0,
r#"[
{"insert":"123456","attributes":{"bold":"true"}}]
"#,
),
Italic(0, Interval::new(2, 4), true),
AssertOpsJson(
0,
r#"[
{"insert":"12","attributes":{"bold":"true"}},
{"insert":"34","attributes":{"bold":"true","italic":"true"}},
{"insert":"56","attributes":{"bold":"true"}}
]"#,
),
Insert(1, "abc", 0),
Invert(0, 1),
AssertOpsJson(
0,
r#"[
{"insert":"12","attributes":{"bold":"true"}},
{"insert":"34","attributes":{"bold":"true","italic":"true"}},
{"insert":"56","attributes":{"bold":"true"}}
]"#,
),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_no_attribute_delta_with_attribute_delta() {
let ops = vec![
Insert(0, "123", 0),
Insert(1, "4567", 0),
Bold(1, Interval::new(0, 3), true),
AssertOpsJson(
1,
r#"[{"insert":"456","attributes":{"bold":"true"}},{"insert":"7"}]"#,
),
Invert(0, 1),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_no_attribute_delta_with_attribute_delta2() {
let ops = vec![
Insert(0, "123", 0),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
Insert(1, "abc", 0),
Bold(1, Interval::new(0, 3), true),
Insert(1, "d", 3),
Italic(1, Interval::new(1, 3), true),
AssertOpsJson(
1,
r#"[{"insert":"a","attributes":{"bold":"true"}},{"insert":"bc","attributes":
{"bold":"true","italic":"true"}},{"insert":"d","attributes":{"bold":"true"
}}]"#,
),
Invert(0, 1),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_attribute_delta_with_attribute_delta() {
let ops = vec![
Insert(0, "123", 0),
Bold(0, Interval::new(0, 3), true),
Insert(0, "456", 3),
AssertOpsJson(0, r#"[{"insert":"123456","attributes":{"bold":"true"}}]"#),
Italic(0, Interval::new(2, 4), true),
AssertOpsJson(
0,
r#"[
{"insert":"12","attributes":{"bold":"true"}},
{"insert":"34","attributes":{"bold":"true","italic":"true"}},
{"insert":"56","attributes":{"bold":"true"}}
]"#,
),
Insert(1, "abc", 0),
Bold(1, Interval::new(0, 3), true),
Insert(1, "d", 3),
Italic(1, Interval::new(1, 3), true),
AssertOpsJson(
1,
r#"[
{"insert":"a","attributes":{"bold":"true"}},
{"insert":"bc","attributes":{"bold":"true","italic":"true"}},
{"insert":"d","attributes":{"bold":"true"}}
]"#,
),
Invert(0, 1),
AssertOpsJson(
0,
r#"[
{"insert":"12","attributes":{"bold":"true"}},
{"insert":"34","attributes":{"bold":"true","italic":"true"}},
{"insert":"56","attributes":{"bold":"true"}}
]"#,
),
];
OpTester::new().run_script(ops);
}

View File

@ -14,10 +14,8 @@ fn delta_get_ops_in_interval_1() {
delta.add(insert_a.clone());
delta.add(insert_b.clone());
assert_eq!(
delta.ops_in_interval(Interval::new(0, 4)),
vec![delta.ops.last().unwrap().clone()]
);
let mut iterator = DeltaIter::new(&delta, Interval::new(0, 4));
assert_eq!(iterator.ops(), delta.ops);
}
#[test]
@ -34,27 +32,27 @@ fn delta_get_ops_in_interval_2() {
delta.add(insert_c.clone());
assert_eq!(
delta.ops_in_interval(Interval::new(0, 2)),
DeltaIter::new(&delta, Interval::new(0, 2)).ops(),
vec![Builder::insert("12").build()]
);
assert_eq!(
delta.ops_in_interval(Interval::new(0, 3)),
DeltaIter::new(&delta, Interval::new(0, 3)).ops(),
vec![insert_a.clone()]
);
assert_eq!(
delta.ops_in_interval(Interval::new(0, 4)),
DeltaIter::new(&delta, Interval::new(0, 4)).ops(),
vec![insert_a.clone(), Builder::retain(1).build()]
);
assert_eq!(
delta.ops_in_interval(Interval::new(0, 6)),
DeltaIter::new(&delta, Interval::new(0, 6)).ops(),
vec![insert_a.clone(), retain_a.clone()]
);
assert_eq!(
delta.ops_in_interval(Interval::new(0, 7)),
DeltaIter::new(&delta, Interval::new(0, 7)).ops(),
vec![insert_a.clone(), retain_a.clone(), insert_b.clone()]
);
}
@ -65,7 +63,7 @@ fn delta_get_ops_in_interval_3() {
let insert_a = Builder::insert("123456").build();
delta.add(insert_a.clone());
assert_eq!(
delta.ops_in_interval(Interval::new(3, 5)),
DeltaIter::new(&delta, Interval::new(3, 5)).ops(),
vec![Builder::insert("45").build()]
);
}
@ -81,12 +79,21 @@ fn delta_get_ops_in_interval_4() {
delta.ops.push(insert_b.clone());
delta.ops.push(insert_c.clone());
assert_eq!(delta.ops_in_interval(Interval::new(0, 2)), vec![insert_a]);
assert_eq!(delta.ops_in_interval(Interval::new(2, 4)), vec![insert_b]);
assert_eq!(delta.ops_in_interval(Interval::new(4, 6)), vec![insert_c]);
assert_eq!(
DeltaIter::new(&delta, Interval::new(0, 2)).ops(),
vec![insert_a]
);
assert_eq!(
DeltaIter::new(&delta, Interval::new(2, 4)).ops(),
vec![insert_b]
);
assert_eq!(
DeltaIter::new(&delta, Interval::new(4, 6)).ops(),
vec![insert_c]
);
assert_eq!(
delta.ops_in_interval(Interval::new(2, 5)),
DeltaIter::new(&delta, Interval::new(2, 5)).ops(),
vec![Builder::insert("34").build(), Builder::insert("5").build()]
);
}
@ -99,12 +106,12 @@ fn delta_get_ops_in_interval_5() {
delta.ops.push(insert_a.clone());
delta.ops.push(insert_b.clone());
assert_eq!(
delta.ops_in_interval(Interval::new(4, 8)),
DeltaIter::new(&delta, Interval::new(4, 8)).ops(),
vec![Builder::insert("56").build(), Builder::insert("78").build()]
);
assert_eq!(
delta.ops_in_interval(Interval::new(8, 9)),
DeltaIter::new(&delta, Interval::new(8, 9)).ops(),
vec![Builder::insert("9").build()]
);
}
@ -115,7 +122,7 @@ fn delta_get_ops_in_interval_6() {
let insert_a = Builder::insert("12345678").build();
delta.add(insert_a.clone());
assert_eq!(
delta.ops_in_interval(Interval::new(4, 6)),
DeltaIter::new(&delta, Interval::new(4, 6)).ops(),
vec![Builder::insert("56").build()]
);
}
@ -336,3 +343,156 @@ fn delta_transform_test() {
serde_json::to_string(&b_prime).unwrap()
);
}
#[test]
fn delta_invert_no_attribute_delta() {
let mut delta = Delta::default();
delta.add(Builder::insert("123").build());
let mut change = Delta::default();
change.add(Builder::retain(3).build());
change.add(Builder::insert("456").build());
let undo = change.invert(&delta);
let new_delta = delta.compose(&change).unwrap();
let delta_after_undo = new_delta.compose(&undo).unwrap();
assert_eq!(delta_after_undo, delta);
}
#[test]
fn delta_invert_no_attribute_delta2() {
let ops = vec![
Insert(0, "123", 0),
Insert(1, "4567", 0),
Invert(0, 1),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_attribute_delta_with_no_attribute_delta() {
let ops = vec![
Insert(0, "123", 0),
Bold(0, Interval::new(0, 3), true),
AssertOpsJson(0, r#"[{"insert":"123","attributes":{"bold":"true"}}]"#),
Insert(1, "4567", 0),
Invert(0, 1),
AssertOpsJson(0, r#"[{"insert":"123","attributes":{"bold":"true"}}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_attribute_delta_with_no_attribute_delta2() {
let ops = vec![
Insert(0, "123", 0),
Bold(0, Interval::new(0, 3), true),
Insert(0, "456", 3),
AssertOpsJson(
0,
r#"[
{"insert":"123456","attributes":{"bold":"true"}}]
"#,
),
Italic(0, Interval::new(2, 4), true),
AssertOpsJson(
0,
r#"[
{"insert":"12","attributes":{"bold":"true"}},
{"insert":"34","attributes":{"bold":"true","italic":"true"}},
{"insert":"56","attributes":{"bold":"true"}}
]"#,
),
Insert(1, "abc", 0),
Invert(0, 1),
AssertOpsJson(
0,
r#"[
{"insert":"12","attributes":{"bold":"true"}},
{"insert":"34","attributes":{"bold":"true","italic":"true"}},
{"insert":"56","attributes":{"bold":"true"}}
]"#,
),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_no_attribute_delta_with_attribute_delta() {
let ops = vec![
Insert(0, "123", 0),
Insert(1, "4567", 0),
Bold(1, Interval::new(0, 3), true),
AssertOpsJson(
1,
r#"[{"insert":"456","attributes":{"bold":"true"}},{"insert":"7"}]"#,
),
Invert(0, 1),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_no_attribute_delta_with_attribute_delta2() {
let ops = vec![
Insert(0, "123", 0),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
Insert(1, "abc", 0),
Bold(1, Interval::new(0, 3), true),
Insert(1, "d", 3),
Italic(1, Interval::new(1, 3), true),
AssertOpsJson(
1,
r#"[{"insert":"a","attributes":{"bold":"true"}},{"insert":"bc","attributes":
{"bold":"true","italic":"true"}},{"insert":"d","attributes":{"bold":"true"
}}]"#,
),
Invert(0, 1),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_attribute_delta_with_attribute_delta() {
let ops = vec![
Insert(0, "123", 0),
Bold(0, Interval::new(0, 3), true),
Insert(0, "456", 3),
AssertOpsJson(0, r#"[{"insert":"123456","attributes":{"bold":"true"}}]"#),
Italic(0, Interval::new(2, 4), true),
AssertOpsJson(
0,
r#"[
{"insert":"12","attributes":{"bold":"true"}},
{"insert":"34","attributes":{"bold":"true","italic":"true"}},
{"insert":"56","attributes":{"bold":"true"}}
]"#,
),
Insert(1, "abc", 0),
Bold(1, Interval::new(0, 3), true),
Insert(1, "d", 3),
Italic(1, Interval::new(1, 3), true),
AssertOpsJson(
1,
r#"[
{"insert":"a","attributes":{"bold":"true"}},
{"insert":"bc","attributes":{"bold":"true","italic":"true"}},
{"insert":"d","attributes":{"bold":"true"}}
]"#,
),
Invert(0, 1),
AssertOpsJson(
0,
r#"[
{"insert":"12","attributes":{"bold":"true"}},
{"insert":"34","attributes":{"bold":"true","italic":"true"}},
{"insert":"56","attributes":{"bold":"true"}}
]"#,
),
];
OpTester::new().run_script(ops);
}