splite attribute and operation into crate

This commit is contained in:
appflowy 2021-08-05 19:55:28 +08:00
parent ab2ee27768
commit 97bba6e286
8 changed files with 86 additions and 701 deletions

View File

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

View File

@ -1,295 +0,0 @@
use crate::core::Operation;
use std::{collections::HashMap, fmt};
const REMOVE_FLAG: &'static str = "";
fn should_remove(s: &str) -> bool { s == REMOVE_FLAG }
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Attributes {
#[serde(skip)]
Follow,
Custom(AttributesData),
#[serde(skip)]
Empty,
}
impl Attributes {
pub fn data(&self) -> Option<AttributesData> {
match self {
Attributes::Follow => None,
Attributes::Custom(data) => Some(data.clone()),
Attributes::Empty => None,
}
}
pub fn is_empty(&self) -> bool {
match self {
Attributes::Follow => true,
Attributes::Custom(data) => data.is_empty(),
Attributes::Empty => true,
}
}
}
impl std::default::Default for Attributes {
fn default() -> Self { Attributes::Empty }
}
impl fmt::Display for Attributes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Attributes::Follow => {
f.write_str("")?;
},
Attributes::Custom(data) => {
f.write_fmt(format_args!("{:?}", data.inner))?;
},
Attributes::Empty => {
f.write_str("")?;
},
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct AttributesData {
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(flatten)]
inner: HashMap<String, String>,
}
impl AttributesData {
pub fn new() -> Self {
AttributesData {
inner: HashMap::new(),
}
}
pub fn is_empty(&self) -> bool {
self.inner.values().filter(|v| !should_remove(v)).count() == 0
}
fn remove_empty(&mut self) { self.inner.retain(|_, v| !should_remove(v)); }
pub fn extend(&mut self, other: AttributesData) { self.inner.extend(other.inner); }
pub fn merge(&mut self, other: Option<AttributesData>) {
if other.is_none() {
return;
}
let mut new_attributes = other.unwrap().inner;
self.inner.iter().for_each(|(k, v)| {
if should_remove(v) {
new_attributes.remove(k);
} else {
new_attributes.insert(k.clone(), v.clone());
}
});
self.inner = new_attributes;
}
}
pub trait AttributesDataRule {
fn apply_rule(&mut self);
fn into_attributes(self) -> Attributes;
}
impl AttributesDataRule for AttributesData {
fn apply_rule(&mut self) { self.remove_empty(); }
fn into_attributes(mut self) -> Attributes {
self.apply_rule();
if self.is_empty() {
Attributes::Empty
} else {
Attributes::Custom(self)
}
}
}
pub trait AttributesRule {
fn apply_rule(self) -> Attributes;
}
impl AttributesRule for Attributes {
fn apply_rule(self) -> Attributes {
match self {
Attributes::Follow => self,
Attributes::Custom(data) => data.into_attributes(),
Attributes::Empty => self,
}
}
}
impl std::convert::From<HashMap<String, String>> for AttributesData {
fn from(attributes: HashMap<String, String>) -> Self { AttributesData { inner: attributes } }
}
impl std::ops::Deref for AttributesData {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl std::ops::DerefMut for AttributesData {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
}
pub struct AttrsBuilder {
inner: AttributesData,
}
impl AttrsBuilder {
pub fn new() -> Self {
Self {
inner: AttributesData::default(),
}
}
pub fn bold(mut self, bold: bool) -> Self {
let val = match bold {
true => "true",
false => REMOVE_FLAG,
};
self.inner.insert("bold".to_owned(), val.to_owned());
self
}
pub fn italic(mut self, italic: bool) -> Self {
let val = match italic {
true => "true",
false => REMOVE_FLAG,
};
self.inner.insert("italic".to_owned(), val.to_owned());
self
}
pub fn underline(mut self) -> Self {
self.inner.insert("underline".to_owned(), "true".to_owned());
self
}
pub fn build(self) -> Attributes { Attributes::Custom(self.inner) }
}
pub(crate) fn attributes_from(operation: &Option<Operation>) -> Option<Attributes> {
match operation {
None => None,
Some(operation) => Some(operation.get_attributes()),
}
}
pub fn compose_operation(left: &Option<Operation>, right: &Option<Operation>) -> Attributes {
if left.is_none() && right.is_none() {
return Attributes::Empty;
}
let attr_l = attributes_from(left);
let attr_r = attributes_from(right);
if attr_l.is_none() {
return attr_r.unwrap();
}
if attr_r.is_none() {
return attr_l.unwrap();
}
compose_attributes(attr_l.unwrap(), attr_r.unwrap())
}
pub fn transform_operation(left: &Option<Operation>, right: &Option<Operation>) -> Attributes {
let attr_l = attributes_from(left);
let attr_r = attributes_from(right);
if attr_l.is_none() {
if attr_r.is_none() {
return Attributes::Empty;
}
return match attr_r.as_ref().unwrap() {
Attributes::Follow => Attributes::Follow,
Attributes::Custom(_) => attr_r.unwrap(),
Attributes::Empty => Attributes::Empty,
};
}
transform_attributes(attr_l.unwrap(), attr_r.unwrap())
}
pub fn invert_attributes(attr: Attributes, base: Attributes) -> Attributes {
let attr = attr.data();
let base = base.data();
if attr.is_none() && base.is_none() {
return Attributes::Empty;
}
let attr = attr.unwrap_or(AttributesData::new());
let base = base.unwrap_or(AttributesData::new());
let base_inverted = base
.iter()
.fold(AttributesData::new(), |mut attributes, (k, v)| {
if base.get(k) != attr.get(k) && attr.contains_key(k) {
attributes.insert(k.clone(), v.clone());
}
attributes
});
let inverted = attr.iter().fold(base_inverted, |mut attributes, (k, _)| {
if base.get(k) != attr.get(k) && !base.contains_key(k) {
// attributes.insert(k.clone(), "".to_owned());
attributes.remove(k);
}
attributes
});
return Attributes::Custom(inverted);
}
pub fn merge_attributes(attributes: Attributes, other: Attributes) -> Attributes {
match (&attributes, &other) {
(Attributes::Custom(data), Attributes::Custom(o_data)) => {
let mut data = data.clone();
data.extend(o_data.clone());
Attributes::Custom(data)
},
(Attributes::Custom(data), _) => Attributes::Custom(data.clone()),
_ => other,
}
}
fn compose_attributes(left: Attributes, right: Attributes) -> Attributes {
log::trace!("compose_attributes: a: {:?}, b: {:?}", left, right);
let attr = match (&left, &right) {
(_, Attributes::Empty) => Attributes::Empty,
(_, Attributes::Custom(_)) => merge_attributes(left, right),
(Attributes::Custom(_), _) => merge_attributes(left, right),
_ => Attributes::Follow,
};
log::trace!("composed_attributes: a: {:?}", attr);
attr.apply_rule()
}
fn transform_attributes(left: Attributes, right: Attributes) -> Attributes {
match (left, right) {
(Attributes::Custom(data_l), Attributes::Custom(data_r)) => {
let result = data_r
.iter()
.fold(AttributesData::new(), |mut new_attr_data, (k, v)| {
if !data_l.contains_key(k) {
new_attr_data.insert(k.clone(), v.clone());
}
new_attr_data
});
Attributes::Custom(result)
},
_ => Attributes::Empty,
}
}

View File

@ -57,6 +57,8 @@ impl FromIterator<Operation> for Delta {
}
impl Delta {
pub fn new() -> Self { Self::default() }
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
@ -562,7 +564,7 @@ impl Delta {
ops
}
pub fn attributes_in_interval(&self, interval: Interval) -> Attributes {
pub fn get_attributes(&self, interval: Interval) -> Attributes {
let mut attributes_data = AttributesData::new();
let mut offset: usize = 0;

View File

@ -2,7 +2,6 @@ mod attributes;
mod delta;
mod interval;
mod operation;
mod operation_serde;
pub use attributes::*;
pub use delta::*;

View File

@ -1,232 +0,0 @@
use crate::core::Attributes;
use bytecount::num_chars;
use std::{
fmt,
ops::{Deref, DerefMut},
str::Chars,
};
#[derive(Debug, Clone, PartialEq)]
pub enum Operation {
Delete(u64),
Retain(Retain),
Insert(Insert),
}
impl Operation {
pub fn is_delete(&self) -> bool {
match self {
Operation::Delete(_) => true,
_ => false,
}
}
pub fn is_noop(&self) -> bool {
match self {
Operation::Retain(_) => true,
_ => false,
}
}
pub fn get_attributes(&self) -> Attributes {
match self {
Operation::Delete(_) => Attributes::Empty,
Operation::Retain(retain) => retain.attributes.clone(),
Operation::Insert(insert) => insert.attributes.clone(),
}
}
pub fn set_attributes(&mut self, attributes: Attributes) {
match self {
Operation::Delete(_) => {
log::error!("Delete should not contains attributes");
},
Operation::Retain(retain) => {
retain.attributes = attributes;
},
Operation::Insert(insert) => {
insert.attributes = attributes;
},
}
}
pub fn has_attribute(&self) -> bool {
match self.get_attributes() {
Attributes::Follow => true,
Attributes::Custom(_) => false,
Attributes::Empty => true,
}
}
pub fn length(&self) -> u64 {
match self {
Operation::Delete(n) => *n,
Operation::Retain(r) => r.n,
Operation::Insert(i) => i.num_chars(),
}
}
pub fn is_empty(&self) -> bool { self.length() == 0 }
}
impl fmt::Display for Operation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Operation::Delete(n) => {
f.write_fmt(format_args!("delete: {}", n))?;
},
Operation::Retain(r) => {
f.write_fmt(format_args!(
"retain: {}, attributes: {}",
r.n, r.attributes
))?;
},
Operation::Insert(i) => {
f.write_fmt(format_args!(
"insert: {}, attributes: {}",
i.s, i.attributes
))?;
},
}
Ok(())
}
}
pub struct OpBuilder {
ty: Operation,
attrs: Attributes,
}
impl OpBuilder {
pub fn new(ty: Operation) -> OpBuilder {
OpBuilder {
ty,
attrs: Attributes::Empty,
}
}
pub fn retain(n: u64) -> OpBuilder { OpBuilder::new(Operation::Retain(n.into())) }
pub fn delete(n: u64) -> OpBuilder { OpBuilder::new(Operation::Delete(n)) }
pub fn insert(s: &str) -> OpBuilder { OpBuilder::new(Operation::Insert(s.into())) }
pub fn attributes(mut self, attrs: Attributes) -> OpBuilder {
self.attrs = attrs;
self
}
pub fn build(self) -> Operation {
let mut operation = self.ty;
match &mut operation {
Operation::Delete(_) => {},
Operation::Retain(retain) => retain.attributes = self.attrs,
Operation::Insert(insert) => insert.attributes = self.attrs,
}
operation
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Retain {
#[serde(rename(serialize = "retain", deserialize = "retain"))]
pub n: u64,
#[serde(skip_serializing_if = "is_empty")]
pub attributes: Attributes,
}
impl Retain {
pub fn merge_or_new_op(&mut self, n: u64, attributes: Attributes) -> Option<Operation> {
log::debug!(
"merge_retain_or_new_op: {:?}, {:?}",
self.attributes,
attributes
);
match &attributes {
Attributes::Follow => {
log::debug!("Follow attribute: {:?}", self.attributes);
self.n += n;
None
},
Attributes::Custom(_) | Attributes::Empty => {
if self.attributes == attributes {
log::debug!("Attribute equal");
self.n += n;
None
} else {
log::debug!("New retain op");
Some(OpBuilder::retain(n).attributes(attributes).build())
}
},
}
}
}
impl std::convert::From<u64> for Retain {
fn from(n: u64) -> Self {
Retain {
n,
attributes: Attributes::default(),
}
}
}
impl Deref for Retain {
type Target = u64;
fn deref(&self) -> &Self::Target { &self.n }
}
impl DerefMut for Retain {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.n }
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Insert {
#[serde(rename(serialize = "insert", deserialize = "insert"))]
pub s: String,
#[serde(skip_serializing_if = "is_empty")]
pub attributes: Attributes,
}
impl Insert {
pub fn as_bytes(&self) -> &[u8] { self.s.as_bytes() }
pub fn chars(&self) -> Chars<'_> { self.s.chars() }
pub fn num_chars(&self) -> u64 { num_chars(self.s.as_bytes()) as _ }
pub fn merge_or_new_op(&mut self, s: &str, attributes: Attributes) -> Option<Operation> {
match &attributes {
Attributes::Follow => {
self.s += s;
return None;
},
Attributes::Custom(_) | Attributes::Empty => {
if self.attributes == attributes {
self.s += s;
None
} else {
Some(OpBuilder::insert(s).attributes(attributes).build())
}
},
}
}
}
impl std::convert::From<String> for Insert {
fn from(s: String) -> Self {
Insert {
s,
attributes: Attributes::default(),
}
}
}
impl std::convert::From<&str> for Insert {
fn from(s: &str) -> Self { Insert::from(s.to_owned()) }
}
fn is_empty(attributes: &Attributes) -> bool { attributes.is_empty() }

View File

@ -1,137 +0,0 @@
use crate::core::{Attributes, Delta, Operation};
use serde::{
de,
de::{MapAccess, SeqAccess, Visitor},
ser::{SerializeMap, SerializeSeq},
Deserialize,
Deserializer,
Serialize,
Serializer,
};
use std::fmt;
impl Serialize for Operation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Operation::Retain(retain) => retain.serialize(serializer),
Operation::Delete(i) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("delete", i)?;
map.end()
},
Operation::Insert(insert) => insert.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for Operation {
fn deserialize<D>(deserializer: D) -> Result<Operation, D::Error>
where
D: Deserializer<'de>,
{
struct OperationVisitor;
impl<'de> Visitor<'de> for OperationVisitor {
type Value = Operation;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer between -2^64 and 2^63 or a string")
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut operation = None;
let mut attributes = None;
while let Some(key) = map.next_key()? {
match key {
"delete" => {
if operation.is_some() {
return Err(de::Error::duplicate_field("operation"));
}
operation = Some(Operation::Delete(map.next_value()?));
},
"retain" => {
if operation.is_some() {
return Err(de::Error::duplicate_field("operation"));
}
let i: u64 = map.next_value()?;
operation = Some(Operation::Retain(i.into()));
},
"insert" => {
if operation.is_some() {
return Err(de::Error::duplicate_field("operation"));
}
let i: String = map.next_value()?;
operation = Some(Operation::Insert(i.into()));
},
"attributes" => {
if attributes.is_some() {
return Err(de::Error::duplicate_field("attributes"));
}
let map: Attributes = map.next_value()?;
attributes = Some(map);
},
_ => panic!(),
}
}
match operation {
None => Err(de::Error::missing_field("operation")),
Some(mut operation) => {
operation.set_attributes(attributes.unwrap_or(Attributes::Empty));
Ok(operation)
},
}
}
}
deserializer.deserialize_any(OperationVisitor)
}
}
impl Serialize for Delta {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.ops.len()))?;
for op in self.ops.iter() {
seq.serialize_element(op)?;
}
seq.end()
}
}
impl<'de> Deserialize<'de> for Delta {
fn deserialize<D>(deserializer: D) -> Result<Delta, D::Error>
where
D: Deserializer<'de>,
{
struct OperationSeqVisitor;
impl<'de> Visitor<'de> for OperationSeqVisitor {
type Value = Delta;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut o = Delta::default();
while let Some(op) = seq.next_element()? {
o.add(op);
}
Ok(o)
}
}
deserializer.deserialize_seq(OperationSeqVisitor)
}
}

View File

@ -26,8 +26,8 @@ pub enum TestOp {
Transform(usize, usize),
// invert the delta_a base on the delta_b
#[display(fmt = "Invert")]
Invert(usize, usize),
#[display(fmt = "Undo")]
Undo(usize, usize),
#[display(fmt = "AssertStr")]
AssertStr(usize, &'static str),
@ -91,7 +91,7 @@ impl OpTester {
self.deltas[*delta_a_i] = new_delta_a;
self.deltas[*delta_b_i] = new_delta_b;
},
TestOp::Invert(delta_a_i, delta_b_i) => {
TestOp::Undo(delta_a_i, delta_b_i) => {
let delta_a = &self.deltas[*delta_a_i];
let delta_b = &self.deltas[*delta_b_i];
log::debug!("Invert: ");
@ -149,7 +149,7 @@ impl OpTester {
log::error!("{} out of bounds {}", index, target_interval);
}
let mut attributes = old_delta.attributes_in_interval(Interval::new(index, index + 1));
let mut attributes = old_delta.get_attributes(Interval::new(index, index + 1));
if attributes == Attributes::Empty {
attributes = Attributes::Follow;
}
@ -165,7 +165,7 @@ impl OpTester {
interval: &Interval,
) {
let old_delta = &self.deltas[delta_index];
let old_attributes = old_delta.attributes_in_interval(*interval);
let old_attributes = old_delta.get_attributes(*interval);
log::debug!(
"merge attributes: {:?}, with old: {:?}",
attributes,
@ -211,7 +211,7 @@ fn new_delta_with_op(delta: &Delta, op: Operation, interval: Interval) -> Delta
if prefix.is_empty() == false && prefix != interval {
let intervals = split_interval_with_delta(delta, &prefix);
intervals.into_iter().for_each(|p_interval| {
let attributes = delta.attributes_in_interval(p_interval);
let attributes = delta.get_attributes(p_interval);
log::debug!(
"prefix attribute: {:?}, interval: {:?}",
attributes,
@ -228,7 +228,7 @@ fn new_delta_with_op(delta: &Delta, op: Operation, interval: Interval) -> Delta
if suffix.is_empty() == false {
let intervals = split_interval_with_delta(delta, &suffix);
intervals.into_iter().for_each(|s_interval| {
let attributes = delta.attributes_in_interval(s_interval);
let attributes = delta.get_attributes(s_interval);
log::debug!(
"suffix attribute: {:?}, interval: {:?}",
attributes,
@ -241,27 +241,6 @@ fn new_delta_with_op(delta: &Delta, op: Operation, interval: Interval) -> Delta
delta.compose(&new_delta).unwrap()
}
fn split_interval_with_delta(delta: &Delta, interval: &Interval) -> Vec<Interval> {
let mut start = 0;
let mut new_intervals = vec![];
delta.ops.iter().for_each(|op| match op {
Operation::Delete(_) => {},
Operation::Retain(_) => {},
Operation::Insert(insert) => {
let len = insert.num_chars() as usize;
let end = start + len;
let insert_interval = Interval::new(start, end);
let new_interval = interval.intersect(insert_interval);
if !new_interval.is_empty() {
new_intervals.push(new_interval)
}
start += len;
},
});
new_intervals
}
pub fn target_length_split_with_interval(
length: usize,
interval: Interval,

View File

@ -3,7 +3,7 @@ use crate::helper::{TestOp::*, *};
use flowy_ot::core::{Delta, Interval, OpBuilder};
#[test]
fn delta_invert_delta_test() {
fn delta_invert_no_attribute_delta() {
let mut delta = Delta::default();
delta.add(OpBuilder::insert("123").build());
@ -19,31 +19,31 @@ fn delta_invert_delta_test() {
}
#[test]
fn delta_invert_delta_test2() {
fn delta_invert_no_attribute_delta2() {
let ops = vec![
Insert(0, "123", 0),
Insert(1, "4567", 0),
Invert(0, 1),
Undo(0, 1),
AssertOpsJson(0, r#"[{"insert":"123"}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_delta_with_attribute() {
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),
Undo(0, 1),
AssertOpsJson(0, r#"[{"insert":"123","attributes":{"bold":"true"}}]"#),
];
OpTester::new().run_script(ops);
}
#[test]
fn delta_invert_delta() {
fn delta_invert_attribute_delta_with_no_attribute_delta2() {
let ops = vec![
Insert(0, "123", 0),
Bold(0, Interval::new(0, 3), true),
@ -55,7 +55,71 @@ fn delta_invert_delta() {
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),
Undo(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"}]"#,
),
Undo(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"}}]"#,
),
Undo(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"}}]"#,
),
Undo(0, 1),
AssertOpsJson(
0,
r#"[{"insert":"12","attributes":{"bold":"true"}},{"insert":"34","attributes":{"bold":"true","italic":"true"}},{"insert":"56","attributes":{"bold":"true"}}]"#,