Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[USING, DON'T MERGE THO] Enable Lexical-style parsing #1

Open
wants to merge 52 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
cf619fd
Set up tests
zswaff Mar 21, 2024
e606efe
Improve comments
zswaff Mar 21, 2024
4b1b72f
Update tests
zswaff Mar 27, 2024
d5785dc
Start implementing rust version and add JS version for reference
zswaff Apr 4, 2024
9441f3d
Set up python binding for direct function
zswaff May 6, 2024
bec43ee
Added parse_nested_xml_text_nodes test
ahonko May 21, 2024
59379af
Clean up
ahonko May 21, 2024
022f618
Removed playground.ts
ahonko May 22, 2024
c1623a9
Clean up lexical parsing XML text nodes
ahonko May 22, 2024
cc486a1
Clean up tests
ahonko May 22, 2024
c3582f6
Reverted non related changes in tests
ahonko May 22, 2024
0f64aeb
Moved parse_nested_xml_text_nodes test to y_doc module
ahonko May 22, 2024
ef62cbd
Polish
zswaff May 22, 2024
08bd33b
Tweak version to fix issues with pip resolution
zswaff May 22, 2024
fdbb622
Added more tests
ahonko May 24, 2024
bf079d5
Refactored, removed rust tests
ahonko May 25, 2024
6ce5e4d
Clean up
ahonko May 25, 2024
7bd93d4
Cleanup test
zswaff May 25, 2024
f2903d9
ypy reverse direction
ahonko May 31, 2024
54a944f
Tweak comments
zswaff May 31, 2024
7f93c38
Tweak again
zswaff May 31, 2024
ae613ba
Merge pull request #2 from its-dart/ahonko/DA-54-ypy-reverse-direction
zswaff May 31, 2024
d8d5aa8
ypy enable ints and nulls
ahonko Jun 4, 2024
712cb3a
Merge pull request #3 from its-dart/ahonko/DA-148-ypy-enable-ints-and…
zswaff Jun 4, 2024
c61b1c7
Update version in Cargo.toml
zswaff Jun 4, 2024
8a4c454
ypy use XmlText rather than XmlElement for tree
ahonko Jun 10, 2024
f9d0507
Clean up
ahonko Jun 10, 2024
627e0ef
Clean up
ahonko Jun 10, 2024
f8359b2
Merge pull request #4 from its-dart/ahonko/DA-159-ypy-use-XmlText-rat…
zswaff Jun 10, 2024
f9732ab
Update Cargo.toml
zswaff Jun 10, 2024
c6e6f17
Revert BigInt stuff
zswaff Jun 10, 2024
93f432e
Simplify version
zswaff Jun 10, 2024
b9c796a
Merge pull request #5 from its-dart/zack/revert-bigint
zswaff Jun 10, 2024
c1accaf
Set up library workflow
zswaff Jul 16, 2024
ffa6bad
Update Cargo.toml
ahonko Jul 16, 2024
fcbd331
Install correct lib for testing
zswaff Jul 16, 2024
8b09e9a
Keep improving wheel build process
zswaff Jul 16, 2024
b1ec2a0
Bump version
zswaff Jul 16, 2024
3e21db2
Comment out macos tests
zswaff Jul 16, 2024
0161dbd
Remove all mac tests
zswaff Jul 16, 2024
c4519c7
Rename package
zswaff Jul 16, 2024
496a023
Bump version to get new y-crdt
zswaff Jul 17, 2024
d2b3a37
Revert package rename
zswaff Jul 17, 2024
a34b648
Bump pyo3 version
zswaff Jul 19, 2024
d519732
Bump pyo3 version
zswaff Jul 19, 2024
8002cea
Fix version
zswaff Jul 19, 2024
6720b0c
Create test
zswaff Jul 19, 2024
a2ecb74
Added parsing Element and Fragment XML nodes while processing Text node
ahonko Jul 19, 2024
8ec52e9
Fixed tests
ahonko Jul 19, 2024
52b1afc
Merge pull request #6 from its-dart/zack/parse-nested-elements
zswaff Jul 20, 2024
de73c6b
Bump version
zswaff Jul 20, 2024
e9d95e6
Attempt to enable pushing xml elements
zswaff Jul 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "y-py"
version = "0.7.0-alpha.1"
version = "0.6.3-alpha.4"
rust-version = "1.72"
edition = "2021"

Expand All @@ -12,8 +12,16 @@ crate-type = ["cdylib"]

[dependencies]
lib0 = "0.16.10"
yrs = "0.16.10"
yrs = { git = "https://github.com/its-dart/y-crdt.git", branch = "v0.16.10" }

[dev-dependencies]
assert-json-diff = "2.0.2"
serde = { version = "1.0.136", features = ["derive"] }
serde_json = { version = "1.0.79", features = ["unbounded_depth"] }

[dependencies.pyo3]
version = "0.19.2"
features = ["extension-module"]
# Make the extension-module feature optional and default.
# See https://pyo3.rs/v0.21.2/faq.html#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror for more details
extension-module = ["pyo3/extension-module"]
default = ["extension-module"]
ahonko marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["maturin>=1.2.3,<2"]
build-backend = "maturin"

[project]
name = "y-py"
name = "y-py-dart"
description = "Python bindings for the Y-CRDT built from yrs (Rust)"
license = { file = "LICENSE" }
authors = [
Expand All @@ -30,10 +30,10 @@ classifiers = [
]

[project.urls]
Homepage = "https://github.com/y-crdt/ypy"
Source = "https://github.com/y-crdt/ypy"
Issues = "https://github.com/y-crdt/ypy/issues"
Pypi = "https://pypi.org/project/y-py"
Homepage = "https://github.com/its-dart/ypy"
Source = "https://github.com/its-dart/ypy"
Issues = "https://github.com/its-dart/ypy/issues"
Pypi = "https://pypi.org/project/y-py-dart"

[tool.hatch.envs.test]
dependencies = ["pytest", "maturin"]
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![recursion_limit = "512"]

use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
mod json_builder;
Expand Down
284 changes: 279 additions & 5 deletions src/y_xml.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
use crate::shared_types::CompatiblePyType;
use crate::shared_types::{SubId, TypeWithDoc};
use crate::y_doc::{WithDoc, YDocInner};
use lib0::any::Any;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use std::cell::RefCell;
use std::collections::HashMap;
use std::mem::ManuallyDrop;
use std::ops::Deref;
use std::rc::Rc;
use yrs::types::xml::{TreeWalker, Xml, XmlEvent, XmlTextEvent};
use yrs::block::ItemContent;
use yrs::types::xml::{self, TreeWalker, Xml, XmlEvent, XmlTextEvent};
use yrs::types::{BranchPtr, ToJson, TYPE_REFS_MAP, TYPE_REFS_XML_TEXT};
use yrs::types::{DeepObservable, EntryChange, Path, PathSegment};
use yrs::MapRef;
use yrs::XmlFragmentRef;
use yrs::XmlTextRef;
use yrs::{GetString, XmlElementPrelim, XmlElementRef, XmlTextPrelim};
Expand All @@ -17,6 +23,178 @@ use crate::shared_types::{DeepSubscription, ShallowSubscription};
use crate::type_conversions::{events_into_py, ToPython, WithDocToPython};
use crate::y_transaction::{YTransaction, YTransactionInner};

pub fn process_xml_text_node(txn: &TransactionMut<'static>, xml_text_ref: &XmlTextRef) -> Any {
let mut result: HashMap<String, Any> = HashMap::new();
// Update attributes of the current Text XmlNode
let xml_text_map_ref: MapRef = xml_text_ref.clone().into();
if let Any::Map(at) = xml_text_map_ref.to_json(txn) {
for (k, v) in at.iter() {
result.insert(k.to_string(), v.clone());
}
}
if let Some(xml_text_children) = xml_text_ref.successors() {
let mut children: Vec<Any> = vec![];
let mut child_result: HashMap<String, Any> = HashMap::new();
/* xml_text_children contains a sequence of ItemContent instances:
ItemContent::Type(YMap) => {"__type": "text", "__format": 0, "__style": "", "__mode": 0, "__detail": 0}
ItemContent::String(SplittableString) => "a"
ItemContent::String(SplittableString) => " "
...
ItemContent::Type(YMap) => {"__type": "text", "__format": 0, "__style": "", "__mode": 0, "__detail": 0}
ItemContent::String(SplittableString) => "b"
*/
for child in xml_text_children {
match &child {
ItemContent::Type(c) => {
let ptr = BranchPtr::from(c);
match ptr.type_ref() {
TYPE_REFS_MAP => {
if !child_result.is_empty() {
children.push(Any::Map(Box::new(child_result)));
child_result = HashMap::new();
}
if let Any::Map(at) = MapRef::from(ptr).to_json(txn) {
for (k, v) in at.iter() {
child_result.insert(k.to_string(), v.clone());
}
}
}
TYPE_REFS_XML_TEXT => {
let child_xml_text_ref = XmlTextRef::from(ptr);
if !child_result.is_empty() {
children.push(Any::Map(Box::new(child_result)));
child_result = HashMap::new();
}
children.push(process_xml_text_node(txn, &child_xml_text_ref));
}
_ => {
panic!("Unexpected type ref: {:?}", ptr.type_ref());
}
}
}
ItemContent::String(child_text_part) => {
if !child_result.is_empty() {
let mut child_text = child_result
.get("text")
.unwrap_or(&Any::String("".to_string().into()))
.to_string();
child_text.push_str(child_text_part.as_str());
child_result.insert("text".to_string(), Any::String(child_text.into()));
}
}
ItemContent::Deleted(_) => (),
_ => {
eprintln!("Ignored child of XmlTextRef: {:?}", child);
}
}
}
if !child_result.is_empty() {
children.push(Any::Map(Box::new(child_result)));
}
if !children.is_empty() {
result.insert(
"children".to_string(),
Any::Array(children.into_boxed_slice()),
);
}
}
Any::Map(Box::new(result))
}

pub fn process_xml_node(
txn: &TransactionMut<'static>,
result: &mut HashMap<String, Any>,
node: &XmlNode,
) {
fn set_xml_node_attributes(
txn: &TransactionMut<'static>,
result: &mut HashMap<String, Any>,
xml_node_map: &MapRef,
) {
if let Any::Map(at) = xml_node_map.to_json(txn) {
for (k, v) in at.iter() {
result.insert(k.to_string(), v.clone());
}
}
}

match node {
XmlNode::Text(text) => {
if let Any::Map(text_node_result) = process_xml_text_node(txn, &text) {
for (k, v) in text_node_result.iter() {
result.insert(k.to_string(), v.clone());
}
};
}
XmlNode::Fragment(fragment) => {
set_xml_node_attributes(txn, result, &fragment.clone().into());
if let Some(child_node) = fragment.first_child() {
let mut children: Vec<Any> = vec![];
let mut child_node_result: HashMap<String, Any> = HashMap::new();
process_xml_node(txn, &mut child_node_result, &child_node);
children.push(Any::Map(Box::new(child_node_result)));

match child_node.clone() {
XmlNode::Text(child_node_element) => {
for child_node in child_node_element.siblings(txn) {
let mut child_node_result: HashMap<String, Any> = HashMap::new();
process_xml_node(txn, &mut child_node_result, &child_node);
children.push(Any::Map(Box::new(child_node_result)));
}
}
XmlNode::Element(child_node_element) => {
for child_node in child_node_element.siblings(txn) {
let mut child_node_result: HashMap<String, Any> = HashMap::new();
process_xml_node(txn, &mut child_node_result, &child_node);
children.push(Any::Map(Box::new(child_node_result)));
}
}
_ => {
panic!("Unhandled XmlNode::Fragment child: {:?}", child_node)
}
}
result.insert(
"children".to_string(),
Any::Array(children.into_boxed_slice()),
);
}
}
XmlNode::Element(element) => {
set_xml_node_attributes(txn, result, &element.clone().into());
if let Some(child_node) = element.first_child() {
let mut children: Vec<Any> = vec![];
let mut child_node_result: HashMap<String, Any> = HashMap::new();
process_xml_node(txn, &mut child_node_result, &child_node);
children.push(Any::Map(Box::new(child_node_result)));

match child_node.clone() {
XmlNode::Text(child_node_element) => {
for child_node in child_node_element.siblings(txn) {
let mut child_node_result: HashMap<String, Any> = HashMap::new();
process_xml_node(txn, &mut child_node_result, &child_node);
children.push(Any::Map(Box::new(child_node_result)));
}
}
XmlNode::Element(child_node_element) => {
for child_node in child_node_element.siblings(txn) {
let mut child_node_result: HashMap<String, Any> = HashMap::new();
process_xml_node(txn, &mut child_node_result, &child_node);
children.push(Any::Map(Box::new(child_node_result)));
}
}
_ => {
panic!("Unhandled XmlNode::Fragment child: {:?}", child_node)
}
}
result.insert(
"children".to_string(),
Any::Array(children.into_boxed_slice()),
);
}
}
}
}

/// XML element data type. It represents an XML node, which can contain key-value attributes
/// (interpreted as strings) as well as other nested XML elements or rich text (represented by
/// `YXmlText` type).
Expand Down Expand Up @@ -181,10 +359,40 @@ impl YXmlElement {
format!("YXmlElement({})", self.__str__())
}

/// Converts contents of this `YXmlElement` instance into a Dict representation.
pub fn to_dict(&self) -> PyObject {
Python::with_gil(|py| {
self.0.with_transaction(|txn| {
let mut result: HashMap<String, Any> = HashMap::new();
process_xml_node(
txn.deref(),
&mut result,
&XmlNode::Element(self.0.inner.clone()),
);
result.into_py(py)
})
})
}

/// Sets a `name` and `value` as new attribute for this XML node. If an attribute with the same
/// `name` already existed on that node, its value with be overridden with a provided one.
pub fn set_attribute(&self, txn: &mut YTransaction, name: &str, value: &str) -> PyResult<()> {
txn.transact(|txn| self.0.insert_attribute(txn, name, value))
pub fn set_attribute(
&self,
txn: &mut YTransaction,
name: &str,
value: Py<PyAny>,
) -> PyResult<()> {
Python::with_gil(|py| {
let compatible_py_type_value: CompatiblePyType =
value.extract(py).unwrap_or_else(|err| {
err.restore(py);
CompatiblePyType::None
});
txn.transact(|txn| {
self.0
.insert_attribute(txn, name, Any::try_from(compatible_py_type_value).unwrap())
})
})
}

/// Returns a value of an attribute given its `name`. If no attribute with such name existed,
Expand Down Expand Up @@ -368,6 +576,42 @@ impl YXmlText {
})
}

/// Inserts a new instance of `YXmlText` as a child of this XML node and returns it.
pub fn insert_xml_text(&self, txn: &mut YTransaction, index: u32) -> PyResult<YXmlText> {
txn.transact(|txn| self._insert_xml_text(txn, index))
}
fn _insert_xml_text(&self, txn: &mut YTransactionInner, index: u32) -> YXmlText {
let inner_node = self.0.insert_embed(txn, index, XmlTextPrelim::new(""));
YXmlText::new(inner_node, self.0.doc.clone())
}

/// Appends a new instance of `YXmlText` as the last child of this XML node and returns it.
pub fn push_xml_text(&self, txn: &mut YTransaction) -> PyResult<YXmlText> {
txn.transact(|txn| self._push_xml_text(txn))
}
fn _push_xml_text(&self, txn: &mut YTransactionInner) -> YXmlText {
let index = self._len(txn) as u32;
self._insert_xml_text(txn, index)
}

/// Appends a new instance of `YMap` as the last child of this XML node.
pub fn push_attributes(&self, txn: &mut YTransaction, attributes: &PyDict) {
txn.transact(|txn| self._push_attributes(txn, attributes))
.unwrap();
}
fn _push_attributes(&self, txn: &mut YTransactionInner, attributes: &PyDict) {
let index = self._len(txn) as u32;
let mut map: HashMap<String, Any> = HashMap::new();
for (k, v) in attributes.iter() {
let compatible_py_type_value: CompatiblePyType = v.extract().unwrap();
map.insert(
k.to_string(),
Any::try_from(compatible_py_type_value).unwrap(),
);
}
self.0.push_attributes(txn, map);
}

/// Returns a parent `YXmlElement` node or `undefined` if current node has no parent assigned.
#[getter]
pub fn parent(&self) -> PyObject {
Expand All @@ -389,8 +633,23 @@ impl YXmlText {

/// Sets a `name` and `value` as new attribute for this XML node. If an attribute with the same
/// `name` already existed on that node, its value with be overridden with a provided one.
pub fn set_attribute(&self, txn: &mut YTransaction, name: &str, value: &str) -> PyResult<()> {
txn.transact(|txn| self.0.insert_attribute(txn, name, value))
pub fn set_attribute(
&self,
txn: &mut YTransaction,
name: &str,
value: Py<PyAny>,
) -> PyResult<()> {
Python::with_gil(|py| {
let compatible_py_type_value: CompatiblePyType =
value.extract(py).unwrap_or_else(|err| {
err.restore(py);
CompatiblePyType::None
});
txn.transact(|txn| {
self.0
.insert_attribute(txn, name, Any::try_from(compatible_py_type_value).unwrap())
})
})
}

/// Returns a value of an attribute given its `name`. If no attribute with such name existed,
Expand Down Expand Up @@ -577,6 +836,21 @@ impl YXmlFragment {
self.0.with_transaction(|txn| self.0.get_string(txn))
}

/// Converts contents of this `YXmlFragment` instance into a Dict representation.
pub fn to_dict(&self) -> PyObject {
Python::with_gil(|py| {
self.0.with_transaction(|txn| {
let mut result: HashMap<String, Any> = HashMap::new();
process_xml_node(
txn.deref(),
&mut result,
&XmlNode::Fragment(self.0.inner.clone()),
);
result.into_py(py)
})
})
}

/// Returns an iterator that enables a deep traversal of this XML node - starting from first
/// child over this XML node successors using depth-first strategy.
pub fn tree_walker(&self) -> YXmlTreeWalker {
Expand Down
Loading
Loading