From b61083f8b23bc89d55e00fc91c4db11833edfba4 Mon Sep 17 00:00:00 2001 From: waidhoferj Date: Sun, 3 Apr 2022 10:47:17 -0700 Subject: [PATCH 1/4] Added rich text formatting --- src/type_conversions.rs | 2 +- src/y_text.rs | 116 ++++++++++++++++++++++++++++++++++++++-- tests/test_y_text.py | 55 +++++++++++++++++++ y_py.pyi | 31 ++++++++++- 4 files changed, 198 insertions(+), 6 deletions(-) diff --git a/src/type_conversions.rs b/src/type_conversions.rs index 8181247..f471190 100644 --- a/src/type_conversions.rs +++ b/src/type_conversions.rs @@ -231,7 +231,7 @@ pub fn insert_at(dst: &Array, txn: &mut Transaction, index: u32, src: Vec Option { +pub fn py_into_any(v: PyObject) -> Option { Python::with_gil(|py| -> Option { let v = v.as_ref(py); diff --git a/src/y_text.rs b/src/y_text.rs index 1e0f93e..44e525c 100644 --- a/src/y_text.rs +++ b/src/y_text.rs @@ -1,11 +1,15 @@ +use std::collections::HashMap; + use lib0::any::Any; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::PyList; use yrs::types::text::TextEvent; +use yrs::types::Attrs; use yrs::{SubscriptionId, Text, Transaction}; use crate::shared_types::SharedType; +use crate::type_conversions::py_into_any; use crate::type_conversions::ToPython; use crate::y_transaction::YTransaction; @@ -86,10 +90,86 @@ impl YText { } /// Inserts a given `chunk` of text into this `YText` instance, starting at a given `index`. - pub fn insert(&mut self, txn: &mut YTransaction, index: u32, chunk: &str) { + pub fn insert( + &mut self, + txn: &mut YTransaction, + index: u32, + chunk: &str, + attributes: Option>, + ) -> PyResult<()> { + let attributes: Option> = attributes.map(Self::parse_attrs); + + if let Some(Ok(attributes)) = attributes { + match &mut self.0 { + SharedType::Integrated(text) => { + text.insert_with_attributes(txn, index, chunk, attributes); + Ok(()) + } + SharedType::Prelim(_) => Err(PyTypeError::new_err("OOf")), + } + } else if let Some(Err(error)) = attributes { + Err(error) + } else { + match &mut self.0 { + SharedType::Integrated(text) => text.insert(txn, index, chunk), + SharedType::Prelim(prelim_string) => { + prelim_string.insert_str(index as usize, chunk) + } + } + Ok(()) + } + } + + /// Inserts a given `embed` object into this `YText` instance, starting at a given `index`. + /// + /// Optional object with defined `attributes` will be used to wrap provided `embed` + /// with a formatting blocks.`attributes` are only supported for a `YText` instance which + /// already has been integrated into document store. + pub fn insert_embed( + &mut self, + txn: &mut YTransaction, + index: u32, + embed: PyObject, + attributes: Option>, + ) -> PyResult<()> { match &mut self.0 { - SharedType::Integrated(v) => v.insert(txn, index, chunk), - SharedType::Prelim(v) => v.insert_str(index as usize, chunk), + SharedType::Integrated(text) => { + let content = py_into_any(embed) + .ok_or(PyTypeError::new_err("Content could not be embedded"))?; + if let Some(Ok(attrs)) = attributes.map(Self::parse_attrs) { + text.insert_embed_with_attributes(txn, index, content, attrs) + } else { + text.insert_embed(txn, index, content) + } + Ok(()) + } + SharedType::Prelim(_) => Err(PyTypeError::new_err( + "Insert embeds requires YText instance to be integrated first.", + )), + } + } + + /// Wraps an existing piece of text within a range described by `index`-`length` parameters with + /// formatting blocks containing provided `attributes` metadata. This method only works for + /// `YText` instances that already have been integrated into document store. + pub fn format( + &mut self, + txn: &mut YTransaction, + index: u32, + length: u32, + attributes: HashMap, + ) -> PyResult<()> { + match Self::parse_attrs(attributes) { + Ok(attrs) => match &mut self.0 { + SharedType::Integrated(text) => { + text.format(txn, index, length, attrs); + Ok(()) + } + SharedType::Prelim(_) => Err(PyTypeError::new_err( + "Insert embeds requires YText instance to be integrated first.", + )), + }, + Err(err) => Err(err), } } @@ -143,6 +223,25 @@ impl YText { } } +impl YText { + fn parse_attrs(attrs: HashMap) -> PyResult { + attrs + .into_iter() + .map(|(k, v)| { + let key = k.into_boxed_str(); + let value = py_into_any(v); + if let Some(value) = value { + Ok((key, value)) + } else { + Err(PyTypeError::new_err( + "Cannot convert attributes into a standard type".to_string(), + )) + } + }) + .collect() + } +} + /// Event generated by `YYText.observe` method. Emitted during transaction commit phase. #[pyclass(unsendable)] pub struct YTextEvent { @@ -218,4 +317,15 @@ impl YTextEvent { delta } } + + fn __str__(&self) -> String { + format!( + "YTextEvent(target={:?}, delta={:?})", + self.target, self.delta + ) + } + + fn __repr__(&self) -> String { + self.__str__() + } } diff --git a/tests/test_y_text.py b/tests/test_y_text.py index cd4278f..a84d34e 100644 --- a/tests/test_y_text.py +++ b/tests/test_y_text.py @@ -136,3 +136,58 @@ def register_callback(x, callback): assert str(target) == str(x) assert delta == [{"insert": "abcd"}] + + +def test_delta_embed_attributes(): + + d1 = Y.YDoc() + text = d1.get_text("test") + + delta = None + + def callback(e): + nonlocal delta + delta = e.delta + + sub = text.observe(callback) + + with d1.begin_transaction() as txn: + text.insert(txn, 0, "ab", {"bold": True}) + text.insert_embed(txn, 1, {"image": "imageSrc.png"}, {"width": 100}) + + expected = [ + {"insert": "a", "attributes": {"bold": True}}, + {"insert": {"image": "imageSrc.png"}, "attributes": {"width": 100}}, + {"insert": "b", "attributes": {"bold": True}}, + ] + assert delta == expected + + text.unobserve(sub) + + +def test_formatting(): + d1 = Y.YDoc() + text = d1.get_text("test") + + delta = None + target = None + + def callback(e): + nonlocal delta + nonlocal target + delta = e.delta + target = e.target + + sub = text.observe(callback) + + with d1.begin_transaction() as txn: + text.insert(txn, 0, "stylish") + text.format(txn, 0, 4, {"bold": True}) + + expected = [ + {"insert": "styl", "attributes": {"bold": True}}, + {"insert": "ish"}, + ] + assert delta == expected + + text.unobserve(sub) diff --git a/y_py.pyi b/y_py.pyi index 5800204..c62fa0d 100644 --- a/y_py.pyi +++ b/y_py.pyi @@ -375,9 +375,36 @@ class YText: Returns: The underlying shared string stored in this data type. """ - def insert(self, txn: YTransaction, index: int, chunk: str): + def insert( + self, + txn: YTransaction, + index: int, + chunk: str, + attributes: Optional[Dict[str, Any]], + ): + """ + Inserts a string of text into the `YText` instance starting at a given `index`. + Attributes are optional style modifiers (`{"bold": True}`) that can be attached to the inserted string. + Attributes are only supported for a `YText` instance which already has been integrated into document store. + """ + def insert_embed( + self, + txn: YTransaction, + index: int, + embed: Any, + attributes: Optional[Dict[str, Any]], + ): + """ + Inserts embedded content into the YText at the provided index. Attributes are user-defined metadata associated with the embedded content. + Attributes are only supported for a `YText` instance which already has been integrated into document store. + """ + def format( + self, txn: YTransaction, index: int, length: int, attributes: Dict[str, Any] + ): """ - Inserts a given `chunk` of text into this `YText` instance, starting at a given `index`. + Wraps an existing piece of text within a range described by `index`-`length` parameters with + formatting blocks containing provided `attributes` metadata. This method only works for + `YText` instances that already have been integrated into document store """ def push(self, txn: YTransaction, chunk: str): """ From abf0800d9f2e8f100eaa965a1b32ab899c73ba32 Mon Sep 17 00:00:00 2001 From: waidhoferj Date: Sun, 17 Apr 2022 23:58:41 -0700 Subject: [PATCH 2/4] format test --- tests/test_y_text.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_y_text.py b/tests/test_y_text.py index a84d34e..397603c 100644 --- a/tests/test_y_text.py +++ b/tests/test_y_text.py @@ -182,6 +182,8 @@ def callback(e): with d1.begin_transaction() as txn: text.insert(txn, 0, "stylish") + + with d1.begin_transaction() as txn: text.format(txn, 0, 4, {"bold": True}) expected = [ From e289e59f6d4adc55e00b2ad6ea7e0d3acbcf064b Mon Sep 17 00:00:00 2001 From: waidhoferj Date: Wed, 27 Apr 2022 18:47:38 -0700 Subject: [PATCH 3/4] updated api and test --- src/y_text.rs | 3 ++- tests/test_y_text.py | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/y_text.rs b/src/y_text.rs index 44e525c..b78bd1e 100644 --- a/src/y_text.rs +++ b/src/y_text.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::rc::Rc; use lib0::any::Any; use pyo3::exceptions::PyTypeError; @@ -228,7 +229,7 @@ impl YText { attrs .into_iter() .map(|(k, v)| { - let key = k.into_boxed_str(); + let key = Rc::from(k); let value = py_into_any(v); if let Some(value) = value { Ok((key, value)) diff --git a/tests/test_y_text.py b/tests/test_y_text.py index 397603c..b5b2c8d 100644 --- a/tests/test_y_text.py +++ b/tests/test_y_text.py @@ -165,6 +165,7 @@ def callback(e): text.unobserve(sub) +# TODO: Fix final test def test_formatting(): d1 = Y.YDoc() text = d1.get_text("test") @@ -182,14 +183,16 @@ def callback(e): with d1.begin_transaction() as txn: text.insert(txn, 0, "stylish") - - with d1.begin_transaction() as txn: text.format(txn, 0, 4, {"bold": True}) - expected = [ + assert delta == [ {"insert": "styl", "attributes": {"bold": True}}, {"insert": "ish"}, ] - assert delta == expected + + with d1.begin_transaction() as txn: + text.format(txn, 4, 7, {"bold": True}) + + assert delta == [{"retain": 4}, {"retain": 3, "attributes": {"bold": True}}] text.unobserve(sub) From 03252ff1f6e5805620d0c18e4c0d7d72cca6822a Mon Sep 17 00:00:00 2001 From: waidhoferj Date: Wed, 27 Apr 2022 19:01:19 -0700 Subject: [PATCH 4/4] took out todo --- tests/test_y_text.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_y_text.py b/tests/test_y_text.py index b5b2c8d..774a23e 100644 --- a/tests/test_y_text.py +++ b/tests/test_y_text.py @@ -165,7 +165,6 @@ def callback(e): text.unobserve(sub) -# TODO: Fix final test def test_formatting(): d1 = Y.YDoc() text = d1.get_text("test")