mirror of
https://github.com/loro-dev/loro.git
synced 2024-11-25 04:38:58 +00:00
refactor: use rich text style config (#246)
* refactor: use rich text style config * chore: rm log * feat: support config text style in wasm * feat: overlapped styles * chore: add warning style key cannot contain ':' * test: refine test case for richtext * test: refine test
This commit is contained in:
parent
692c5e3436
commit
b4701a4de6
27 changed files with 650 additions and 572 deletions
|
@ -1,6 +1,6 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use bench_utils::{json, SyncKind};
|
||||
use bench_utils::SyncKind;
|
||||
use examples::{draw::DrawActor, run_async_workflow, run_realtime_collab_workflow};
|
||||
use loro::{LoroDoc, ToJson};
|
||||
use tabled::{settings::Style, Table, Tabled};
|
||||
|
@ -25,24 +25,24 @@ struct BenchResult {
|
|||
pub fn main() {
|
||||
let seed = 123123;
|
||||
let ans = vec![
|
||||
// run_async(1, 100, seed),
|
||||
// run_async(1, 1000, seed),
|
||||
// run_async(1, 5000, seed),
|
||||
// run_async(1, 10000, seed),
|
||||
// run_async(5, 100, seed),
|
||||
// run_async(5, 1000, seed),
|
||||
// run_async(5, 10000, seed),
|
||||
// run_async(10, 1000, seed),
|
||||
// run_async(10, 10000, seed),
|
||||
// run_async(10, 100000, seed),
|
||||
// run_async(10, 100000, 1000),
|
||||
// run_realtime_collab(5, 100, seed),
|
||||
// run_realtime_collab(5, 1000, seed),
|
||||
// run_realtime_collab(5, 10000, seed),
|
||||
// run_realtime_collab(10, 1000, seed),
|
||||
run_async(1, 100, seed),
|
||||
run_async(1, 1000, seed),
|
||||
run_async(1, 5000, seed),
|
||||
run_async(1, 10000, seed),
|
||||
run_async(5, 100, seed),
|
||||
run_async(5, 1000, seed),
|
||||
run_async(5, 10000, seed),
|
||||
run_async(10, 1000, seed),
|
||||
run_async(10, 10000, seed),
|
||||
run_async(10, 100000, seed),
|
||||
run_async(10, 100000, 1000),
|
||||
run_realtime_collab(5, 100, seed),
|
||||
run_realtime_collab(5, 1000, seed),
|
||||
run_realtime_collab(5, 10000, seed),
|
||||
run_realtime_collab(10, 1000, seed),
|
||||
run_realtime_collab(10, 10000, seed),
|
||||
// run_realtime_collab(10, 100000, seed),
|
||||
// run_realtime_collab(10, 100000, 1000),
|
||||
run_realtime_collab(10, 100000, seed),
|
||||
run_realtime_collab(10, 100000, 1000),
|
||||
];
|
||||
let mut table = Table::new(ans);
|
||||
let style = Style::markdown();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use serde_columnar::ColumnarError;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{PeerID, TreeID, ID};
|
||||
use crate::{InternalString, PeerID, TreeID, ID};
|
||||
|
||||
pub type LoroResult<T> = Result<T, LoroError>;
|
||||
|
||||
|
@ -23,7 +23,7 @@ pub enum LoroError {
|
|||
LockError,
|
||||
#[error("Each AppState can only have one transaction at a time")]
|
||||
DuplicatedTransactionError,
|
||||
#[error("Cannot find ({0}) ")]
|
||||
#[error("Cannot find ({0})")]
|
||||
NotFoundError(Box<str>),
|
||||
// TODO: more details transaction error
|
||||
#[error("Transaction error ({0})")]
|
||||
|
@ -38,6 +38,8 @@ pub enum LoroError {
|
|||
ArgErr(Box<str>),
|
||||
#[error("Auto commit has not started. The doc is readonly when detached. You should ensure autocommit is on and the doc and the state is attached.")]
|
||||
AutoCommitNotStarted,
|
||||
#[error("You need to specify the style flag for \"({0:?})\" before mark with this key")]
|
||||
StyleConfigMissing(InternalString),
|
||||
#[error("Unknown Error ({0})")]
|
||||
Unknown(Box<str>),
|
||||
}
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use crate::Timestamp;
|
||||
pub use crate::container::richtext::config::{StyleConfig, StyleConfigMap};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Configure {
|
||||
pub get_time: fn() -> Timestamp,
|
||||
pub rand: Arc<dyn SecureRandomGenerator>,
|
||||
pub text_style_config: Arc<RwLock<StyleConfigMap>>,
|
||||
}
|
||||
|
||||
impl Debug for Configure {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Configure")
|
||||
.field("get_time", &self.get_time)
|
||||
.finish()
|
||||
impl Default for Configure {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text_style_config: Arc::new(RwLock::new(StyleConfigMap::default_rich_text_config())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +17,7 @@ pub struct DefaultRandom;
|
|||
|
||||
#[cfg(test)]
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::{Arc, RwLock};
|
||||
#[cfg(test)]
|
||||
static mut TEST_RANDOM: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
|
@ -63,12 +61,3 @@ pub trait SecureRandomGenerator: Send + Sync {
|
|||
i32::from_le_bytes(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Configure {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
get_time: || 0,
|
||||
rand: Arc::new(DefaultRandom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
//!
|
||||
//! The users of this type can only operate on unicode index or utf16 index, but calculated entity index will be provided.
|
||||
|
||||
pub(crate) mod config;
|
||||
mod fugue_span;
|
||||
mod query_by_len;
|
||||
pub(crate) mod richtext_state;
|
||||
|
@ -39,10 +40,6 @@ pub struct RichtextSpan {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Style {
|
||||
pub key: InternalString,
|
||||
/// The value of the style.
|
||||
///
|
||||
/// - If the style is a container, this is the Container
|
||||
/// - Otherwise, this is true
|
||||
pub data: LoroValue,
|
||||
}
|
||||
|
||||
|
@ -60,52 +57,27 @@ pub struct StyleOp {
|
|||
#[derive(Debug, Hash, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub(crate) enum StyleKey {
|
||||
Key(InternalString),
|
||||
KeyWithId { key: InternalString, id: ID },
|
||||
}
|
||||
|
||||
impl StyleKey {
|
||||
pub fn to_attr_key(&self) -> String {
|
||||
match self {
|
||||
Self::Key(key) => key.to_string(),
|
||||
Self::KeyWithId { key: _, id } => format!("id:{}", id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(&self) -> &InternalString {
|
||||
match self {
|
||||
Self::Key(key) => key,
|
||||
Self::KeyWithId { key, .. } => key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_id(&self) -> bool {
|
||||
matches!(self, Self::KeyWithId { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleOp {
|
||||
pub fn to_style(&self) -> Style {
|
||||
if self.info.is_delete() {
|
||||
return Style {
|
||||
key: self.key.clone(),
|
||||
data: LoroValue::Bool(false),
|
||||
};
|
||||
}
|
||||
|
||||
if self.info.is_container() {
|
||||
Style {
|
||||
key: self.key.clone(),
|
||||
data: LoroValue::Container(loro_common::ContainerID::Normal {
|
||||
peer: self.peer,
|
||||
counter: self.cnt,
|
||||
container_type: loro_common::ContainerType::Map,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Style {
|
||||
key: self.key.clone(),
|
||||
data: LoroValue::Bool(true),
|
||||
}
|
||||
Style {
|
||||
key: self.key.clone(),
|
||||
data: self.value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,14 +86,7 @@ impl StyleOp {
|
|||
}
|
||||
|
||||
pub(crate) fn get_style_key(&self) -> StyleKey {
|
||||
if !self.info.mergeable() {
|
||||
StyleKey::KeyWithId {
|
||||
key: self.key.clone(),
|
||||
id: self.id(),
|
||||
}
|
||||
} else {
|
||||
StyleKey::Key(self.key.clone())
|
||||
}
|
||||
StyleKey::Key(self.key.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -160,11 +125,11 @@ impl Ord for StyleOp {
|
|||
///
|
||||
/// Note: we assume style with the same key has the same `Mergeable` and `isContainer` value.
|
||||
///
|
||||
/// - Mergeable (1st bit): whether two styles with the same key can be merged into one.
|
||||
/// - 0 (1st bit)
|
||||
/// - Expand Before (2nd bit): when inserting new text before this style, whether the new text should inherit this style.
|
||||
/// - Expand After (3rd bit): when inserting new text after this style, whether the new text should inherit this style.
|
||||
/// - Delete (4th bit): whether this is used to remove a style from a range.
|
||||
/// - isContainer (5th bit): whether the style also store other data in a associated map container with the same OpID.
|
||||
/// - 0 (5th bit): whether the style also store other data in a associated map container with the same OpID.
|
||||
/// - 0 (6th bit)
|
||||
/// - 0 (7th bit)
|
||||
/// - isAlive (8th bit): always 1 unless the style is garbage collected. If this is 0, all other bits should be 0 as well.
|
||||
|
@ -178,20 +143,15 @@ impl Debug for TextStyleInfoFlag {
|
|||
f.debug_struct("TextStyleInfo")
|
||||
// write data in binary format
|
||||
.field("data", &format!("{:#010b}", self.data))
|
||||
.field("mergeable", &self.mergeable())
|
||||
.field("expand_before", &self.expand_before())
|
||||
.field("expand_after", &self.expand_after())
|
||||
.field("is_delete", &self.is_delete())
|
||||
.field("is_container", &self.is_container())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
const MERGEABLE_MASK: u8 = 0b0000_0001;
|
||||
const EXPAND_BEFORE_MASK: u8 = 0b0000_0010;
|
||||
const EXPAND_AFTER_MASK: u8 = 0b0000_0100;
|
||||
const DELETE_MASK: u8 = 0b0000_1000;
|
||||
const CONTAINER_MASK: u8 = 0b0001_0000;
|
||||
const ALIVE_MASK: u8 = 0b1000_0000;
|
||||
|
||||
/// Whether to expand the style when inserting new text around it.
|
||||
|
@ -240,7 +200,7 @@ impl ExpandType {
|
|||
|
||||
/// Create reversed expand type.
|
||||
///
|
||||
/// Beofre -> After
|
||||
/// Before -> After
|
||||
/// After -> Before
|
||||
/// Both -> None
|
||||
/// None -> Both
|
||||
|
@ -258,13 +218,6 @@ impl ExpandType {
|
|||
}
|
||||
|
||||
impl TextStyleInfoFlag {
|
||||
/// Whether two styles with the same key can be merged into one.
|
||||
/// If false, the styles will coexist in the same range.
|
||||
#[inline(always)]
|
||||
pub fn mergeable(self) -> bool {
|
||||
self.data & MERGEABLE_MASK != 0
|
||||
}
|
||||
|
||||
/// When inserting new text around this style, prefer inserting after it.
|
||||
#[inline(always)]
|
||||
pub fn expand_before(self) -> bool {
|
||||
|
@ -292,38 +245,14 @@ impl TextStyleInfoFlag {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_delete(&self) -> bool {
|
||||
self.data & DELETE_MASK != 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_container(&self) -> bool {
|
||||
self.data & CONTAINER_MASK != 0
|
||||
}
|
||||
|
||||
pub const fn new(
|
||||
mergeable: bool,
|
||||
expand_type: ExpandType,
|
||||
is_delete: bool,
|
||||
is_container: bool,
|
||||
) -> Self {
|
||||
pub const fn new(expand_type: ExpandType) -> Self {
|
||||
let mut data = ALIVE_MASK;
|
||||
if mergeable {
|
||||
data |= MERGEABLE_MASK;
|
||||
}
|
||||
if expand_type.expand_before() {
|
||||
data |= EXPAND_BEFORE_MASK;
|
||||
}
|
||||
if expand_type.expand_after() {
|
||||
data |= EXPAND_AFTER_MASK;
|
||||
}
|
||||
if is_delete {
|
||||
data |= DELETE_MASK;
|
||||
}
|
||||
if is_container {
|
||||
data |= CONTAINER_MASK;
|
||||
}
|
||||
|
||||
TextStyleInfoFlag { data }
|
||||
}
|
||||
|
@ -347,12 +276,9 @@ impl TextStyleInfoFlag {
|
|||
Self { data }
|
||||
}
|
||||
|
||||
pub const BOLD: TextStyleInfoFlag =
|
||||
TextStyleInfoFlag::new(true, ExpandType::After, false, false);
|
||||
pub const LINK: TextStyleInfoFlag =
|
||||
TextStyleInfoFlag::new(true, ExpandType::None, false, false);
|
||||
pub const COMMENT: TextStyleInfoFlag =
|
||||
TextStyleInfoFlag::new(false, ExpandType::None, false, true);
|
||||
pub const BOLD: TextStyleInfoFlag = TextStyleInfoFlag::new(ExpandType::After);
|
||||
pub const LINK: TextStyleInfoFlag = TextStyleInfoFlag::new(ExpandType::None);
|
||||
pub const COMMENT: TextStyleInfoFlag = TextStyleInfoFlag::new(ExpandType::None);
|
||||
|
||||
pub const fn to_byte(&self) -> u8 {
|
||||
self.data
|
||||
|
|
123
crates/loro-internal/src/container/richtext/config.rs
Normal file
123
crates/loro-internal/src/container/richtext/config.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
use fxhash::FxHashMap;
|
||||
use loro_common::InternalString;
|
||||
|
||||
use super::{ExpandType, TextStyleInfoFlag};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StyleConfigMap {
|
||||
map: FxHashMap<InternalString, StyleConfig>,
|
||||
}
|
||||
|
||||
impl StyleConfigMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
map: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: InternalString, value: StyleConfig) {
|
||||
if key.contains(':') {
|
||||
panic!("style key should not contain ':'");
|
||||
}
|
||||
|
||||
self.map.insert(key, value);
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &InternalString) -> Option<&StyleConfig> {
|
||||
self.map.get(key)
|
||||
}
|
||||
|
||||
pub fn get_style_flag(&self, key: &InternalString) -> Option<TextStyleInfoFlag> {
|
||||
self._get_style_flag(key, false)
|
||||
}
|
||||
|
||||
pub fn get_style_flag_for_unmark(&self, key: &InternalString) -> Option<TextStyleInfoFlag> {
|
||||
self._get_style_flag(key, true)
|
||||
}
|
||||
|
||||
fn _get_style_flag(&self, key: &InternalString, is_del: bool) -> Option<TextStyleInfoFlag> {
|
||||
let f = |x: &StyleConfig| {
|
||||
TextStyleInfoFlag::new(if is_del { x.expand.reverse() } else { x.expand })
|
||||
};
|
||||
if let Some(index) = key.find(':') {
|
||||
let key = key[..index].into();
|
||||
self.map.get(&key).map(f)
|
||||
} else {
|
||||
self.map.get(key).map(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_rich_text_config() -> Self {
|
||||
let mut map = Self {
|
||||
map: FxHashMap::default(),
|
||||
};
|
||||
|
||||
map.map.insert(
|
||||
"bold".into(),
|
||||
StyleConfig {
|
||||
expand: ExpandType::After,
|
||||
},
|
||||
);
|
||||
|
||||
map.map.insert(
|
||||
"italic".into(),
|
||||
StyleConfig {
|
||||
expand: ExpandType::After,
|
||||
},
|
||||
);
|
||||
|
||||
map.map.insert(
|
||||
"underline".into(),
|
||||
StyleConfig {
|
||||
expand: ExpandType::After,
|
||||
},
|
||||
);
|
||||
|
||||
map.map.insert(
|
||||
"link".into(),
|
||||
StyleConfig {
|
||||
expand: ExpandType::None,
|
||||
},
|
||||
);
|
||||
|
||||
map.map.insert(
|
||||
"highlight".into(),
|
||||
StyleConfig {
|
||||
expand: ExpandType::None,
|
||||
},
|
||||
);
|
||||
|
||||
map.map.insert(
|
||||
"comment".into(),
|
||||
StyleConfig {
|
||||
expand: ExpandType::None,
|
||||
},
|
||||
);
|
||||
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct StyleConfig {
|
||||
pub expand: ExpandType,
|
||||
}
|
||||
|
||||
impl StyleConfig {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
expand: ExpandType::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(mut self, expand: ExpandType) -> Self {
|
||||
self.expand = expand;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StyleConfig {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
|
@ -1368,7 +1368,44 @@ impl RichtextState {
|
|||
/// This method only updates `style_ranges`.
|
||||
/// When this method is called, the style start anchor and the style end anchor should already have been inserted.
|
||||
pub(crate) fn annotate_style_range(&mut self, range: Range<usize>, style: Arc<StyleOp>) {
|
||||
self.ensure_style_ranges_mut().annotate(range, style)
|
||||
self.ensure_style_ranges_mut().annotate(range, style, None)
|
||||
}
|
||||
|
||||
/// This method only updates `style_ranges`.
|
||||
/// When this method is called, the style start anchor and the style end anchor should already have been inserted.
|
||||
///
|
||||
/// This method will return the event of this annotation in event length
|
||||
pub(crate) fn annotate_style_range_with_event(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
style: Arc<StyleOp>,
|
||||
) -> impl Iterator<Item = (StyleMeta, usize)> + '_ {
|
||||
let mut ranges_in_entity_index: Vec<(StyleMeta, Range<usize>)> = Vec::new();
|
||||
let mut start = range.start;
|
||||
let end = range.end;
|
||||
self.ensure_style_ranges_mut().annotate(
|
||||
range,
|
||||
style,
|
||||
Some(&mut |s, len| {
|
||||
let range = start..start + len;
|
||||
start += len;
|
||||
ranges_in_entity_index.push((s.into(), range));
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(ranges_in_entity_index.last().unwrap().1.end, end);
|
||||
let mut converter = ContinuousIndexConverter::new(self);
|
||||
ranges_in_entity_index
|
||||
.into_iter()
|
||||
.filter_map(move |(meta, range)| {
|
||||
let start = converter.convert_entity_index_to_event_index(range.start);
|
||||
let end = converter.convert_entity_index_to_event_index(range.end);
|
||||
if end == start {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((meta, end - start))
|
||||
})
|
||||
}
|
||||
|
||||
/// init style ranges if not initialized
|
||||
|
@ -1384,11 +1421,11 @@ impl RichtextState {
|
|||
/// The result is only different from `query` when there are style anchors around the insert pos.
|
||||
/// Returns the right neighbor of the insert pos and the entity index.
|
||||
///
|
||||
/// 1. Insertions occur before tombstones that contain the beginning of new marks.
|
||||
/// 2. Insertions occur before tombstones that contain the end of bold-like marks
|
||||
/// 3. Insertions occur after tombstones that contain the end of link-like marks
|
||||
/// 1. Insertions occur before style anchors that contain the beginning of new marks.
|
||||
/// 2. Insertions occur before style anchors that contain the end of bold-like marks
|
||||
/// 3. Insertions occur after style anchors that contain the end of link-like marks
|
||||
///
|
||||
/// Rule 1 should be satisfied before rules 2 and 3 to avoid this problem.
|
||||
/// Rule 1 should be satisfied before rules 2 and 3 to avoid creating a new style out of nowhere
|
||||
///
|
||||
/// The current method will scan forward to find the last position that satisfies 1 and 2.
|
||||
/// Then it scans backward to find the first position that satisfies 3.
|
||||
|
@ -1739,7 +1776,7 @@ impl RichtextState {
|
|||
// 1. We inserted a start anchor before end_entity_index, so we need to +1
|
||||
// 2. We need to include the end anchor in the range, so we need to +1
|
||||
self.ensure_style_ranges_mut()
|
||||
.annotate(range.start..range.end + 2, style);
|
||||
.annotate(range.start..range.end + 2, style, None);
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = RichtextSpan> + '_ {
|
||||
|
@ -1966,6 +2003,69 @@ impl RichtextState {
|
|||
}
|
||||
}
|
||||
|
||||
use converter::ContinuousIndexConverter;
|
||||
mod converter {
|
||||
use generic_btree::{rle::HasLength, Cursor};
|
||||
|
||||
use super::{query::EntityQuery, RichtextState};
|
||||
|
||||
/// Convert entity index into event index.
|
||||
/// It assumes the entity_index are in ascending order
|
||||
pub(super) struct ContinuousIndexConverter<'a> {
|
||||
state: &'a RichtextState,
|
||||
last_entity_index_cache: Option<ConverterCache>,
|
||||
}
|
||||
|
||||
struct ConverterCache {
|
||||
entity_index: usize,
|
||||
cursor: Cursor,
|
||||
event_index: usize,
|
||||
cursor_elem_len: usize,
|
||||
}
|
||||
|
||||
impl<'a> ContinuousIndexConverter<'a> {
|
||||
pub fn new(state: &'a RichtextState) -> Self {
|
||||
Self {
|
||||
state,
|
||||
last_entity_index_cache: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_entity_index_to_event_index(&mut self, entity_index: usize) -> usize {
|
||||
if let Some(last) = self.last_entity_index_cache.as_ref() {
|
||||
if last.entity_index == entity_index {
|
||||
return last.event_index;
|
||||
}
|
||||
|
||||
assert!(entity_index > last.entity_index);
|
||||
if last.cursor.offset + entity_index - last.entity_index < last.cursor_elem_len {
|
||||
// in the same cursor
|
||||
return self.state.cursor_to_event_index(Cursor {
|
||||
leaf: last.cursor.leaf,
|
||||
offset: last.cursor.offset + entity_index - last.entity_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = self
|
||||
.state
|
||||
.tree
|
||||
.query::<EntityQuery>(&entity_index)
|
||||
.unwrap()
|
||||
.cursor;
|
||||
let ans = self.state.cursor_to_event_index(cursor);
|
||||
let len = self.state.tree.get_elem(cursor.leaf).unwrap().rle_len();
|
||||
self.last_entity_index_cache = Some(ConverterCache {
|
||||
entity_index,
|
||||
cursor,
|
||||
event_index: ans,
|
||||
cursor_elem_len: len,
|
||||
});
|
||||
ans
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use append_only_bytes::AppendOnlyBytes;
|
||||
|
@ -2029,7 +2129,7 @@ mod test {
|
|||
fn comment(n: isize) -> Arc<StyleOp> {
|
||||
Arc::new(StyleOp::new_for_test(
|
||||
n,
|
||||
"comment",
|
||||
&format!("comment:{}", n),
|
||||
"comment".into(),
|
||||
TextStyleInfoFlag::COMMENT,
|
||||
))
|
||||
|
@ -2355,33 +2455,21 @@ mod test {
|
|||
{
|
||||
"insert": "H",
|
||||
"attributes": {
|
||||
"id:0@0": {
|
||||
"key": "comment",
|
||||
"data": "comment"
|
||||
},
|
||||
"comment:0": "comment",
|
||||
},
|
||||
},
|
||||
{
|
||||
"insert": "ello",
|
||||
"attributes": {
|
||||
"id:0@0": {
|
||||
"key": "comment",
|
||||
"data": "comment"
|
||||
},
|
||||
"id:1@1": {
|
||||
"key": "comment",
|
||||
"data": "comment"
|
||||
}
|
||||
"comment:0": "comment",
|
||||
"comment:1": "comment",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
"insert": " ",
|
||||
"attributes": {
|
||||
"id:1@1": {
|
||||
"key": "comment",
|
||||
"data": "comment"
|
||||
}
|
||||
"comment:1": "comment",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -133,7 +133,12 @@ impl StyleRangeMap {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn annotate(&mut self, range: Range<usize>, style: Arc<StyleOp>) {
|
||||
pub fn annotate(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
style: Arc<StyleOp>,
|
||||
mut yield_style: Option<&mut dyn FnMut(&Styles, usize)>,
|
||||
) {
|
||||
let range = self.tree.range::<LengthFinder>(range);
|
||||
if range.is_none() {
|
||||
unreachable!();
|
||||
|
@ -152,6 +157,9 @@ impl StyleRangeMap {
|
|||
x.styles.insert(key, value);
|
||||
}
|
||||
|
||||
if let Some(y) = yield_style.as_mut() {
|
||||
y(&x.styles, x.len);
|
||||
}
|
||||
None
|
||||
});
|
||||
}
|
||||
|
@ -255,7 +263,7 @@ impl StyleRangeMap {
|
|||
let right = self.tree.shift_path_by_one_offset(left).unwrap();
|
||||
if left.leaf == right.leaf {
|
||||
let styles = &self.tree.get_elem(left.leaf).unwrap().styles;
|
||||
styles.clone().into()
|
||||
styles.into()
|
||||
} else {
|
||||
let mut styles = self.tree.get_elem(left.leaf).unwrap().styles.clone();
|
||||
let right_styles = &self.tree.get_elem(right.leaf).unwrap().styles;
|
||||
|
@ -502,7 +510,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_basic_insert() {
|
||||
let mut map = StyleRangeMap::default();
|
||||
map.annotate(1..10, new_style(1));
|
||||
map.annotate(1..10, new_style(1), None);
|
||||
{
|
||||
map.insert(0, 1);
|
||||
assert_eq!(map.iter().count(), 1);
|
||||
|
@ -532,7 +540,7 @@ mod test {
|
|||
#[test]
|
||||
fn delete_style() {
|
||||
let mut map = StyleRangeMap::default();
|
||||
map.annotate(1..10, new_style(1));
|
||||
map.annotate(1..10, new_style(1), None);
|
||||
{
|
||||
map.delete(0..2);
|
||||
assert_eq!(map.iter().count(), 1);
|
||||
|
|
|
@ -615,12 +615,15 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
|
|||
}
|
||||
|
||||
pub fn chop(mut self) -> Self {
|
||||
let last_op = self.vec.last();
|
||||
if let Some(last_op) = last_op {
|
||||
if last_op.is_retain() && last_op.meta().is_empty() {
|
||||
self.vec.pop();
|
||||
loop {
|
||||
match self.vec.last() {
|
||||
Some(last_op) if last_op.is_retain() && last_op.meta().is_empty() => {
|
||||
self.vec.pop();
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@ impl StyleMetaItem {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Styles> for StyleMeta {
|
||||
fn from(styles: Styles) -> Self {
|
||||
impl From<&Styles> for StyleMeta {
|
||||
fn from(styles: &Styles) -> Self {
|
||||
let mut map = FxHashMap::with_capacity_and_hasher(styles.len(), Default::default());
|
||||
for (key, value) in styles.iter() {
|
||||
if let Some(value) = value.get() {
|
||||
|
@ -52,6 +52,13 @@ impl From<Styles> for StyleMeta {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Styles> for StyleMeta {
|
||||
fn from(styles: Styles) -> Self {
|
||||
let temp = &styles;
|
||||
temp.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Meta for StyleMeta {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
|
@ -103,17 +110,7 @@ impl StyleMeta {
|
|||
return None;
|
||||
}
|
||||
|
||||
Some((
|
||||
key.to_attr_key(),
|
||||
if key.contains_id() {
|
||||
let mut map: FxHashMap<String, LoroValue> = Default::default();
|
||||
map.insert("key".into(), key.key().to_string().into());
|
||||
map.insert("data".into(), value.value.clone());
|
||||
LoroValue::Map(Arc::new(map))
|
||||
} else {
|
||||
value.value.clone()
|
||||
},
|
||||
))
|
||||
Some((key.to_attr_key(), value.value.clone()))
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
|
@ -124,15 +121,7 @@ impl ToJson for StyleMeta {
|
|||
fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut map = serde_json::Map::new();
|
||||
for (key, style) in self.iter() {
|
||||
let value = if !key.contains_id() {
|
||||
serde_json::to_value(&style.data).unwrap()
|
||||
} else {
|
||||
let mut value = serde_json::Map::new();
|
||||
value.insert("key".to_string(), style.key.to_string().into());
|
||||
let data = serde_json::to_value(&style.data).unwrap();
|
||||
value.insert("data".to_string(), data);
|
||||
value.into()
|
||||
};
|
||||
let value = serde_json::to_value(&style.data).unwrap();
|
||||
map.insert(key.to_attr_key(), value);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ pub mod tree;
|
|||
|
||||
use crate::{
|
||||
array_mut_ref,
|
||||
container::richtext::TextStyleInfoFlag,
|
||||
delta::{Delta, DeltaItem, StyleMeta},
|
||||
event::Diff,
|
||||
loro::LoroDoc,
|
||||
|
@ -13,7 +12,7 @@ use crate::{
|
|||
};
|
||||
use debug_log::debug_log;
|
||||
use enum_as_inner::EnumAsInner;
|
||||
use loro_common::{ContainerID, LoroValue};
|
||||
use loro_common::ContainerID;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
sync::{Arc, Mutex},
|
||||
|
@ -21,17 +20,7 @@ use std::{
|
|||
};
|
||||
use tabled::{TableIteratorExt, Tabled};
|
||||
|
||||
const STYLES: [TextStyleInfoFlag; 8] = [
|
||||
TextStyleInfoFlag::BOLD,
|
||||
TextStyleInfoFlag::COMMENT,
|
||||
TextStyleInfoFlag::LINK,
|
||||
TextStyleInfoFlag::LINK.to_delete(),
|
||||
TextStyleInfoFlag::BOLD.to_delete(),
|
||||
TextStyleInfoFlag::COMMENT.to_delete(),
|
||||
TextStyleInfoFlag::from_byte(0),
|
||||
TextStyleInfoFlag::from_byte(0).to_delete(),
|
||||
];
|
||||
|
||||
const STYLES_NAME: [&str; 4] = ["bold", "comment", "link", "highlight"];
|
||||
#[derive(arbitrary::Arbitrary, EnumAsInner, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum Action {
|
||||
Ins {
|
||||
|
@ -256,18 +245,13 @@ impl Actionable for Vec<LoroDoc> {
|
|||
let site = &mut self[*site as usize];
|
||||
let mut txn = site.txn().unwrap();
|
||||
let text = txn.get_text("text");
|
||||
let style = STYLES[*style_key as usize];
|
||||
text.mark_with_txn(
|
||||
&mut txn,
|
||||
*pos,
|
||||
*pos + *len,
|
||||
&style_key.to_string(),
|
||||
if style.is_delete() {
|
||||
LoroValue::Null
|
||||
} else {
|
||||
true.into()
|
||||
},
|
||||
style,
|
||||
STYLES_NAME[*style_key as usize % STYLES_NAME.len()],
|
||||
(*pos as i32).into(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -12,36 +12,11 @@ use crate::{
|
|||
array_mut_ref, container::ContainerID, delta::DeltaItem, id::PeerID, ContainerType, LoroValue,
|
||||
};
|
||||
use crate::{
|
||||
container::richtext::{StyleKey, TextStyleInfoFlag},
|
||||
event::Diff,
|
||||
handler::TextDelta,
|
||||
loro::LoroDoc,
|
||||
value::ToJson,
|
||||
version::Frontiers,
|
||||
TextHandler,
|
||||
container::richtext::StyleKey, event::Diff, handler::TextDelta, loro::LoroDoc, value::ToJson,
|
||||
version::Frontiers, TextHandler,
|
||||
};
|
||||
|
||||
const STYLES: [TextStyleInfoFlag; 8] = [
|
||||
TextStyleInfoFlag::BOLD,
|
||||
TextStyleInfoFlag::COMMENT,
|
||||
TextStyleInfoFlag::LINK,
|
||||
TextStyleInfoFlag::from_byte(0),
|
||||
TextStyleInfoFlag::LINK.to_delete(),
|
||||
TextStyleInfoFlag::BOLD.to_delete(),
|
||||
TextStyleInfoFlag::COMMENT.to_delete(),
|
||||
TextStyleInfoFlag::from_byte(0).to_delete(),
|
||||
];
|
||||
|
||||
const STYLES_NAME: [&str; 8] = [
|
||||
"BOLD",
|
||||
"COMMENT",
|
||||
"LINK",
|
||||
"0",
|
||||
"DEL_LINK",
|
||||
"DEL_BOLD",
|
||||
"DEL_COMMENT",
|
||||
"DEL_0",
|
||||
];
|
||||
const STYLES_NAME: [&str; 4] = ["bold", "comment", "link", "highlight"];
|
||||
|
||||
#[derive(Arbitrary, EnumAsInner, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum Action {
|
||||
|
@ -123,15 +98,6 @@ impl Actor {
|
|||
.filter(|(_, v)| !v.data.is_null())
|
||||
.map(|(k, v)| match k {
|
||||
StyleKey::Key(k) => (k.to_string(), v.data),
|
||||
StyleKey::KeyWithId { key, id } => {
|
||||
let mut data = FxHashMap::default();
|
||||
data.insert(
|
||||
"key".to_string(),
|
||||
LoroValue::String(Arc::new(key.to_string())),
|
||||
);
|
||||
data.insert("data".to_string(), v.data);
|
||||
(format!("id:{}", id), LoroValue::Map(Arc::new(data)))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let attributes = if attributes.is_empty() {
|
||||
|
@ -154,15 +120,6 @@ impl Actor {
|
|||
.filter(|(_, v)| !v.data.is_null())
|
||||
.map(|(k, v)| match k {
|
||||
StyleKey::Key(k) => (k.to_string(), v.data),
|
||||
StyleKey::KeyWithId { key, id } => {
|
||||
let mut data = FxHashMap::default();
|
||||
data.insert(
|
||||
"key".to_string(),
|
||||
LoroValue::String(Arc::new(key.to_string())),
|
||||
);
|
||||
data.insert("data".to_string(), v.data);
|
||||
(format!("id:{}", id), LoroValue::Map(Arc::new(data)))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let attributes = if attributes.is_empty() {
|
||||
|
@ -182,7 +139,7 @@ impl Actor {
|
|||
// text_doc.peer_id(),
|
||||
// text_h.get_richtext_value()
|
||||
// );
|
||||
// println!("delta {:?}", text_deltas);
|
||||
debug_log::debug_log!("delta {:?}", text_deltas);
|
||||
text_h.apply_delta_with_txn(&mut txn, &text_deltas).unwrap();
|
||||
|
||||
// println!("after {:?}\n", text_h.get_richtext_value());
|
||||
|
@ -329,7 +286,7 @@ impl Actionable for Vec<Actor> {
|
|||
*pos %= length;
|
||||
*len %= length - *pos;
|
||||
*len = 1.max(*len);
|
||||
*i %= STYLES.len();
|
||||
*i %= STYLES_NAME.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -395,18 +352,13 @@ impl Actionable for Vec<Actor> {
|
|||
text.delete_with_txn(&mut txn, *pos, *len).unwrap();
|
||||
}
|
||||
RichTextAction::Mark(i) => {
|
||||
let style = STYLES[*i];
|
||||
text.mark_with_txn(
|
||||
&mut txn,
|
||||
*pos,
|
||||
*pos + *len,
|
||||
STYLES_NAME[*i],
|
||||
if style.is_delete() {
|
||||
LoroValue::Null
|
||||
} else {
|
||||
true.into()
|
||||
},
|
||||
style,
|
||||
(*pos as i32).into(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -469,6 +421,7 @@ fn check_eq(a_actor: &mut Actor, b_actor: &mut Actor) {
|
|||
|
||||
debug_log::debug_log!("{}", a_result.to_json_pretty());
|
||||
assert_eq!(&a_result, &b_result);
|
||||
debug_log::debug_log!("{}", a_value.to_json_pretty());
|
||||
assert_value_eq(&a_result, &a_value);
|
||||
}
|
||||
|
||||
|
@ -1034,4 +987,25 @@ mod failed_tests {
|
|||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_overlap() {
|
||||
test_multi_sites(
|
||||
5,
|
||||
&mut [
|
||||
RichText {
|
||||
site: 255,
|
||||
pos: 562949940576255,
|
||||
value: 10,
|
||||
action: RichTextAction::Insert,
|
||||
},
|
||||
RichText {
|
||||
site: 0,
|
||||
pos: 52793738066393,
|
||||
value: 15637060856183783423,
|
||||
action: RichTextAction::Mark(15697817505862638041),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
container::{
|
||||
idx::ContainerIdx,
|
||||
list::list_op::{DeleteSpan, ListOp},
|
||||
richtext::{richtext_state::PosType, TextStyleInfoFlag},
|
||||
richtext::richtext_state::PosType,
|
||||
tree::tree_op::TreeOp,
|
||||
},
|
||||
delta::{TreeDiffItem, TreeExternalDiff},
|
||||
|
@ -16,12 +16,14 @@ use crate::{
|
|||
use enum_as_inner::EnumAsInner;
|
||||
use fxhash::FxHashMap;
|
||||
use loro_common::{
|
||||
ContainerID, ContainerType, LoroError, LoroResult, LoroTreeError, LoroValue, TreeID,
|
||||
ContainerID, ContainerType, InternalString, LoroError, LoroResult, LoroTreeError, LoroValue,
|
||||
TreeID,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::smallvec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ops::Deref,
|
||||
sync::{Mutex, Weak},
|
||||
};
|
||||
|
||||
|
@ -41,6 +43,7 @@ pub enum TextDelta {
|
|||
},
|
||||
}
|
||||
|
||||
/// Flatten attributes that allow overlap
|
||||
#[derive(Clone)]
|
||||
pub struct TextHandler {
|
||||
txn: Weak<Mutex<Option<Transaction>>>,
|
||||
|
@ -315,7 +318,7 @@ impl TextHandler {
|
|||
pos: usize,
|
||||
s: &str,
|
||||
attr: Option<&FxHashMap<String, LoroValue>>,
|
||||
) -> Result<Vec<(String, LoroValue)>, LoroError> {
|
||||
) -> Result<Vec<(InternalString, LoroValue)>, LoroError> {
|
||||
if s.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
@ -345,10 +348,11 @@ impl TextHandler {
|
|||
// current styles
|
||||
let map: FxHashMap<_, _> = styles
|
||||
.iter()
|
||||
.map(|x| (x.0.to_attr_key(), x.1.data))
|
||||
.map(|x| (x.0.key().clone(), x.1.data))
|
||||
.collect();
|
||||
debug_log::debug_dbg!(&map);
|
||||
for (key, style) in map.iter() {
|
||||
match attr.get(key) {
|
||||
match attr.get(key.deref()) {
|
||||
Some(v) if v == style => {}
|
||||
new_style_value => {
|
||||
// need to override
|
||||
|
@ -359,8 +363,9 @@ impl TextHandler {
|
|||
}
|
||||
|
||||
for (key, style) in attr.iter() {
|
||||
if !map.contains_key(key) {
|
||||
override_styles.push((key.clone(), style.clone()));
|
||||
let key = key.as_str().into();
|
||||
if !map.contains_key(&key) {
|
||||
override_styles.push((key, style.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -466,12 +471,28 @@ impl TextHandler {
|
|||
&self,
|
||||
start: usize,
|
||||
end: usize,
|
||||
key: &str,
|
||||
key: impl Into<InternalString>,
|
||||
value: LoroValue,
|
||||
flag: TextStyleInfoFlag,
|
||||
) -> LoroResult<()> {
|
||||
with_txn(&self.txn, |txn| {
|
||||
self.mark_with_txn(txn, start, end, key, value, flag)
|
||||
self.mark_with_txn(txn, start, end, key, value, false)
|
||||
})
|
||||
}
|
||||
|
||||
/// `start` and `end` are [Event Index]s:
|
||||
///
|
||||
/// - if feature="wasm", pos is a UTF-16 index
|
||||
/// - if feature!="wasm", pos is a Unicode index
|
||||
///
|
||||
/// This method requires auto_commit to be enabled.
|
||||
pub fn unmark(
|
||||
&self,
|
||||
start: usize,
|
||||
end: usize,
|
||||
key: impl Into<InternalString>,
|
||||
) -> LoroResult<()> {
|
||||
with_txn(&self.txn, |txn| {
|
||||
self.mark_with_txn(txn, start, end, key, LoroValue::Null, true)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -484,9 +505,9 @@ impl TextHandler {
|
|||
txn: &mut Transaction,
|
||||
start: usize,
|
||||
end: usize,
|
||||
key: &str,
|
||||
key: impl Into<InternalString>,
|
||||
value: LoroValue,
|
||||
flag: TextStyleInfoFlag,
|
||||
is_delete: bool,
|
||||
) -> LoroResult<()> {
|
||||
if start >= end {
|
||||
return Err(loro_common::LoroError::ArgErr(
|
||||
|
@ -499,27 +520,24 @@ impl TextHandler {
|
|||
return Err(LoroError::OutOfBound { pos: end, len });
|
||||
}
|
||||
|
||||
let (entity_range, skip) = self
|
||||
.state
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.with_state_mut(self.container_idx, |state| {
|
||||
let (entity_range, styles) = state
|
||||
.as_richtext_state_mut()
|
||||
.unwrap()
|
||||
.get_entity_range_and_styles_at_range(start..end, PosType::Event);
|
||||
let key: InternalString = key.into();
|
||||
let mutex = &self.state.upgrade().unwrap();
|
||||
let mut doc_state = mutex.lock().unwrap();
|
||||
let (entity_range, skip) = doc_state.with_state_mut(self.container_idx, |state| {
|
||||
let (entity_range, styles) = state
|
||||
.as_richtext_state_mut()
|
||||
.unwrap()
|
||||
.get_entity_range_and_styles_at_range(start..end, PosType::Event);
|
||||
|
||||
let skip = match styles {
|
||||
Some(styles) if styles.has_key_value(key, &value) => {
|
||||
// already has the same style, skip
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
(entity_range, skip)
|
||||
});
|
||||
let skip = match styles {
|
||||
Some(styles) if styles.has_key_value(&key, &value) => {
|
||||
// already has the same style, skip
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
(entity_range, skip)
|
||||
});
|
||||
|
||||
if skip {
|
||||
return Ok(());
|
||||
|
@ -527,24 +545,32 @@ impl TextHandler {
|
|||
|
||||
let entity_start = entity_range.start;
|
||||
let entity_end = entity_range.end;
|
||||
let style_config = doc_state.config.text_style_config.try_read().unwrap();
|
||||
let flag = if is_delete {
|
||||
style_config
|
||||
.get_style_flag_for_unmark(&key)
|
||||
.ok_or_else(|| LoroError::StyleConfigMissing(key.clone()))?
|
||||
} else {
|
||||
style_config
|
||||
.get_style_flag(&key)
|
||||
.ok_or_else(|| LoroError::StyleConfigMissing(key.clone()))?
|
||||
};
|
||||
|
||||
drop(style_config);
|
||||
drop(doc_state);
|
||||
txn.apply_local_op(
|
||||
self.container_idx,
|
||||
crate::op::RawOpContent::List(ListOp::StyleStart {
|
||||
start: entity_start as u32,
|
||||
end: entity_end as u32,
|
||||
key: key.into(),
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
info: flag,
|
||||
}),
|
||||
EventHint::Mark {
|
||||
start: start as u32,
|
||||
end: end as u32,
|
||||
info: flag,
|
||||
style: crate::container::richtext::Style {
|
||||
key: key.into(),
|
||||
data: value,
|
||||
},
|
||||
style: crate::container::richtext::Style { key, data: value },
|
||||
},
|
||||
&self.state,
|
||||
)?;
|
||||
|
@ -559,6 +585,20 @@ impl TextHandler {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check(&self) {
|
||||
self.state
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
.try_lock()
|
||||
.unwrap()
|
||||
.with_state_mut(self.container_idx, |state| {
|
||||
state
|
||||
.as_richtext_state_mut()
|
||||
.unwrap()
|
||||
.check_consistency_between_content_and_style_ranges()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply_delta(&self, delta: &[TextDelta]) -> LoroResult<()> {
|
||||
with_txn(&self.txn, |txn| self.apply_delta_with_txn(txn, delta))
|
||||
}
|
||||
|
@ -581,8 +621,9 @@ impl TextHandler {
|
|||
Some(attributes.as_ref().unwrap_or(&Default::default())),
|
||||
)?;
|
||||
|
||||
debug_log::debug_dbg!(&override_styles);
|
||||
for (key, value) in override_styles {
|
||||
marks.push((index, end, Cow::Owned(key), value));
|
||||
marks.push((index, end, key, value));
|
||||
}
|
||||
|
||||
index = end;
|
||||
|
@ -595,12 +636,7 @@ impl TextHandler {
|
|||
match attributes {
|
||||
Some(attr) if !attr.is_empty() => {
|
||||
for (key, value) in attr {
|
||||
marks.push((
|
||||
index,
|
||||
end,
|
||||
Cow::Borrowed(key.as_str()),
|
||||
value.clone(),
|
||||
));
|
||||
marks.push((index, end, key.deref().into(), value.clone()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -611,8 +647,7 @@ impl TextHandler {
|
|||
}
|
||||
|
||||
for (start, end, key, value) in marks {
|
||||
// FIXME: allow users to set a config table to store the flag, so that we can use it directly
|
||||
self.mark_with_txn(txn, start, end, &key, value, TextStyleInfoFlag::BOLD)?;
|
||||
self.mark_with_txn(txn, start, end, key.deref(), value, false)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -1348,7 +1383,6 @@ fn with_txn<R>(
|
|||
mod test {
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::container::richtext::TextStyleInfoFlag;
|
||||
use crate::loro::LoroDoc;
|
||||
use crate::version::Frontiers;
|
||||
use crate::{fx_map, ToJson};
|
||||
|
@ -1446,7 +1480,7 @@ mod test {
|
|||
let handler = loro.get_text("richtext");
|
||||
handler.insert_with_txn(&mut txn, 0, "hello world").unwrap();
|
||||
handler
|
||||
.mark_with_txn(&mut txn, 0, 5, "bold", true.into(), TextStyleInfoFlag::BOLD)
|
||||
.mark_with_txn(&mut txn, 0, 5, "bold", true.into(), false)
|
||||
.unwrap();
|
||||
txn.commit().unwrap();
|
||||
|
||||
|
@ -1498,7 +1532,7 @@ mod test {
|
|||
let handler = loro.get_text("richtext");
|
||||
handler.insert_with_txn(&mut txn, 0, "hello world").unwrap();
|
||||
handler
|
||||
.mark_with_txn(&mut txn, 0, 5, "bold", true.into(), TextStyleInfoFlag::BOLD)
|
||||
.mark_with_txn(&mut txn, 0, 5, "bold", true.into(), false)
|
||||
.unwrap();
|
||||
txn.commit().unwrap();
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ pub(crate) mod group;
|
|||
pub(crate) mod macros;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod value;
|
||||
pub(crate) use change::Timestamp;
|
||||
pub(crate) use id::{PeerID, ID};
|
||||
|
||||
// TODO: rename as Key?
|
||||
|
|
|
@ -15,7 +15,8 @@ use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue};
|
|||
use crate::{
|
||||
arena::SharedArena,
|
||||
change::Timestamp,
|
||||
container::{idx::ContainerIdx, IntoContainerId},
|
||||
configure::Configure,
|
||||
container::{idx::ContainerIdx, richtext::config::StyleConfigMap, IntoContainerId},
|
||||
encoding::{
|
||||
decode_snapshot, export_snapshot, parse_header_and_body, EncodeMode, ParsedHeaderAndBody,
|
||||
},
|
||||
|
@ -57,6 +58,7 @@ pub struct LoroDoc {
|
|||
oplog: Arc<Mutex<OpLog>>,
|
||||
state: Arc<Mutex<DocState>>,
|
||||
arena: SharedArena,
|
||||
config: Configure,
|
||||
observer: Arc<Observer>,
|
||||
diff_calculator: Arc<Mutex<DiffCalculator>>,
|
||||
// when dropping the doc, the txn will be committed
|
||||
|
@ -76,11 +78,13 @@ impl LoroDoc {
|
|||
let oplog = OpLog::new();
|
||||
let arena = oplog.arena.clone();
|
||||
let global_txn = Arc::new(Mutex::new(None));
|
||||
let config: Configure = Default::default();
|
||||
// share arena
|
||||
let state = DocState::new_arc(arena.clone(), Arc::downgrade(&global_txn));
|
||||
let state = DocState::new_arc(arena.clone(), Arc::downgrade(&global_txn), config.clone());
|
||||
Self {
|
||||
oplog: Arc::new(Mutex::new(oplog)),
|
||||
state,
|
||||
config,
|
||||
detached: AtomicBool::new(false),
|
||||
auto_commit: AtomicBool::new(false),
|
||||
observer: Arc::new(Observer::new(arena.clone())),
|
||||
|
@ -90,6 +94,11 @@ impl LoroDoc {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn config_text_style(&self, text_style: StyleConfigMap) {
|
||||
*self.config.text_style_config.try_write().unwrap() = text_style;
|
||||
}
|
||||
|
||||
/// Create a doc with auto commit enabled.
|
||||
#[inline]
|
||||
pub fn new_auto_commit() -> Self {
|
||||
|
@ -129,6 +138,7 @@ impl LoroDoc {
|
|||
Self {
|
||||
arena: oplog.arena.clone(),
|
||||
observer: Arc::new(obs),
|
||||
config: Default::default(),
|
||||
auto_commit: AtomicBool::new(false),
|
||||
oplog: Arc::new(Mutex::new(oplog)),
|
||||
state: Arc::new(Mutex::new(state)),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
sync::{Arc, Mutex, Weak},
|
||||
sync::{Arc, Mutex, RwLock, Weak},
|
||||
};
|
||||
|
||||
use enum_as_inner::EnumAsInner;
|
||||
|
@ -9,12 +9,12 @@ use fxhash::{FxHashMap, FxHashSet};
|
|||
use loro_common::{ContainerID, LoroError, LoroResult};
|
||||
|
||||
use crate::{
|
||||
configure::{DefaultRandom, SecureRandomGenerator},
|
||||
configure::{Configure, DefaultRandom, SecureRandomGenerator},
|
||||
container::{
|
||||
idx::ContainerIdx, list::list_op::ListOp, map::MapSet, tree::tree_op::TreeOp,
|
||||
ContainerIdRaw,
|
||||
idx::ContainerIdx, list::list_op::ListOp, map::MapSet, richtext::config::StyleConfigMap,
|
||||
tree::tree_op::TreeOp, ContainerIdRaw,
|
||||
},
|
||||
delta::{DeltaItem, MapValue},
|
||||
delta::DeltaItem,
|
||||
encoding::{StateSnapshotDecodeContext, StateSnapshotEncoder},
|
||||
event::{Diff, Index, InternalContainerDiff, InternalDiff},
|
||||
fx_map,
|
||||
|
@ -38,6 +38,17 @@ pub(crate) use tree_state::{get_meta_value, TreeState};
|
|||
|
||||
use super::{arena::SharedArena, event::InternalDocDiff};
|
||||
|
||||
macro_rules! get_or_create {
|
||||
($doc_state: ident, $idx: expr) => {{
|
||||
if !$doc_state.states.contains_key(&$idx) {
|
||||
let state = $doc_state.create_state($idx);
|
||||
$doc_state.states.insert($idx, state);
|
||||
}
|
||||
|
||||
$doc_state.states.get_mut(&$idx).unwrap()
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DocState {
|
||||
pub(super) peer: PeerID,
|
||||
|
@ -45,7 +56,7 @@ pub struct DocState {
|
|||
pub(super) frontiers: Frontiers,
|
||||
pub(super) states: FxHashMap<ContainerIdx, State>,
|
||||
pub(super) arena: SharedArena,
|
||||
|
||||
pub(crate) config: Configure,
|
||||
// resolve event stuff
|
||||
weak_state: Weak<Mutex<DocState>>,
|
||||
global_txn: Weak<Mutex<Option<Transaction>>>,
|
||||
|
@ -206,8 +217,8 @@ impl State {
|
|||
Self::MapState(Box::new(MapState::new(idx)))
|
||||
}
|
||||
|
||||
pub fn new_richtext(idx: ContainerIdx) -> Self {
|
||||
Self::RichtextState(Box::new(RichtextState::new(idx)))
|
||||
pub fn new_richtext(idx: ContainerIdx, config: Arc<RwLock<StyleConfigMap>>) -> Self {
|
||||
Self::RichtextState(Box::new(RichtextState::new(idx, config)))
|
||||
}
|
||||
|
||||
pub fn new_tree(idx: ContainerIdx) -> Self {
|
||||
|
@ -220,6 +231,7 @@ impl DocState {
|
|||
pub fn new_arc(
|
||||
arena: SharedArena,
|
||||
global_txn: Weak<Mutex<Option<Transaction>>>,
|
||||
config: Configure,
|
||||
) -> Arc<Mutex<Self>> {
|
||||
let peer = DefaultRandom.next_u64();
|
||||
// TODO: maybe we should switch to certain version in oplog?
|
||||
|
@ -230,6 +242,7 @@ impl DocState {
|
|||
frontiers: Frontiers::default(),
|
||||
states: FxHashMap::default(),
|
||||
weak_state: weak.clone(),
|
||||
config,
|
||||
global_txn,
|
||||
in_txn: false,
|
||||
changed_idx_in_txn: FxHashSet::default(),
|
||||
|
@ -376,10 +389,7 @@ impl DocState {
|
|||
diff.bring_back = true;
|
||||
}
|
||||
if diff.bring_back {
|
||||
let state = self
|
||||
.states
|
||||
.entry(diff.idx)
|
||||
.or_insert_with(|| create_state(idx));
|
||||
let state = get_or_create!(self, diff.idx);
|
||||
let state_diff = state.to_diff(&self.arena, &self.global_txn, &self.weak_state);
|
||||
if diff.diff.is_none() && state_diff.is_empty() {
|
||||
// empty diff, skip it
|
||||
|
@ -419,7 +429,7 @@ impl DocState {
|
|||
self.changed_idx_in_txn.insert(idx);
|
||||
}
|
||||
self.set_parent_by_diff(internal_diff.as_internal().unwrap(), idx);
|
||||
let state = self.states.entry(idx).or_insert_with(|| create_state(idx));
|
||||
let state = get_or_create!(self, idx);
|
||||
if is_recording {
|
||||
// process bring_back before apply
|
||||
let external_diff = if diff.bring_back {
|
||||
|
@ -465,11 +475,7 @@ impl DocState {
|
|||
pub fn apply_local_op(&mut self, raw_op: &RawOp, op: &Op) -> LoroResult<()> {
|
||||
// set parent first, `MapContainer` will only be created for TreeID that does not contain
|
||||
self.set_container_parent_by_op(raw_op);
|
||||
let state = self
|
||||
.states
|
||||
.entry(op.container)
|
||||
.or_insert_with(|| create_state(op.container));
|
||||
|
||||
let state = get_or_create!(self, op.container);
|
||||
if self.in_txn {
|
||||
self.changed_idx_in_txn.insert(op.container);
|
||||
}
|
||||
|
@ -526,12 +532,7 @@ impl DocState {
|
|||
}
|
||||
}
|
||||
RawOpContent::Tree(TreeOp { target, .. }) => {
|
||||
let state = self
|
||||
.states
|
||||
.entry(container)
|
||||
.or_insert_with(|| create_state(container))
|
||||
.as_tree_state()
|
||||
.unwrap();
|
||||
let state = get_or_create!(self, container).as_tree_state().unwrap();
|
||||
// create associated metadata container
|
||||
// TODO: maybe we could create map container only when setting metadata
|
||||
if !&state.trees.contains_key(target) {
|
||||
|
@ -570,12 +571,7 @@ impl DocState {
|
|||
}
|
||||
}
|
||||
InternalDiff::Tree(tree) => {
|
||||
let state = self
|
||||
.states
|
||||
.entry(container)
|
||||
.or_insert_with(|| create_state(container))
|
||||
.as_tree_state()
|
||||
.unwrap();
|
||||
let state = get_or_create!(self, container).as_tree_state().unwrap();
|
||||
for diff in tree.diff.iter() {
|
||||
let target = &diff.target;
|
||||
if !state.trees.contains_key(target) {
|
||||
|
@ -595,7 +591,7 @@ impl DocState {
|
|||
decode_ctx: StateSnapshotDecodeContext,
|
||||
) {
|
||||
let idx = self.arena.register_container(&cid);
|
||||
let state = self.states.entry(idx).or_insert_with(|| create_state(idx));
|
||||
let state = get_or_create!(self, idx);
|
||||
state.import_from_snapshot_ops(decode_ctx);
|
||||
}
|
||||
|
||||
|
@ -699,7 +695,7 @@ impl DocState {
|
|||
let idx = idx.unwrap();
|
||||
self.states
|
||||
.entry(idx)
|
||||
.or_insert_with(|| State::new_richtext(idx))
|
||||
.or_insert_with(|| State::new_richtext(idx, self.config.text_style_config.clone()))
|
||||
.as_richtext_state_mut()
|
||||
.map(|x| &mut **x)
|
||||
}
|
||||
|
@ -713,7 +709,7 @@ impl DocState {
|
|||
if let Some(state) = state {
|
||||
f(state)
|
||||
} else {
|
||||
let state = create_state(idx);
|
||||
let state = self.create_state(idx);
|
||||
let ans = f(&state);
|
||||
self.states.insert(idx, state);
|
||||
ans
|
||||
|
@ -729,7 +725,7 @@ impl DocState {
|
|||
if let Some(state) = state {
|
||||
f(state)
|
||||
} else {
|
||||
let mut state = create_state(idx);
|
||||
let mut state = self.create_state(idx);
|
||||
let ans = f(&mut state);
|
||||
self.states.insert(idx, state);
|
||||
ans
|
||||
|
@ -1083,6 +1079,18 @@ impl DocState {
|
|||
state_size_sum
|
||||
);
|
||||
}
|
||||
|
||||
pub fn create_state(&self, idx: ContainerIdx) -> State {
|
||||
match idx.get_type() {
|
||||
ContainerType::Map => State::MapState(Box::new(MapState::new(idx))),
|
||||
ContainerType::List => State::ListState(Box::new(ListState::new(idx))),
|
||||
ContainerType::Text => State::RichtextState(Box::new(RichtextState::new(
|
||||
idx,
|
||||
self.config.text_style_config.clone(),
|
||||
))),
|
||||
ContainerType::Tree => State::TreeState(Box::new(TreeState::new(idx))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SubContainerDiffPatch {
|
||||
|
@ -1176,15 +1184,6 @@ impl SubContainerDiffPatch {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn create_state(idx: ContainerIdx) -> State {
|
||||
match idx.get_type() {
|
||||
ContainerType::Map => State::MapState(Box::new(MapState::new(idx))),
|
||||
ContainerType::List => State::ListState(Box::new(ListState::new(idx))),
|
||||
ContainerType::Text => State::RichtextState(Box::new(RichtextState::new(idx))),
|
||||
ContainerType::Tree => State::TreeState(Box::new(TreeState::new(idx))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct EventRecorder {
|
||||
recording_diff: bool,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{
|
||||
ops::Range,
|
||||
sync::{Arc, Mutex, Weak},
|
||||
sync::{Arc, Mutex, RwLock, Weak},
|
||||
};
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
|
@ -12,6 +12,7 @@ use crate::{
|
|||
container::{
|
||||
idx::ContainerIdx,
|
||||
richtext::{
|
||||
config::StyleConfigMap,
|
||||
richtext_state::{EntityRangeInfo, PosType},
|
||||
AnchorType, RichtextState as InnerState, StyleOp, Styles,
|
||||
},
|
||||
|
@ -33,14 +34,16 @@ use super::ContainerState;
|
|||
#[derive(Debug)]
|
||||
pub struct RichtextState {
|
||||
idx: ContainerIdx,
|
||||
config: Arc<RwLock<StyleConfigMap>>,
|
||||
pub(crate) state: LazyLoad<RichtextStateLoader, InnerState>,
|
||||
}
|
||||
|
||||
impl RichtextState {
|
||||
#[inline]
|
||||
pub fn new(idx: ContainerIdx) -> Self {
|
||||
pub fn new(idx: ContainerIdx, config: Arc<RwLock<StyleConfigMap>>) -> Self {
|
||||
Self {
|
||||
idx,
|
||||
config,
|
||||
state: LazyLoad::Src(Default::default()),
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +77,7 @@ impl Clone for RichtextState {
|
|||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
idx: self.idx,
|
||||
config: self.config.clone(),
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
|
@ -176,25 +180,18 @@ impl ContainerState for RichtextState {
|
|||
event_index: start_event_index,
|
||||
} = style_starts.remove(style).unwrap();
|
||||
|
||||
let mut delta: Delta<StringSlice, _> =
|
||||
Delta::new().retain(start_event_index);
|
||||
// we need to + 1 because we also need to annotate the end anchor
|
||||
self.state.get_mut().annotate_style_range(
|
||||
let event = self.state.get_mut().annotate_style_range_with_event(
|
||||
start_entity_index..entity_index + 1,
|
||||
style.clone(),
|
||||
);
|
||||
for (s, l) in event {
|
||||
delta = delta.retain_with_meta(l, s);
|
||||
}
|
||||
|
||||
let mut meta = StyleMeta::default();
|
||||
|
||||
meta.insert(
|
||||
style.get_style_key(),
|
||||
crate::delta::StyleMetaItem {
|
||||
lamport: style.lamport,
|
||||
peer: style.peer,
|
||||
value: style.to_value(),
|
||||
},
|
||||
);
|
||||
let delta: Delta<StringSlice, _> = Delta::new()
|
||||
.retain(start_event_index)
|
||||
.retain_with_meta(new_event_index - start_event_index, meta);
|
||||
delta = delta.chop();
|
||||
style_delta = style_delta.compose(delta);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ use crate::{
|
|||
container::{
|
||||
idx::ContainerIdx,
|
||||
list::list_op::{DeleteSpan, InnerListOp},
|
||||
richtext::{Style, StyleKey, TextStyleInfoFlag},
|
||||
richtext::{Style, StyleKey},
|
||||
IntoContainerId,
|
||||
},
|
||||
delta::{
|
||||
|
@ -73,7 +73,6 @@ pub(super) enum EventHint {
|
|||
start: u32,
|
||||
end: u32,
|
||||
style: Style,
|
||||
info: TextStyleInfoFlag,
|
||||
},
|
||||
InsertText {
|
||||
/// pos is a Unicode index. If wasm, it's a UTF-16 index.
|
||||
|
@ -501,38 +500,16 @@ fn change_to_diff(
|
|||
}
|
||||
'outer: {
|
||||
match hint {
|
||||
EventHint::Mark {
|
||||
start,
|
||||
end,
|
||||
style,
|
||||
info,
|
||||
} => {
|
||||
EventHint::Mark { start, end, style } => {
|
||||
let mut meta = StyleMeta::default();
|
||||
if info.mergeable() {
|
||||
meta.insert(
|
||||
StyleKey::Key(style.key.clone()),
|
||||
StyleMetaItem {
|
||||
lamport,
|
||||
peer: change.id.peer,
|
||||
value: style.data,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
meta.insert(
|
||||
StyleKey::KeyWithId {
|
||||
key: style.key.clone(),
|
||||
id: ID {
|
||||
peer: change.id.peer,
|
||||
counter: op.counter,
|
||||
},
|
||||
},
|
||||
StyleMetaItem {
|
||||
lamport,
|
||||
peer: change.id.peer,
|
||||
value: style.data,
|
||||
},
|
||||
)
|
||||
}
|
||||
meta.insert(
|
||||
StyleKey::Key(style.key.clone()),
|
||||
StyleMetaItem {
|
||||
lamport,
|
||||
peer: change.id.peer,
|
||||
value: style.data,
|
||||
},
|
||||
);
|
||||
let diff = Delta::new()
|
||||
.retain(start as usize)
|
||||
.retain_with_meta((end - start) as usize, meta);
|
||||
|
|
|
@ -5,10 +5,6 @@ pub enum LazyLoad<Src, Dst: From<Src>> {
|
|||
}
|
||||
|
||||
impl<Src: Default, Dst: From<Src>> LazyLoad<Src, Dst> {
|
||||
pub fn new_dst(dst: Dst) -> Self {
|
||||
LazyLoad::Dst(dst)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut Dst {
|
||||
match self {
|
||||
LazyLoad::Src(src) => {
|
||||
|
|
|
@ -456,16 +456,7 @@ pub mod wasm {
|
|||
// TODO: refactor: should we extract the common code of ToJson and ToJsValue
|
||||
let obj = Object::new();
|
||||
for (key, style) in value.iter() {
|
||||
let value = if !key.contains_id() {
|
||||
JsValue::from(style.data)
|
||||
} else {
|
||||
let value = Object::new();
|
||||
js_sys::Reflect::set(&value, &"key".into(), &JsValue::from_str(&style.key))
|
||||
.unwrap();
|
||||
let data = JsValue::from(style.data);
|
||||
js_sys::Reflect::set(&value, &"data".into(), &data).unwrap();
|
||||
value.into()
|
||||
};
|
||||
let value = JsValue::from(style.data);
|
||||
js_sys::Reflect::set(&obj, &JsValue::from_str(&key.to_attr_key()), &value).unwrap();
|
||||
}
|
||||
|
||||
|
|
|
@ -36,18 +36,7 @@
|
|||
| Concurrent B | `Hello a World` |
|
||||
| Expected Result | `Hello a <b>World</b>` |
|
||||
|
||||
#### 4. Concurrently insert text and style that expands
|
||||
|
||||
This doesn't work well in our implementation
|
||||
|
||||
| Name | Text |
|
||||
|:----------------|:----------------------|
|
||||
| Origin | `Hello World` |
|
||||
| Concurrent A | `<b>Hello</b> World` |
|
||||
| Concurrent B | `Hello* World` |
|
||||
| Expected Result | `<b>Hello*</b> World` |
|
||||
|
||||
#### 5. Concurrent text edit & style that shrink
|
||||
#### 4. Concurrent text edit & style that shrink
|
||||
|
||||
| Name | Text |
|
||||
|:----------------|:---------------------------|
|
||||
|
@ -56,7 +45,7 @@ This doesn't work well in our implementation
|
|||
| Concurrent B | `Hey World` |
|
||||
| Expected Result | `<link>Hey</link> World` |
|
||||
|
||||
#### 6. Local insertion expand rules
|
||||
#### 5. Local insertion expand rules
|
||||
|
||||
> [**Hello**](https://www.google.com) World
|
||||
|
||||
|
@ -71,7 +60,7 @@ When insert a new character after "Hello", the new char should be bold but not l
|
|||
| Expected Result | `<b><link>Hello</link>t<b> World` |
|
||||
|
||||
|
||||
#### 7. Concurrent unbold
|
||||
#### 6. Concurrent unbold
|
||||
|
||||
In Peritext paper 2.3.2
|
||||
|
||||
|
@ -82,7 +71,7 @@ In Peritext paper 2.3.2
|
|||
| Concurrent B | `<b>The </b>fox<b> jumped</b> over the dog.` |
|
||||
| Expected Result | `The fox jumped over the dog.` |
|
||||
|
||||
#### 8. Bold & Unbold
|
||||
#### 7. Bold & Unbold
|
||||
|
||||
In Peritext paper 2.3.3
|
||||
|
||||
|
@ -93,7 +82,7 @@ In Peritext paper 2.3.3
|
|||
| Concurrent B | `<b>The</b> fox jumped over the <b>dog</b>.` |
|
||||
| Expected Result | `<b>The</b> fox jumped over the <b>dog</b>.` |
|
||||
|
||||
#### 9. Overlapped formatting
|
||||
#### 8. Overlapped formatting
|
||||
|
||||
In Peritext paper 3.2, example 3
|
||||
|
||||
|
@ -104,6 +93,6 @@ In Peritext paper 3.2, example 3
|
|||
| Concurrent B | The *fox jumped*. |
|
||||
| Expected Result | **The _fox_**<i> jumped</i>. |
|
||||
|
||||
#### 10. Multiple instances of the same mark
|
||||
#### 9. Multiple instances of the same mark
|
||||
|
||||
![](https://i.postimg.cc/MTNGq8cH/Clean-Shot-2023-10-09-at-12-16-29-2x.png)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use loro_common::LoroValue;
|
||||
use loro_internal::{container::richtext::TextStyleInfoFlag, LoroDoc, ToJson};
|
||||
use loro_internal::{LoroDoc, ToJson};
|
||||
use serde_json::json;
|
||||
|
||||
fn init(s: &str) -> LoroDoc {
|
||||
|
@ -37,14 +37,6 @@ impl Kind {
|
|||
Kind::Italic => "italic",
|
||||
}
|
||||
}
|
||||
|
||||
fn flag(&self) -> TextStyleInfoFlag {
|
||||
match self {
|
||||
Kind::Bold => TextStyleInfoFlag::BOLD,
|
||||
Kind::Link => TextStyleInfoFlag::LINK,
|
||||
Kind::Italic => TextStyleInfoFlag::BOLD,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(doc: &LoroDoc, pos: usize, s: &str) {
|
||||
|
@ -62,18 +54,7 @@ fn delete(doc: &LoroDoc, pos: usize, len: usize) {
|
|||
fn mark(doc: &LoroDoc, range: Range<usize>, kind: Kind) {
|
||||
let richtext = doc.get_text("r");
|
||||
doc.with_txn(|txn| {
|
||||
richtext.mark_with_txn(
|
||||
txn,
|
||||
range.start,
|
||||
range.end,
|
||||
kind.key(),
|
||||
if kind.flag().is_delete() {
|
||||
LoroValue::Null
|
||||
} else {
|
||||
true.into()
|
||||
},
|
||||
kind.flag(),
|
||||
)
|
||||
richtext.mark_with_txn(txn, range.start, range.end, kind.key(), true.into(), false)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -81,14 +62,15 @@ fn mark(doc: &LoroDoc, range: Range<usize>, kind: Kind) {
|
|||
fn unmark(doc: &LoroDoc, range: Range<usize>, kind: Kind) {
|
||||
let richtext = doc.get_text("r");
|
||||
doc.with_txn(|txn| {
|
||||
richtext.mark_with_txn(
|
||||
txn,
|
||||
range.start,
|
||||
range.end,
|
||||
kind.key(),
|
||||
false.into(),
|
||||
kind.flag().to_delete(),
|
||||
)
|
||||
richtext.mark_with_txn(txn, range.start, range.end, kind.key(), false.into(), false)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn mark_kv(doc: &LoroDoc, range: Range<usize>, key: &str, value: impl Into<LoroValue>) {
|
||||
let richtext = doc.get_text("r");
|
||||
doc.with_txn(|txn| {
|
||||
richtext.mark_with_txn(txn, range.start, range.end, key, value.into(), false)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -185,7 +167,7 @@ fn case3() {
|
|||
/// | Concurrent B | `Hey World` |
|
||||
/// | Expected Result | `<link>Hey</link> World` |
|
||||
#[test]
|
||||
fn case5() {
|
||||
fn case4() {
|
||||
let doc_a = init("Hello World");
|
||||
let doc_b = clone(&doc_a, 2);
|
||||
mark(&doc_a, 0..5, Kind::Link);
|
||||
|
@ -213,7 +195,7 @@ fn case5() {
|
|||
/// | Origin | `<b><link>Hello</link><b> World` |
|
||||
/// | Expected Result | `<b><link>Hello</link>t<b> World` |
|
||||
#[test]
|
||||
fn case6() {
|
||||
fn case5() {
|
||||
let doc = init("Hello World");
|
||||
mark(&doc, 0..5, Kind::Bold);
|
||||
expect_result(
|
||||
|
@ -251,7 +233,7 @@ fn case6() {
|
|||
/// | Concurrent B | `<b>The </b>fox<b> jumped</b> over the dog.` |
|
||||
/// | Expected Result | `The fox jumped over the dog.` |
|
||||
#[test]
|
||||
fn case7() {
|
||||
fn case6() {
|
||||
let doc_a = init("The fox jumped over the dog.");
|
||||
mark(&doc_a, 0..3, Kind::Bold);
|
||||
let doc_b = clone(&doc_a, 2);
|
||||
|
@ -278,7 +260,7 @@ fn case7() {
|
|||
/// | Concurrent B | `<b>The</b> fox jumped over the <b>dog</b>.` |
|
||||
/// | Expected Result | `<b>The</b> fox jumped over the <b>dog</b>.` |
|
||||
#[test]
|
||||
fn case8() {
|
||||
fn case7() {
|
||||
let doc_a = init("The fox jumped over the dog.");
|
||||
mark(&doc_a, 0..14, Kind::Bold);
|
||||
let doc_b = clone(&doc_a, 2);
|
||||
|
@ -307,7 +289,7 @@ fn case8() {
|
|||
/// | Concurrent B | The *fox jumped*. |
|
||||
/// | Expected Result | **The _fox_**<i> jumped</i>. |
|
||||
#[test]
|
||||
fn case9() {
|
||||
fn case8() {
|
||||
let doc_a = init("The fox jumped.");
|
||||
let doc_b = clone(&doc_a, 2);
|
||||
mark(&doc_a, 0..7, Kind::Bold);
|
||||
|
@ -326,6 +308,25 @@ fn case9() {
|
|||
doc_b.check_state_diff_calc_consistency_slow();
|
||||
}
|
||||
|
||||
/// ![](https://i.postimg.cc/MTNGq8cH/Clean-Shot-2023-10-09-at-12-16-29-2x.png)
|
||||
#[test]
|
||||
fn case9() {
|
||||
let doc_a = init("The fox jumped.");
|
||||
let doc_b = clone(&doc_a, 2);
|
||||
mark_kv(&doc_a, 0..7, "comment:alice", "alice comment");
|
||||
mark_kv(&doc_a, 4..14, "comment:bob", "bob comment");
|
||||
merge(&doc_a, &doc_b);
|
||||
expect_result(
|
||||
&doc_a,
|
||||
serde_json::json!([
|
||||
{"insert": "The ", "attributes": {"comment:alice": "alice comment"}},
|
||||
{"insert": "fox", "attributes": {"comment:alice": "alice comment", "comment:bob": "bob comment"}},
|
||||
{"insert": " jumped", "attributes": {"comment:bob": "bob comment"}},
|
||||
{"insert": "."}
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_after_link() {
|
||||
let doc_a = init("The fox jumped.");
|
||||
|
|
|
@ -2,7 +2,6 @@ use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
|||
|
||||
use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue, ID};
|
||||
use loro_internal::{
|
||||
container::richtext::TextStyleInfoFlag,
|
||||
handler::{Handler, TextDelta, ValueOrContainer},
|
||||
version::Frontiers,
|
||||
ApplyDiff, LoroDoc, ToJson,
|
||||
|
@ -14,7 +13,7 @@ fn issue_225() -> LoroResult<()> {
|
|||
let doc = LoroDoc::new_auto_commit();
|
||||
let text = doc.get_text("text");
|
||||
text.insert(0, "123")?;
|
||||
text.mark(0, 3, "bold", true.into(), TextStyleInfoFlag::BOLD)?;
|
||||
text.mark(0, 3, "bold", true.into())?;
|
||||
// when apply_delta, the attributes of insert should override the current styles
|
||||
text.apply_delta(&[
|
||||
TextDelta::Retain {
|
||||
|
@ -66,12 +65,10 @@ fn mark_with_the_same_key_value_should_be_skipped() {
|
|||
let a = LoroDoc::new_auto_commit();
|
||||
let text = a.get_text("text");
|
||||
text.insert(0, "Hello world!").unwrap();
|
||||
text.mark(0, 11, "key", "value".into(), TextStyleInfoFlag::BOLD)
|
||||
.unwrap();
|
||||
text.mark(0, 11, "bold", "value".into()).unwrap();
|
||||
a.commit_then_renew();
|
||||
let v = a.oplog_vv();
|
||||
text.mark(0, 5, "key", "value".into(), TextStyleInfoFlag::BOLD)
|
||||
.unwrap();
|
||||
text.mark(0, 5, "bold", "value".into()).unwrap();
|
||||
a.commit_then_renew();
|
||||
let new_v = a.oplog_vv();
|
||||
// new mark should be ignored, so vv should be the same
|
||||
|
@ -139,15 +136,9 @@ fn out_of_bound_test() {
|
|||
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
|
||||
let err = a.get_text("text").delete(3, 5).unwrap_err();
|
||||
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
|
||||
let err = a
|
||||
.get_text("text")
|
||||
.mark(0, 8, "h", 5.into(), TextStyleInfoFlag::BOLD)
|
||||
.unwrap_err();
|
||||
let err = a.get_text("text").mark(0, 8, "h", 5.into()).unwrap_err();
|
||||
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
|
||||
let _err = a
|
||||
.get_text("text")
|
||||
.mark(3, 0, "h", 5.into(), TextStyleInfoFlag::BOLD)
|
||||
.unwrap_err();
|
||||
let _err = a.get_text("text").mark(3, 0, "h", 5.into()).unwrap_err();
|
||||
let err = a.get_list("list").insert(6, "Hello").unwrap_err();
|
||||
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
|
||||
let err = a.get_list("list").delete(3, 2).unwrap_err();
|
||||
|
@ -211,17 +202,9 @@ fn richtext_mark_event() {
|
|||
}),
|
||||
);
|
||||
a.get_text("text").insert(0, "Hello").unwrap();
|
||||
a.get_text("text").mark(0, 5, "bold", true.into()).unwrap();
|
||||
a.get_text("text")
|
||||
.mark(0, 5, "bold", true.into(), TextStyleInfoFlag::BOLD)
|
||||
.unwrap();
|
||||
a.get_text("text")
|
||||
.mark(
|
||||
2,
|
||||
4,
|
||||
"bold",
|
||||
LoroValue::Null,
|
||||
TextStyleInfoFlag::BOLD.to_delete(),
|
||||
)
|
||||
.mark(2, 4, "bold", LoroValue::Null)
|
||||
.unwrap();
|
||||
a.commit_then_stop();
|
||||
let b = LoroDoc::new_auto_commit();
|
||||
|
@ -250,12 +233,8 @@ fn concurrent_richtext_mark_event() {
|
|||
a.get_text("text").insert(0, "Hello").unwrap();
|
||||
b.merge(&a).unwrap();
|
||||
c.merge(&a).unwrap();
|
||||
b.get_text("text")
|
||||
.mark(0, 3, "bold", true.into(), TextStyleInfoFlag::BOLD)
|
||||
.unwrap();
|
||||
c.get_text("text")
|
||||
.mark(1, 4, "link", true.into(), TextStyleInfoFlag::LINK)
|
||||
.unwrap();
|
||||
b.get_text("text").mark(0, 3, "bold", true.into()).unwrap();
|
||||
c.get_text("text").mark(1, 4, "link", true.into()).unwrap();
|
||||
b.merge(&c).unwrap();
|
||||
let sub_id = a.subscribe(
|
||||
&a.get_text("text").id(),
|
||||
|
@ -287,7 +266,7 @@ fn concurrent_richtext_mark_event() {
|
|||
},
|
||||
{
|
||||
"retain": 1,
|
||||
"attributes": {"bold": null}
|
||||
"attributes": {"bold": null, "link": true}
|
||||
}
|
||||
])
|
||||
)
|
||||
|
@ -295,13 +274,7 @@ fn concurrent_richtext_mark_event() {
|
|||
);
|
||||
|
||||
b.get_text("text")
|
||||
.mark(
|
||||
2,
|
||||
3,
|
||||
"bold",
|
||||
LoroValue::Null,
|
||||
TextStyleInfoFlag::BOLD.to_delete(),
|
||||
)
|
||||
.mark(2, 3, "bold", LoroValue::Null)
|
||||
.unwrap();
|
||||
a.merge(&b).unwrap();
|
||||
a.unsubscribe(sub_id);
|
||||
|
@ -331,9 +304,7 @@ fn concurrent_richtext_mark_event() {
|
|||
fn insert_richtext_event() {
|
||||
let a = LoroDoc::new_auto_commit();
|
||||
a.get_text("text").insert(0, "Hello").unwrap();
|
||||
a.get_text("text")
|
||||
.mark(0, 5, "bold", true.into(), TextStyleInfoFlag::BOLD)
|
||||
.unwrap();
|
||||
a.get_text("text").mark(0, 5, "bold", true.into()).unwrap();
|
||||
a.commit_then_renew();
|
||||
let text = a.get_text("text");
|
||||
a.subscribe(
|
||||
|
|
|
@ -3,10 +3,8 @@ use convert::resolved_diff_to_js;
|
|||
use js_sys::{Array, Object, Promise, Reflect, Uint8Array};
|
||||
use loro_internal::{
|
||||
change::Lamport,
|
||||
container::{
|
||||
richtext::{ExpandType, TextStyleInfoFlag},
|
||||
ContainerID,
|
||||
},
|
||||
configure::{StyleConfig, StyleConfigMap},
|
||||
container::{richtext::ExpandType, ContainerID},
|
||||
event::{Diff, Index},
|
||||
handler::{ListHandler, MapHandler, TextDelta, TextHandler, TreeHandler, ValueOrContainer},
|
||||
id::{Counter, TreeID, ID},
|
||||
|
@ -68,9 +66,7 @@ extern "C" {
|
|||
pub type JsOrigin;
|
||||
#[wasm_bindgen(typescript_type = "{ peer: PeerID, counter: number }")]
|
||||
pub type JsID;
|
||||
#[wasm_bindgen(
|
||||
typescript_type = "{ start: number, end: number, expand?: 'before'|'after'|'both'|'none' }"
|
||||
)]
|
||||
#[wasm_bindgen(typescript_type = "{ start: number, end: number }")]
|
||||
pub type JsRange;
|
||||
#[wasm_bindgen(typescript_type = "number|bool|string|null")]
|
||||
pub type JsMarkValue;
|
||||
|
@ -92,6 +88,8 @@ extern "C" {
|
|||
pub type JsValueOrContainerOrUndefined;
|
||||
#[wasm_bindgen(typescript_type = "[string, Value | Container]")]
|
||||
pub type MapEntry;
|
||||
#[wasm_bindgen(typescript_type = "{[key: string]: { expand: 'before'|'after'|'none'|'both' }}")]
|
||||
pub type JsTextStyles;
|
||||
}
|
||||
|
||||
mod observer {
|
||||
|
@ -232,6 +230,34 @@ impl Loro {
|
|||
Self(Arc::new(doc))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "configTextStyle")]
|
||||
pub fn config_text_style(&self, styles: JsTextStyles) -> JsResult<()> {
|
||||
let mut style_config = StyleConfigMap::new();
|
||||
// read key value pair in styles
|
||||
for key in Reflect::own_keys(&styles)?.iter() {
|
||||
let value = Reflect::get(&styles, &key).unwrap();
|
||||
let key = key.as_string().unwrap();
|
||||
// Assert value is an object, otherwise throw an error with desc
|
||||
if !value.is_object() {
|
||||
return Err("Text style config format error".into());
|
||||
}
|
||||
// read expand value from value
|
||||
let expand = Reflect::get(&value, &"expand".into()).expect("`expand` not specified");
|
||||
let expand_str = expand.as_string().unwrap();
|
||||
// read allowOverlap value from value
|
||||
style_config.insert(
|
||||
key.into(),
|
||||
StyleConfig {
|
||||
expand: ExpandType::try_from_str(&expand_str)
|
||||
.expect("`expand` must be one of `none`, `start`, `end`, `both`"),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
self.0.config_text_style(style_config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a loro document from the snapshot.
|
||||
///
|
||||
/// @see You can check out what is the snapshot [here](#).
|
||||
|
@ -1033,7 +1059,6 @@ pub struct LoroText {
|
|||
struct MarkRange {
|
||||
start: usize,
|
||||
end: usize,
|
||||
expand: Option<String>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
|
@ -1104,20 +1129,7 @@ impl LoroText {
|
|||
pub fn mark(&self, range: JsRange, key: &str, value: JsValue) -> Result<(), JsError> {
|
||||
let range: MarkRange = serde_wasm_bindgen::from_value(range.into())?;
|
||||
let value: LoroValue = LoroValue::from(value);
|
||||
let expand = range
|
||||
.expand
|
||||
.map(|x| {
|
||||
ExpandType::try_from_str(&x)
|
||||
.expect_throw("`expand` must be one of `none`, `start`, `end`, `both`")
|
||||
})
|
||||
.unwrap_or(ExpandType::After);
|
||||
self.handler.mark(
|
||||
range.start,
|
||||
range.end,
|
||||
key,
|
||||
value,
|
||||
TextStyleInfoFlag::new(true, expand, false, false),
|
||||
)?;
|
||||
self.handler.mark(range.start, range.end, key, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1151,21 +1163,7 @@ impl LoroText {
|
|||
pub fn unmark(&self, range: JsRange, key: &str) -> Result<(), JsValue> {
|
||||
// Internally, this may be marking with null or deleting all the marks with key in the range entirely.
|
||||
let range: MarkRange = serde_wasm_bindgen::from_value(range.into())?;
|
||||
let expand = range
|
||||
.expand
|
||||
.map(|x| {
|
||||
ExpandType::try_from_str(&x)
|
||||
.expect_throw("`expand` must be one of `none`, `start`, `end`, `both`")
|
||||
})
|
||||
.unwrap_or(ExpandType::After);
|
||||
let expand = expand.reverse();
|
||||
self.handler.mark(
|
||||
range.start,
|
||||
range.end,
|
||||
key,
|
||||
LoroValue::Null,
|
||||
TextStyleInfoFlag::new(true, expand, true, false),
|
||||
)?;
|
||||
self.handler.unmark(range.start, range.end, key)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ use serde_json::json;
|
|||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.insert(0, "Hello world!").unwrap();
|
||||
text.mark(0..5, ExpandType::After, "bold", true).unwrap();
|
||||
text.mark(0..5, "bold", true).unwrap();
|
||||
assert_eq!(
|
||||
text.to_delta().to_json_value(),
|
||||
json!([
|
||||
|
@ -63,7 +63,7 @@ assert_eq!(
|
|||
{ "insert": " world!" },
|
||||
])
|
||||
);
|
||||
text.unmark(3..5, ExpandType::After, "bold").unwrap();
|
||||
text.unmark(3..5, "bold").unwrap();
|
||||
assert_eq!(
|
||||
text.to_delta().to_json_value(),
|
||||
json!([
|
||||
|
@ -88,7 +88,7 @@ doc_b.import(&bytes).unwrap();
|
|||
assert_eq!(doc.get_deep_value(), doc_b.get_deep_value());
|
||||
let text_b = doc_b.get_text("text");
|
||||
text_b
|
||||
.mark(0..5, loro::ExpandType::After, "bold", true)
|
||||
.mark(0..5, "bold", true)
|
||||
.unwrap();
|
||||
doc.import(&doc_b.export_from(&doc.oplog_vv())).unwrap();
|
||||
assert_eq!(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
use either::Either;
|
||||
use loro_internal::change::Timestamp;
|
||||
use loro_internal::container::richtext::TextStyleInfoFlag;
|
||||
use loro_internal::container::IntoContainerId;
|
||||
use loro_internal::handler::TextDelta;
|
||||
use loro_internal::handler::ValueOrContainer;
|
||||
|
@ -534,17 +533,10 @@ impl LoroText {
|
|||
pub fn mark(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
expand: ExpandType,
|
||||
key: &str,
|
||||
value: impl Into<LoroValue>,
|
||||
) -> LoroResult<()> {
|
||||
self.handler.mark(
|
||||
range.start,
|
||||
range.end,
|
||||
key,
|
||||
value.into(),
|
||||
TextStyleInfoFlag::new(true, expand, false, false),
|
||||
)
|
||||
self.handler.mark(range.start, range.end, key, value.into())
|
||||
}
|
||||
|
||||
/// Unmark a range of text with a key and a value.
|
||||
|
@ -563,15 +555,8 @@ impl LoroText {
|
|||
/// *You should make sure that a key is always associated with the same expand type.*
|
||||
///
|
||||
/// Note: you cannot delete unmergeable annotations like comments by this method.
|
||||
pub fn unmark(&self, range: Range<usize>, expand: ExpandType, key: &str) -> LoroResult<()> {
|
||||
let expand = expand.reverse();
|
||||
self.handler.mark(
|
||||
range.start,
|
||||
range.end,
|
||||
key,
|
||||
LoroValue::Null,
|
||||
TextStyleInfoFlag::new(true, expand, true, false),
|
||||
)
|
||||
pub fn unmark(&self, range: Range<usize>, key: &str) -> LoroResult<()> {
|
||||
self.handler.unmark(range.start, range.end, key)
|
||||
}
|
||||
|
||||
/// Get the text in [Delta](https://quilljs.com/docs/delta/) format.
|
||||
|
@ -584,7 +569,7 @@ impl LoroText {
|
|||
/// let doc = LoroDoc::new();
|
||||
/// let text = doc.get_text("text");
|
||||
/// text.insert(0, "Hello world!").unwrap();
|
||||
/// text.mark(0..5, ExpandType::After, "bold", true).unwrap();
|
||||
/// text.mark(0..5, "bold", true).unwrap();
|
||||
/// assert_eq!(
|
||||
/// text.to_delta().to_json_value(),
|
||||
/// json!([
|
||||
|
@ -592,7 +577,7 @@ impl LoroText {
|
|||
/// { "insert": " world!" },
|
||||
/// ])
|
||||
/// );
|
||||
/// text.unmark(3..5, ExpandType::After, "bold").unwrap();
|
||||
/// text.unmark(3..5, "bold").unwrap();
|
||||
/// assert_eq!(
|
||||
/// text.to_delta().to_json_value(),
|
||||
/// json!([
|
||||
|
|
|
@ -90,13 +90,13 @@ fn check_sync_send(_doc: impl Sync + Send) {}
|
|||
|
||||
#[test]
|
||||
fn richtext_test() {
|
||||
use loro::{ExpandType, LoroDoc, ToJson};
|
||||
use loro::{LoroDoc, ToJson};
|
||||
use serde_json::json;
|
||||
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.insert(0, "Hello world!").unwrap();
|
||||
text.mark(0..5, ExpandType::After, "bold", true).unwrap();
|
||||
text.mark(0..5, "bold", true).unwrap();
|
||||
assert_eq!(
|
||||
text.to_delta().to_json_value(),
|
||||
json!([
|
||||
|
@ -104,7 +104,7 @@ fn richtext_test() {
|
|||
{ "insert": " world!" },
|
||||
])
|
||||
);
|
||||
text.unmark(3..5, ExpandType::After, "bold").unwrap();
|
||||
text.unmark(3..5, "bold").unwrap();
|
||||
assert_eq!(
|
||||
text.to_delta().to_json_value(),
|
||||
json!([
|
||||
|
@ -127,9 +127,7 @@ fn sync() {
|
|||
doc_b.import(&bytes).unwrap();
|
||||
assert_eq!(doc.get_deep_value(), doc_b.get_deep_value());
|
||||
let text_b = doc_b.get_text("text");
|
||||
text_b
|
||||
.mark(0..5, loro::ExpandType::After, "bold", true)
|
||||
.unwrap();
|
||||
text_b.mark(0..5, "bold", true).unwrap();
|
||||
doc.import(&doc_b.export_from(&doc.oplog_vv())).unwrap();
|
||||
assert_eq!(
|
||||
text.to_delta().to_json_value(),
|
||||
|
|
|
@ -114,11 +114,58 @@ describe("richtext", () => {
|
|||
text2.applyDelta(e.diff);
|
||||
});
|
||||
text.insert(0, "foo");
|
||||
text.mark({ start: 0, end: 3, expand: "none" }, "link", true);
|
||||
text.mark({ start: 0, end: 3 }, "link", true);
|
||||
doc.commit();
|
||||
text.insert(3, "baz");
|
||||
doc.commit();
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
expect(text2.toDelta()).toStrictEqual([{ insert: 'foo', attributes: { link: true } }, { insert: 'baz' }]);
|
||||
})
|
||||
|
||||
it("custom richtext type", async () => {
|
||||
const doc = new Loro();
|
||||
doc.configTextStyle({
|
||||
myStyle: {
|
||||
expand: "none",
|
||||
}
|
||||
})
|
||||
const text = doc.getText("text");
|
||||
text.insert(0, "foo");
|
||||
text.mark({ start: 0, end: 3 }, "myStyle", 123);
|
||||
expect(text.toDelta()).toStrictEqual([{ insert: 'foo', attributes: { myStyle: 123 } }]);
|
||||
|
||||
expect(() => {
|
||||
text.mark({ start: 0, end: 3 }, "unknownStyle", 2);
|
||||
}).toThrowError()
|
||||
|
||||
expect(() => {
|
||||
// default style config should be overwritten
|
||||
text.mark({ start: 0, end: 3 }, "bold", 2);
|
||||
}).toThrowError()
|
||||
})
|
||||
|
||||
it("allow overlapped styles", () => {
|
||||
const doc = new Loro();
|
||||
doc.configTextStyle({
|
||||
comment: { expand: "none", }
|
||||
})
|
||||
const text = doc.getText("text");
|
||||
text.insert(0, "The fox jumped.");
|
||||
text.mark({ start: 0, end: 7 }, "comment:alice", "Hi");
|
||||
text.mark({ start: 4, end: 14 }, "comment:bob", "Jump");
|
||||
expect(text.toDelta()).toStrictEqual([
|
||||
{
|
||||
insert: "The ", attributes: { "comment:alice": "Hi" },
|
||||
},
|
||||
{
|
||||
insert: "fox", attributes: { "comment:alice": "Hi", "comment:bob": "Jump" },
|
||||
},
|
||||
{
|
||||
insert: " jumped", attributes: { "comment:bob": "Jump" },
|
||||
},
|
||||
{
|
||||
insert: ".",
|
||||
}
|
||||
])
|
||||
})
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue