From 7bea107293048cc6590072606597896bcb05489c Mon Sep 17 00:00:00 2001 From: Sebastian Imlay Date: Fri, 1 Mar 2024 21:49:07 -0500 Subject: [PATCH 01/23] Initial changes need for integration into liveview-client-swiftui --- .../Sources/LiveViewNativeCore/Support.swift | 134 ++++++++++++++++++ crates/core/src/diff/diff.rs | 36 ++--- crates/core/src/diff/patch.rs | 14 +- crates/core/src/dom/ffi.rs | 16 ++- crates/core/src/dom/mod.rs | 68 ++++----- crates/core/src/dom/node.rs | 92 +++++++++--- crates/core/src/dom/printer.rs | 10 +- crates/core/src/dom/select.rs | 4 +- crates/core/src/lib.rs | 4 - crates/core/tests/dom.rs | 8 +- crates/core/tests/parser.rs | 8 +- 11 files changed, 292 insertions(+), 102 deletions(-) create mode 100644 crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift diff --git a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift new file mode 100644 index 00000000..2f911f9c --- /dev/null +++ b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift @@ -0,0 +1,134 @@ +import Foundation +class SimpleHandler: DocumentChangeHandler { + func handle(_ context: String, _ changeType: ChangeType, _ nodeRef: NodeRef, _ parent: NodeRef?) { + } +} +public enum EventType { + /// When a document is modified in some way, the `changed` event is raised + case changed +} + +//public typealias Payload = [String: Any] +extension Document { + public subscript(ref: NodeRef) -> Node { + let data = self.get(ref) + return Node(self, ref, data) + } + public static func parseFragmentJson(payload: [String: Any]) throws -> Document { + let jsonData = try JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) + let payload = String(data: jsonData, encoding: .utf8)! + return try Document.parseFragmentJson(payload) + } + public func mergeFragmentJson( + _ payload: [String: Any] + //_ callback: @escaping (Document, NodeRef) -> () + ) throws { + let jsonData = try JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) + let payload = String(data: jsonData, encoding: .utf8)! + + // TODO: Fix this + let simple = SimpleHandler() + return try self.mergeFragmentJson(payload, simple) + } + + // TODO: Fix this + public func on(_ event: EventType, _ callback: @escaping (Document, NodeRef) -> ()) { + + //precondition(!self.handlers.keys.contains(event)) + //self.handlers[event] = callback + } +} + +extension AttributeName: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(namespace: .none, name: value) + } + public var rawValue: String { + if let namespace { + return "\(namespace):\(name)" + } else { + return name + } + } + public init?(rawValue: String) { + let parts = rawValue.split(separator: ":") + switch parts.count { + case 1: + self.name = rawValue + case 2: + self.namespace = String(parts[0]) + self.name = String(parts[1]) + default: + return nil + } + } +} +extension Node { + public func children() -> NodeChildrenSequence { + let children = self.getChildren() + return NodeChildrenSequence(doc: self.document(), slice: children, startIndex: children.startIndex, endIndex: children.endIndex) + } + public func depthFirstChildren() -> NodeDepthFirstChildrenSequence { + return NodeDepthFirstChildrenSequence(root: self) + } + public subscript(_ name: AttributeName) -> Attribute? { + let attributes = self.attributes() + return attributes.first { $0.name == name } + } +} +extension NodeRef: Hashable { + public static func == (lhs: NodeRef, rhs: NodeRef) -> Bool { + return lhs.ref() == rhs.ref() + } + public func hash(into hasher: inout Hasher) { + hasher.combine(ref()) + } +} + +public struct NodeChildrenSequence: Sequence, Collection, RandomAccessCollection { + //public typealias Element = Node + public typealias Element = Node + public typealias Index = Int + + let doc: Document + let slice: [NodeRef] + public let startIndex: Int + public let endIndex: Int + + public func index(after i: Int) -> Int { + i + 1 + } + public subscript(position: Int) -> Node { + doc[slice[startIndex + position]] + } +} +public struct NodeDepthFirstChildrenSequence: Sequence { + public typealias Element = Node + + let root: Node + + public func makeIterator() -> Iterator { + //return Iterator(children: [root.children().makeIterator()]) + return Iterator(children: []) + } + + public struct Iterator: IteratorProtocol { + public typealias Element = Node + + var children: [NodeChildrenSequence.Iterator] + + public mutating func next() -> Node? { + if !children.isEmpty { + if let node = children[children.count - 1].next() { + //children.append(node.children().makeIterator()) + return node + } else { + children.removeLast() + return self.next() + } + } else { + return nil + } + } + } +} diff --git a/crates/core/src/diff/diff.rs b/crates/core/src/diff/diff.rs index fb416eb7..0187f09d 100644 --- a/crates/core/src/diff/diff.rs +++ b/crates/core/src/diff/diff.rs @@ -31,7 +31,7 @@ impl<'a> Cursor<'a> { } } - fn node(&self) -> &Node { + fn node(&self) -> &NodeData { self.doc.get(self.node) } @@ -145,30 +145,30 @@ impl<'a> From<&'a Document> for Cursor<'a> { } impl<'a> Deref for Cursor<'a> { - type Target = Node; + type Target = NodeData; fn deref(&self) -> &Self::Target { self.node() } } -trait CompatibleWith: Deref { +trait CompatibleWith: Deref { fn is_compatible_with(&self, other: &T) -> bool where - T: Deref, + T: Deref, { match (self.deref(), other.deref()) { - (Node::NodeElement { element: from }, Node::NodeElement { element: to }) => { + (NodeData::NodeElement { element: from }, NodeData::NodeElement { element: to }) => { to.name.eq(&from.name) && to.id().eq(&from.id()) } - (Node::Leaf { value: _ }, Node::Leaf { value: _ }) => true, - (Node::Root, Node::Root) => true, + (NodeData::Leaf { value: _ }, NodeData::Leaf { value: _ }) => true, + (NodeData::Root, NodeData::Root) => true, _ => false, } } } -impl CompatibleWith for T where T: Deref {} +impl CompatibleWith for T where T: Deref {} #[derive(Debug)] enum Op<'a> { @@ -348,7 +348,7 @@ impl<'a> Iterator for Morph<'a> { ref to, } => { if cursor.next().is_some() { - if let Node::NodeElement { element: el } = cursor.node() { + if let NodeData::NodeElement { element: el } = cursor.node() { if let Some(id) = el.id() { if to.doc.get_by_id(id).is_some() { // Only detach if not previously moved @@ -510,37 +510,37 @@ impl<'a> Iterator for Morph<'a> { } match (from.node(), to.node()) { - (Node::Root, Node::Root) | (Node::Root, _) | (_, Node::Root) => { + (NodeData::Root, NodeData::Root) | (NodeData::Root, _) | (_, NodeData::Root) => { self.advance(Advance::BothCursors, false); } - (Node::Leaf { value: old_content }, Node::Leaf { value: content }) => { + (NodeData::Leaf { value: old_content }, NodeData::Leaf { value: content }) => { if old_content.ne(content) { self.queue.push(Op::Patch(Patch::Replace { node: from.node, - replacement: Node::Leaf { value: content.to_owned() }, + replacement: NodeData::Leaf { value: content.to_owned() }, })); } self.advance(Advance::BothCursors, false); } - (Node::Leaf { value: _ }, Node::NodeElement { element: _ }) => { + (NodeData::Leaf { value: _ }, NodeData::NodeElement { element: _ }) => { self.queue .push(Op::Patch(Patch::Remove { node: from.node })); self.advance(Advance::From, true); } - (Node::NodeElement { element: _ }, Node::Leaf { value: content }) => { + (NodeData::NodeElement { element: _ }, NodeData::Leaf { value: content }) => { self.queue.push(Op::Patch(Patch::InsertBefore { before: from.node, - node: Node::Leaf { value: content.to_owned() }, + node: NodeData::Leaf { value: content.to_owned() }, })); self.advance(Advance::To, true); } - (Node::NodeElement { element: from_el }, Node::NodeElement { element: to_el }) => { + (NodeData::NodeElement { element: from_el }, NodeData::NodeElement { element: to_el }) => { // nodes are compatible; morph attribute changes and continue if to_el.name.eq(&from_el.name) && to_el.id().eq(&from_el.id()) { - if from_el.attributes().ne(to_el.attributes()) { + if from_el.attributes.ne(&to_el.attributes) { self.queue.push(Op::Patch(Patch::SetAttributes { node: from.node, attributes: to.attributes().to_vec(), @@ -621,7 +621,7 @@ impl<'a> Iterator for Morph<'a> { // TODO: as an optimization, use peek to add node caching self.queue.push(Op::Patch(Patch::Replace { node: from.node, - replacement: Node::NodeElement { element: to_el.to_owned() }, + replacement: NodeData::NodeElement { element: to_el.to_owned() }, })); self.advance(Advance::BothCursors, false); diff --git a/crates/core/src/diff/patch.rs b/crates/core/src/diff/patch.rs index 58446095..46a5744f 100644 --- a/crates/core/src/diff/patch.rs +++ b/crates/core/src/diff/patch.rs @@ -5,20 +5,20 @@ use crate::dom::*; pub enum Patch { InsertBefore { before: NodeRef, - node: Node, + node: NodeData, }, InsertAfter { after: NodeRef, - node: Node, + node: NodeData, }, /// Creates `node` without attaching it to a parent /// It is expected to be pushed on the argument stack and popped off by subsequent ops Create { - node: Node, + node: NodeData, }, /// Same as `Create`, but also makes the new node the current node CreateAndMoveTo { - node: Node, + node: NodeData, }, /// Pushes the currently selected NodeRef on the stack, intended for use in conjunction with other stack-based ops PushCurrent, @@ -43,7 +43,7 @@ pub enum Patch { /// This is used in conjunction with `Move` to construct a subtree /// without modifying a Document, which is necessary when generating diffs Append { - node: Node, + node: NodeData, }, /// Pops an argument off the stack and appends it as the next sibling of `after` AppendAfter { @@ -52,14 +52,14 @@ pub enum Patch { /// Appends `node` to `parent` AppendTo { parent: NodeRef, - node: Node, + node: NodeData, }, Remove { node: NodeRef, }, Replace { node: NodeRef, - replacement: Node, + replacement: NodeData, }, /// Adds `attr` to the current node AddAttribute { diff --git a/crates/core/src/dom/ffi.rs b/crates/core/src/dom/ffi.rs index 768d46f8..5d595990 100644 --- a/crates/core/src/dom/ffi.rs +++ b/crates/core/src/dom/ffi.rs @@ -7,7 +7,7 @@ use std::{ }; pub use super::{ attribute::Attribute, - node::{Node, NodeRef}, + node::{NodeData, NodeRef}, printer::PrintOptions, DocumentChangeHandler, }; @@ -15,7 +15,7 @@ use crate::parser::ParseError; use crate::diff::fragment::RenderError; -#[derive(uniffi::Object)] +#[derive(Clone, uniffi::Object)] pub struct Document { inner: Arc>, } @@ -75,13 +75,23 @@ impl Document { pub fn get_attributes(&self, node_ref: Arc) -> Vec { self.inner.read().expect("Failed to get lock").attributes(*node_ref).to_vec() } - pub fn get(&self, node_ref: Arc) -> Node { + pub fn get(&self, node_ref: Arc) -> NodeData { self.inner.read().expect("Failed to get lock").get(*node_ref).clone() } pub fn render(&self) -> String { self.to_string() } } +impl Document { + pub fn print_node( + &self, + node: NodeRef, + writer: &mut dyn std::fmt::Write, + options: PrintOptions, + ) -> fmt::Result { + self.inner.read().expect("Failed to get lock").print_node(node, writer, options) + } +} impl fmt::Display for Document { #[inline] diff --git a/crates/core/src/dom/mod.rs b/crates/core/src/dom/mod.rs index d292dc2e..7f682b42 100644 --- a/crates/core/src/dom/mod.rs +++ b/crates/core/src/dom/mod.rs @@ -25,7 +25,7 @@ use smallvec::SmallVec; use self::printer::Printer; pub use self::{ attribute::{Attribute, AttributeName, AttributeValue}, - node::{Element, ElementName, Node, NodeRef}, + node::{Element, ElementName, NodeData, NodeRef}, printer::PrintOptions, select::{SelectionIter, Selector}, }; @@ -76,7 +76,7 @@ pub struct Document { /// The fragment template. pub fragment_template: Option>>, /// A map from node reference to node data - nodes: PrimaryMap, + nodes: PrimaryMap, /// A map from a node to its parent node, if it currently has one parents: SecondaryMap>, /// A map from a node to its child nodes @@ -92,7 +92,7 @@ impl fmt::Debug for Document { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use petgraph::dot::{Config, Dot}; let edge_getter = |_, edge_ref: EdgeRef| format!("{} to {}", edge_ref.from, edge_ref.to); - let node_getter = |_, node_ref: (NodeRef, &Node)| format!("id = {}", node_ref.0); + let node_getter = |_, node_ref: (NodeRef, &NodeData)| format!("id = {}", node_ref.0); let dot = Dot::with_attr_getters(self, &[Config::EdgeNoLabel], &edge_getter, &node_getter); write!(f, "{:?}", &dot) } @@ -125,7 +125,7 @@ impl Document { /// Creates a new empty Document, with preallocated capacity for nodes pub fn with_capacity(cap: usize) -> Self { let mut nodes = PrimaryMap::with_capacity(cap); - let root = nodes.push(Node::Root); + let root = nodes.push(NodeData::Root); Self { root, nodes, @@ -160,7 +160,7 @@ impl Document { /// Clears all data from this document, but keeps the allocated capacity, for more efficient reuse pub fn clear(&mut self) { self.nodes.clear(); - self.root = self.nodes.push(Node::Root); + self.root = self.nodes.push(NodeData::Root); self.parents.clear(); self.children.clear(); self.ids.clear(); @@ -191,34 +191,34 @@ impl Document { /// Returns the data associated with the given `NodeRef` #[inline] - pub fn get(&self, node: NodeRef) -> &Node { + pub fn get(&self, node: NodeRef) -> &NodeData { &self.nodes[node] } /// Returns the data associated with the given `NodeRef`, mutably #[inline] - pub fn get_mut(&mut self, node: NodeRef) -> &mut Node { + pub fn get_mut(&mut self, node: NodeRef) -> &mut NodeData { &mut self.nodes[node] } /// Returns the set of attribute refs associated with `node` - pub fn attributes(&self, node: NodeRef) -> &[Attribute] { + pub fn attributes(&self, node: NodeRef) -> Vec { match &self.nodes[node] { - Node::NodeElement { element: ref elem } => elem.attributes(), - _ => &[], + NodeData::NodeElement { element: ref elem } => elem.attributes.clone(), + _ => vec![], } } /// Returns the attribute `name` on `node`, otherwise `None` - pub fn get_attribute_by_name<'a, N: Into>( - &'a self, + pub fn get_attribute_by_name>( + &self, node: NodeRef, name: N, - ) -> Option<&'a Attribute> { + ) -> Option { let name = name.into(); self.attributes(node) .iter() - .find_map(|attr| if attr.name == name { Some(attr) } else { None }) + .find_map(|attr| if attr.name == name { Some(attr.clone()) } else { None }) } /// Returns the parent of `node`, if it has one @@ -270,13 +270,13 @@ impl Document { self.nodes.reserve(num_nodes); for (k, v) in doc.nodes.into_iter() { match v { - Node::Root => continue, - v @ Node::Leaf { value: _ } => { + NodeData::Root => continue, + v @ NodeData::Leaf { value: _ } => { let new_k = self.nodes.push(v); node_mapping.insert(k, new_k); } - Node::NodeElement { element: elem } => { - let new_k = self.nodes.push(Node::NodeElement { element: elem }); + NodeData::NodeElement { element: elem } => { + let new_k = self.nodes.push(NodeData::NodeElement { element: elem }); node_mapping.insert(k, new_k); } } @@ -425,7 +425,7 @@ impl Document { /// /// This operation adds `node` to the document without inserting it in the tree, i.e. it is initially detached #[inline] - pub fn push_node>(&mut self, node: N) -> NodeRef { + pub fn push_node>(&mut self, node: N) -> NodeRef { self.nodes.push(node.into()) } @@ -438,7 +438,7 @@ impl Document { name: K, value: V, ) -> bool { - if let Node::NodeElement { element: ref mut elem } = &mut self.nodes[node] { + if let NodeData::NodeElement { element: ref mut elem } = &mut self.nodes[node] { let name = name.into(); let value = value.into(); elem.set_attribute(name, value); @@ -450,7 +450,7 @@ impl Document { /// Removes the attribute `name` from `node`. pub fn remove_attribute>(&mut self, node: NodeRef, name: K) { - if let Node::NodeElement { element: ref mut elem } = &mut self.nodes[node] { + if let NodeData::NodeElement { element: ref mut elem } = &mut self.nodes[node] { let name = name.into(); elem.remove_attribute(&name); } @@ -462,7 +462,7 @@ impl Document { node: NodeRef, attributes: Vec, ) -> Option> { - if let Node::NodeElement { element: ref mut elem } = &mut self.nodes[node] { + if let NodeData::NodeElement { element: ref mut elem } = &mut self.nodes[node] { Some(mem::replace(&mut elem.attributes, attributes)) } else { None @@ -474,7 +474,7 @@ impl Document { where P: FnMut(&Attribute) -> bool, { - if let Node::NodeElement { element: ref mut elem } = &mut self.nodes[node] { + if let NodeData::NodeElement { element: ref mut elem } = &mut self.nodes[node] { elem.attributes.retain(predicate); } } @@ -676,7 +676,7 @@ pub trait DocumentBuilder { /// Returns the `Node` corresponding to the current insertion point #[inline] - fn current_node(&self) -> &Node { + fn current_node(&self) -> &NodeData { self.document().get(self.insertion_point()) } @@ -719,7 +719,7 @@ pub trait DocumentBuilder { } /// Creates a node, returning its NodeRef, without attaching it to the element tree - fn push_node>(&mut self, node: N) -> NodeRef { + fn push_node>(&mut self, node: N) -> NodeRef { self.document_mut().push_node(node.into()) } @@ -748,13 +748,13 @@ pub trait DocumentBuilder { /// Appends `node` as a child of the current node #[inline] - fn append>(&mut self, node: N) -> NodeRef { + fn append>(&mut self, node: N) -> NodeRef { let ip = self.insertion_point(); self.append_child(ip, node.into()) } /// Appends `node` as a child of `to` - fn append_child>(&mut self, to: NodeRef, node: N) -> NodeRef { + fn append_child>(&mut self, to: NodeRef, node: N) -> NodeRef { let doc = self.document_mut(); let nr = doc.nodes.push(node.into()); doc.append_child(to, nr); @@ -763,7 +763,7 @@ pub trait DocumentBuilder { /// Inserts `node`, returning its NodeRef, and making it the new insertion point #[inline] - fn insert>(&mut self, node: N) -> NodeRef { + fn insert>(&mut self, node: N) -> NodeRef { let ip = self.insertion_point(); let nr = self.insert_after(node, ip); self.set_insertion_point(ip); @@ -771,7 +771,7 @@ pub trait DocumentBuilder { } /// Inserts `node` as a sibling of `after`, immediately following it in the document - fn insert_after>(&mut self, node: N, after: NodeRef) -> NodeRef { + fn insert_after>(&mut self, node: N, after: NodeRef) -> NodeRef { let doc = self.document_mut(); let nr = doc.nodes.push(node.into()); doc.insert_after(nr, after); @@ -779,7 +779,7 @@ pub trait DocumentBuilder { } /// Inserts `node` as a sibling of `before`, immediately preceding it in the document - fn insert_before>(&mut self, node: N, before: NodeRef) -> NodeRef { + fn insert_before>(&mut self, node: N, before: NodeRef) -> NodeRef { let doc = self.document_mut(); let nr = doc.nodes.push(node.into()); doc.insert_before(nr, before); @@ -792,7 +792,7 @@ pub trait DocumentBuilder { } /// Replaces the content of `node` with `replacement` - fn replace>(&mut self, node: NodeRef, replacement: N) { + fn replace>(&mut self, node: NodeRef, replacement: N) { let replace = self.document_mut().get_mut(node); *replace = replacement.into(); } @@ -974,7 +974,7 @@ impl petgraph::visit::GraphProp for Document { type EdgeType = petgraph::Directed; } impl petgraph::visit::Data for Document { - type NodeWeight = Node; + type NodeWeight = NodeData; type EdgeWeight = (); } impl petgraph::visit::NodeCount for Document { @@ -994,7 +994,7 @@ impl<'a> petgraph::visit::IntoNodeIdentifiers for &'a Document { } } impl<'a> petgraph::visit::IntoNodeReferences for &'a Document { - type NodeRef = (NodeRef, &'a Node); + type NodeRef = (NodeRef, &'a NodeData); type NodeReferences = NodeReferences<'a>; #[inline] @@ -1110,7 +1110,7 @@ impl<'a> NodeReferences<'a> { } } impl<'a> Iterator for NodeReferences<'a> { - type Item = (NodeRef, &'a Node); + type Item = (NodeRef, &'a NodeData); fn next(&mut self) -> Option { match self.0.next() { diff --git a/crates/core/src/dom/node.rs b/crates/core/src/dom/node.rs index 45746090..c9af4aa4 100644 --- a/crates/core/src/dom/node.rs +++ b/crates/core/src/dom/node.rs @@ -1,10 +1,11 @@ use std::fmt; +use std::sync::Arc; use cranelift_entity::entity_impl; use petgraph::graph::{IndexType, NodeIndex}; use smallstr::SmallString; -use super::{Attribute, AttributeName}; +use super::{Attribute, AttributeName, ffi::Document as FFiDocument}; use crate::{InternedString, Symbol}; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, uniffi::Object)] @@ -53,7 +54,7 @@ unsafe impl IndexType for NodeRef { /// This enum represents the valid node types of a `Document` tree #[derive(Debug, Clone, PartialEq, uniffi::Enum)] -pub enum Node { +pub enum NodeData { /// A marker node that indicates the root of a document /// /// A document may only have a single root, and it has no attributes @@ -63,22 +64,66 @@ pub enum Node { /// A leaf node is an untyped node, typically text, and does not have any attributes or children Leaf { value: String }, } + +#[derive(Clone, uniffi::Object)] +pub struct Node { + pub document: FFiDocument, + pub id: NodeRef, + pub data: NodeData, +} +impl std::fmt::Display for Node { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.document.print_node(self.id, f, crate::dom::PrintOptions::Pretty) + } +} + +#[uniffi::export] impl Node { - /// Creates a new, empty element node with the given tag name - #[inline] - pub fn new>(tag: T) -> Self { - Self::NodeElement { element: Element::new(tag.into()) } + #[uniffi::constructor] + pub fn new(document: &FFiDocument, id: &NodeRef, data: NodeData) -> Self { + Self { + document: document.clone(), + id: id.clone(), + data, + } } + pub fn get_children(&self) -> Vec> { + self.document.children(self.id.into()) + /* + self.document.children(self.id.into()).iter().map(|id| { + let document = self.document.clone(); + let data = document.get(id.clone()); + Arc::new(Self::new(&document, id, data)) + }).collect() + */ + } + pub fn document(&self) -> FFiDocument { + self.document.clone() + } + pub fn id(&self) -> NodeRef { + self.id + } + pub fn data(&self) -> NodeData { + self.data.clone() + } + pub fn attributes(&self) -> Vec { + self.data.attributes() + } + pub fn to_string(&self) -> String { + todo!(); + } +} +impl NodeData { /// Returns a slice of Attributes for this node, if applicable - pub fn attributes(&self) -> &[Attribute] { + pub fn attributes(&self) -> Vec { match self { - Self::NodeElement { element: elem } => elem.attributes(), - _ => &[], + Self::NodeElement { element: elem } => elem.attributes.clone(), + _ => vec![], } } - pub(crate) fn id(&self) -> Option> { + pub fn id(&self) -> Option { match self { Self::NodeElement { element: el } => el.id(), _ => None, @@ -93,25 +138,33 @@ impl Node { } } } -impl From for Node { +impl NodeData { + /// Creates a new, empty element node with the given tag name + #[inline] + pub fn new>(tag: T) -> Self { + Self::NodeElement { element: Element::new(tag.into()) } + } +} + +impl From for NodeData { #[inline(always)] fn from(elem: Element) -> Self { Self::NodeElement { element: elem } } } -impl From<&str> for Node { +impl From<&str> for NodeData { #[inline(always)] fn from(string: &str) -> Self { Self::Leaf { value: string.to_string() } } } -impl From for Node { +impl From for NodeData { #[inline(always)] fn from(string: String) -> Self { Self::Leaf { value: string } } } -impl From> for Node { +impl From> for NodeData { #[inline(always)] fn from(string: SmallString<[u8; 16]>) -> Self { Self::Leaf { value: string.to_string() } @@ -207,13 +260,10 @@ impl Element { } } - pub(crate) fn id(&self) -> Option> { + pub(crate) fn id(&self) -> Option { for attr in &self.attributes { if attr.name.eq("id") { - if let Some(value) = &attr.value { - let value = SmallString::<[u8; 16]>::from_string(value.clone()); - return Some(value); - } + return attr.value.clone(); } } None @@ -221,8 +271,8 @@ impl Element { /// Returns a slice of AttributeRefs associated to this element #[inline] - pub fn attributes(&self) -> &[Attribute] { - self.attributes.as_slice() + pub fn attributes(&self) -> Vec { + self.attributes.clone() } /// Sets the attribute named `name` on this element. diff --git a/crates/core/src/dom/printer.rs b/crates/core/src/dom/printer.rs index 745a846d..cc448d44 100644 --- a/crates/core/src/dom/printer.rs +++ b/crates/core/src/dom/printer.rs @@ -1,6 +1,6 @@ use std::fmt; -use super::{Document, Node, NodeRef}; +use super::{Document, NodeData, NodeRef}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum PrintOptions { @@ -42,7 +42,7 @@ impl<'a> Printer<'a> { DfsEvent::Discover(node, _) => { // We're encountering `node` for the first time match &self.doc.nodes[node] { - Node::NodeElement { element: elem } => { + NodeData::NodeElement { element: elem } => { let pretty = self.options.pretty(); let self_closing = self.doc.children[node].is_empty(); if pretty { @@ -69,7 +69,7 @@ impl<'a> Printer<'a> { writer.write_str(">") } } - Node::Leaf { value: content } => { + NodeData::Leaf { value: content } => { if self.options.pretty() { if !first { writer.write_char('\n')?; @@ -80,12 +80,12 @@ impl<'a> Printer<'a> { } writer.write_str(content.as_str()) } - Node::Root => Ok(()), + NodeData::Root => Ok(()), } } DfsEvent::Finish(node, _) => { // We've visited all the children of `node` - if let Node::NodeElement { element: elem } = &self.doc.nodes[node] { + if let NodeData::NodeElement { element: elem } = &self.doc.nodes[node] { let self_closing = self.doc.children[node].is_empty(); if self_closing { return Ok(()); diff --git a/crates/core/src/dom/select.rs b/crates/core/src/dom/select.rs index cbf78759..07101a58 100644 --- a/crates/core/src/dom/select.rs +++ b/crates/core/src/dom/select.rs @@ -48,8 +48,8 @@ impl<'a> Selector<'a> { /// Checks if the given node matches this selector pub fn matches(&self, node: NodeRef, document: &Document) -> bool { let element = match &document.nodes[node] { - Node::NodeElement { element: ref elem } => elem, - Node::Leaf { value: _ } | Node::Root => return false, + NodeData::NodeElement { element: ref elem } => elem, + NodeData::Leaf { value: _ } | NodeData::Root => return false, }; match self { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index e18bcf53..caed0d11 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,9 +1,5 @@ -#![feature(allocator_api)] #![feature(slice_take)] #![feature(assert_matches)] -#![feature(exact_size_is_empty)] -#![feature(vec_into_raw_parts)] -#![feature(c_unwind)] pub mod diff; diff --git a/crates/core/tests/dom.rs b/crates/core/tests/dom.rs index aaedd553..c9219798 100644 --- a/crates/core/tests/dom.rs +++ b/crates/core/tests/dom.rs @@ -8,20 +8,20 @@ fn dom_builder_example() { let mut builder = Document::build(); // Start creating a new document rooted at the given node - let html = builder.append(Node::new("html")); + let html = builder.append(NodeData::new("html")); builder.set_insertion_point(html); // Apply an attribute to the html node builder.set_attribute("lang", "en".to_string()); // Insert a new node and move the insertion point to that node - let head = builder.append(Node::new("head")); + let head = builder.append(NodeData::new("head")); builder.set_insertion_point(head); // Use of insertion guards { let mut guard = builder.insert_guard(); - let meta = guard.append(Node::new("meta")); + let meta = guard.append(NodeData::new("meta")); guard.set_insertion_point(meta); guard.set_attribute("charset", "utf-8".to_string()); } @@ -29,7 +29,7 @@ fn dom_builder_example() { assert_eq!(builder.insertion_point(), head); // Insert a node after another node, regardless of where the builder is currently positioned - let body = builder.insert_after(Node::new("body"), head); + let body = builder.insert_after(NodeData::new("body"), head); builder.set_insertion_point(body); builder.set_attribute("class", "main".to_string()); diff --git a/crates/core/tests/parser.rs b/crates/core/tests/parser.rs index ab651ad8..d535d2d8 100644 --- a/crates/core/tests/parser.rs +++ b/crates/core/tests/parser.rs @@ -3,7 +3,7 @@ use std::assert_matches::assert_matches; use liveview_native_core::{ - dom::{AttributeName, Node}, + dom::{AttributeName, NodeData}, parser, InternedString, }; @@ -50,8 +50,8 @@ fn parser_whitespace_handling() { let children = document.children(body); assert_eq!(children.len(), 1); let content = document.get(children[0]); - assert_matches!(content, Node::Leaf {..}); - let Node::Leaf { value: content } = content else { + assert_matches!(content, NodeData::Leaf {..}); + let NodeData::Leaf { value: content } = content else { unreachable!() }; assert_eq!(content.as_str(), "some content"); @@ -65,7 +65,7 @@ fn parser_preserve_upcase() { let root = document.root(); let component = document.children(root)[0]; let element = document.get(component); - let Node::NodeElement { element } = element else { + let NodeData::NodeElement { element } = element else { panic!("expected element"); }; let expected_name: InternedString = "Component".into(); From 71fee7c974f1aaaaa238d71f41f9a71ee3805212 Mon Sep 17 00:00:00 2001 From: Sebastian Imlay Date: Sat, 9 Mar 2024 01:01:22 -0500 Subject: [PATCH 02/23] More fixes and cleanups --- .../Sources/LiveViewNativeCore/Support.swift | 40 +++++++++----- crates/core/src/dom/ffi.rs | 20 ++++++- crates/core/src/dom/mod.rs | 54 ++++++++++--------- crates/core/src/dom/node.rs | 12 +---- 4 files changed, 75 insertions(+), 51 deletions(-) diff --git a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift index 2f911f9c..195b6e0b 100644 --- a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift +++ b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift @@ -1,14 +1,27 @@ import Foundation + class SimpleHandler: DocumentChangeHandler { - func handle(_ context: String, _ changeType: ChangeType, _ nodeRef: NodeRef, _ parent: NodeRef?) { + var callback: (Document, NodeRef) -> () + init ( + _ callback: @escaping (Document, NodeRef) -> () + ) { + self.callback = callback + } + func handle(_ context: Document, _ changeType: ChangeType, _ node: NodeRef, _ parent: NodeRef?) { + switch changeType { + case .add: + self.callback(context, parent!) + case .remove: + self.callback(context, parent!) + case .change: + self.callback(context, node) + case .replace: + self.callback(context, parent!) + } } -} -public enum EventType { - /// When a document is modified in some way, the `changed` event is raised - case changed + } -//public typealias Payload = [String: Any] extension Document { public subscript(ref: NodeRef) -> Node { let data = self.get(ref) @@ -26,16 +39,13 @@ extension Document { let jsonData = try JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) let payload = String(data: jsonData, encoding: .utf8)! - // TODO: Fix this - let simple = SimpleHandler() - return try self.mergeFragmentJson(payload, simple) + return try self.mergeFragmentJson(payload) } - // TODO: Fix this public func on(_ event: EventType, _ callback: @escaping (Document, NodeRef) -> ()) { - //precondition(!self.handlers.keys.contains(event)) - //self.handlers[event] = callback + let simple = SimpleHandler(callback) + self.setEventHandler(simple) } } @@ -63,6 +73,7 @@ extension AttributeName: ExpressibleByStringLiteral { } } } + extension Node { public func children() -> NodeChildrenSequence { let children = self.getChildren() @@ -75,7 +86,11 @@ extension Node { let attributes = self.attributes() return attributes.first { $0.name == name } } + public func toString() -> String { + return self.display() + } } + extension NodeRef: Hashable { public static func == (lhs: NodeRef, rhs: NodeRef) -> Bool { return lhs.ref() == rhs.ref() @@ -86,7 +101,6 @@ extension NodeRef: Hashable { } public struct NodeChildrenSequence: Sequence, Collection, RandomAccessCollection { - //public typealias Element = Node public typealias Element = Node public typealias Index = Int diff --git a/crates/core/src/dom/ffi.rs b/crates/core/src/dom/ffi.rs index 5d595990..088a6a28 100644 --- a/crates/core/src/dom/ffi.rs +++ b/crates/core/src/dom/ffi.rs @@ -20,6 +20,14 @@ pub struct Document { inner: Arc>, } +impl From for Document { + fn from(doc: super::Document) -> Self { + Self { + inner: Arc::new(RwLock::new(doc)) + } + } +} + #[uniffi::export] impl Document { #[uniffi::constructor] @@ -47,14 +55,22 @@ impl Document { inner })) } + pub fn set_event_handler( + &self, + handler: Box + ) { + if let Ok(mut inner) = self.inner.write() { + inner.event_callback = Some(Arc::from(handler)); + } + } + pub fn merge_fragment_json( &self, json: String, - handler: Box ) -> Result<(), RenderError> { if let Ok(mut inner) = self.inner.write() { - Ok(inner.merge_fragment_json(json, handler)?) + Ok(inner.merge_fragment_json(json)?) } else { unimplemented!("The error case for when we cannot get the lock for the Document has not been finished yet"); } diff --git a/crates/core/src/dom/mod.rs b/crates/core/src/dom/mod.rs index 7f682b42..fd81cc13 100644 --- a/crates/core/src/dom/mod.rs +++ b/crates/core/src/dom/mod.rs @@ -6,13 +6,7 @@ mod ffi; use std::{ collections::{BTreeMap, VecDeque}, - fmt, mem, - ops::{Deref, DerefMut}, - path::Path, - sync::{ - Arc, - Mutex, - } + fmt, mem, ops::{Deref, DerefMut}, path::Path, sync::Arc, }; use cranelift_entity::{packed_option::PackedOption, EntityRef, PrimaryMap, SecondaryMap}; @@ -74,7 +68,8 @@ use crate::diff::PatchResult; pub struct Document { root: NodeRef, /// The fragment template. - pub fragment_template: Option>>, + pub fragment_template: Option, + pub event_callback: Option>, /// A map from node reference to node data nodes: PrimaryMap, /// A map from a node to its parent node, if it currently has one @@ -133,6 +128,7 @@ impl Document { children: SecondaryMap::new(), ids: Default::default(), fragment_template: None, + event_callback: None, } } @@ -497,14 +493,12 @@ impl Document { pub fn merge_fragment(&mut self, root_diff: RootDiff) -> Result { let root = if let Some(root) = &self.fragment_template { - let mut root = root.as_ref().lock().unwrap(); - *root = root.clone().merge(root_diff)?; - root.clone() + root.clone().merge(root_diff)? } else { let new_root : Root = root_diff.try_into()?; - self.fragment_template = Some(Arc::new(Mutex::new(new_root.clone()))); new_root }; + self.fragment_template = Some(root.clone()); Ok(root) } @@ -516,25 +510,24 @@ impl Document { let root : Root = fragment.try_into()?; let rendered : String = root.clone().try_into()?; let mut document = crate::parser::parse(&rendered)?; - document.fragment_template = Some(Arc::new(Mutex::new(root))); + document.fragment_template = Some(root); Ok(document) } + pub fn merge_fragment_json( &mut self, json: String, - handler: Box ) -> Result<(), RenderError> { let fragment: RootDiff = serde_json::from_str(&json).map_err(|e| RenderError::from(e))?; let root = if let Some(root) = &self.fragment_template { - let mut root = root.as_ref().lock().unwrap(); - *root = root.clone().merge(fragment)?; - root.clone() + root.clone().merge(fragment)? } else { - self.fragment_template = Some(Arc::new(Mutex::new(fragment.try_into()?))); + self.fragment_template = Some(fragment.try_into()?); return Ok(()); }; + self.fragment_template = Some(root.clone()); let rendered_root : String = root.clone().try_into()?; let new_doc = Self::parse(rendered_root)?; @@ -543,33 +536,38 @@ impl Document { if patches.is_empty() { return Ok(()); } + let handler = if let Some(handler) = self.event_callback.clone() { + handler + } else { + return Ok(()) + }; let mut editor = self.edit(); let mut stack = vec![]; for patch in patches.into_iter() { let patch_result = patch.apply(&mut editor, &mut stack); // TODO: Use the actual context - let context = String::new(); + let context = ffi::Document::from(new_doc.clone()); match patch_result { None => (), Some(PatchResult::Add { node, parent }) => { - handler.handle(context, ChangeType::Add, node.into(), Some(parent.into())); + handler.handle(context.into(), ChangeType::Add, node.into(), Some(parent.into())); } Some(PatchResult::Remove { node, parent }) => { handler.handle( - context, + context.into(), ChangeType::Remove, node.into(), Some(parent.into()), ); } Some(PatchResult::Change { node }) => { - handler.handle(context, ChangeType::Change, node.into(), None); + handler.handle(context.into(), ChangeType::Change, node.into(), None); } Some(PatchResult::Replace { node, parent }) => { handler.handle( - context, + context.into(), ChangeType::Replace, node.into(), Some(parent.into()), @@ -580,7 +578,6 @@ impl Document { editor.finish(); Ok(()) } - } #[repr(C)] @@ -592,11 +589,16 @@ pub enum ChangeType { Replace = 3, } +#[derive(Copy, Clone, uniffi::Enum)] +pub enum EventType { + Changed, // { change: ChangeType }, +} + #[uniffi::export(callback_interface)] -pub trait DocumentChangeHandler { +pub trait DocumentChangeHandler : Send + Sync { fn handle( &self, - context: String, + doc: Arc, change_type: ChangeType, node_ref: Arc, parent: Option>, diff --git a/crates/core/src/dom/node.rs b/crates/core/src/dom/node.rs index c9af4aa4..16a5f320 100644 --- a/crates/core/src/dom/node.rs +++ b/crates/core/src/dom/node.rs @@ -89,14 +89,6 @@ impl Node { } pub fn get_children(&self) -> Vec> { self.document.children(self.id.into()) - /* - self.document.children(self.id.into()).iter().map(|id| { - let document = self.document.clone(); - let data = document.get(id.clone()); - - Arc::new(Self::new(&document, id, data)) - }).collect() - */ } pub fn document(&self) -> FFiDocument { self.document.clone() @@ -110,8 +102,8 @@ impl Node { pub fn attributes(&self) -> Vec { self.data.attributes() } - pub fn to_string(&self) -> String { - todo!(); + pub fn display(&self) -> String { + format!("{self}") } } impl NodeData { From 193980b397c04604d04777af655ede69e9b89337 Mon Sep 17 00:00:00 2001 From: Sebastian Imlay Date: Wed, 13 Mar 2024 15:27:23 -0400 Subject: [PATCH 03/23] Minor fixes --- .../Sources/LiveViewNativeCore/Support.swift | 8 +++----- crates/core/src/dom/ffi.rs | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift index 195b6e0b..7bceeaac 100644 --- a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift +++ b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift @@ -34,7 +34,6 @@ extension Document { } public func mergeFragmentJson( _ payload: [String: Any] - //_ callback: @escaping (Document, NodeRef) -> () ) throws { let jsonData = try JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) let payload = String(data: jsonData, encoding: .utf8)! @@ -113,7 +112,7 @@ public struct NodeChildrenSequence: Sequence, Collection, RandomAccessCollection i + 1 } public subscript(position: Int) -> Node { - doc[slice[startIndex + position]] + return doc[slice[startIndex + position]] } } public struct NodeDepthFirstChildrenSequence: Sequence { @@ -122,8 +121,7 @@ public struct NodeDepthFirstChildrenSequence: Sequence { let root: Node public func makeIterator() -> Iterator { - //return Iterator(children: [root.children().makeIterator()]) - return Iterator(children: []) + return Iterator(children: [root.children().makeIterator()]) } public struct Iterator: IteratorProtocol { @@ -134,7 +132,7 @@ public struct NodeDepthFirstChildrenSequence: Sequence { public mutating func next() -> Node? { if !children.isEmpty { if let node = children[children.count - 1].next() { - //children.append(node.children().makeIterator()) + children.append(node.children().makeIterator()) return node } else { children.removeLast() diff --git a/crates/core/src/dom/ffi.rs b/crates/core/src/dom/ffi.rs index 088a6a28..ad8ec0c0 100644 --- a/crates/core/src/dom/ffi.rs +++ b/crates/core/src/dom/ffi.rs @@ -64,7 +64,6 @@ impl Document { } } - pub fn merge_fragment_json( &self, json: String, From 021ca156d7c5cea5080db09b11855bd72b50dd61 Mon Sep 17 00:00:00 2001 From: Sebastian Imlay Date: Thu, 14 Mar 2024 19:08:22 -0400 Subject: [PATCH 04/23] Fix swift tests --- .../Sources/LiveViewNativeCore/Support.swift | 7 ++++--- .../LiveViewNativeCoreTests.swift | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift index 7bceeaac..cf832581 100644 --- a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift +++ b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift @@ -76,7 +76,7 @@ extension AttributeName: ExpressibleByStringLiteral { extension Node { public func children() -> NodeChildrenSequence { let children = self.getChildren() - return NodeChildrenSequence(doc: self.document(), slice: children, startIndex: children.startIndex, endIndex: children.endIndex) + return NodeChildrenSequence(doc: self.document(), slice: children) } public func depthFirstChildren() -> NodeDepthFirstChildrenSequence { return NodeDepthFirstChildrenSequence(root: self) @@ -105,8 +105,9 @@ public struct NodeChildrenSequence: Sequence, Collection, RandomAccessCollection let doc: Document let slice: [NodeRef] - public let startIndex: Int - public let endIndex: Int + public var startIndex: Int { 0 } + + public var endIndex: Int { self.slice.count } public func index(after i: Int) -> Int { i + 1 diff --git a/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift b/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift index ac187c6d..2c0d208e 100644 --- a/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift +++ b/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift @@ -1,7 +1,7 @@ import XCTest @testable import LiveViewNativeCore class SimpleHandler: DocumentChangeHandler { - func handle(_ context: String, _ changeType: ChangeType, _ nodeRef: NodeRef, _ parent: NodeRef?) { + func handle(_ doc: Document, _ changeType: ChangeType, _ nodeRef: NodeRef, _ parent: NodeRef?) { } } @@ -50,7 +50,9 @@ final class LiveViewNativeCoreTests: XCTestCase { ] } """ + let simple = SimpleHandler() let initial_document = try Document.parseFragmentJson(initial_json) + initial_document.setEventHandler(simple) let initial_rendered = initial_document.render() var expected = """ @@ -110,8 +112,7 @@ final class LiveViewNativeCoreTests: XCTestCase { } } """ - let simple = SimpleHandler() - try initial_document.mergeFragmentJson(first_increment, simple) + try initial_document.mergeFragmentJson(first_increment) let second_render = initial_document.render() expected = """ @@ -182,7 +183,7 @@ final class LiveViewNativeCoreTests: XCTestCase { } } """ - try initial_document.mergeFragmentJson(second_increment, simple) + try initial_document.mergeFragmentJson(second_increment) let third_render = initial_document.render() expected = """ From 10e44d76bb2a30e2dae2bbf35e4f7c4b941bcd88 Mon Sep 17 00:00:00 2001 From: Sebastian Imlay Date: Fri, 15 Mar 2024 16:33:54 -0400 Subject: [PATCH 05/23] Fix kotlin tests --- .../org/phoenixframework/liveview_jetpack/DocumentTest.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/core/liveview-native-core-jetpack/core/src/test/java/org/phoenixframework/liveview_jetpack/DocumentTest.kt b/crates/core/liveview-native-core-jetpack/core/src/test/java/org/phoenixframework/liveview_jetpack/DocumentTest.kt index c7dcaa0a..598bc1c2 100644 --- a/crates/core/liveview-native-core-jetpack/core/src/test/java/org/phoenixframework/liveview_jetpack/DocumentTest.kt +++ b/crates/core/liveview-native-core-jetpack/core/src/test/java/org/phoenixframework/liveview_jetpack/DocumentTest.kt @@ -36,7 +36,7 @@ class SimpleChangeHandler: DocumentChangeHandler { } override fun `handle`( - `context`: String, + `context`: Document, `changeType`: ChangeType, `nodeRef`: NodeRef, `optionNodeRef`: NodeRef?, @@ -141,8 +141,9 @@ class DocumentTest { } } """ - var simple = SimpleChangeHandler() - doc.mergeFragmentJson(first_increment, simple); + var simple = SimpleChangeHandler(); + doc.setEventHandler(simple); + doc.mergeFragmentJson(first_increment); rendered = doc.render(); expected = """ \n \n Static Text \n Counter 1: ", + " \n Counter 2: ", + " \n", + "\n" + ] + } + """ + doc.mergeFragmentJson(input) + var expected = """ + + + + Static Text + + + Counter 1: 0 + + + Counter 2: 0 + +""" + var rendered = doc.render(); + assertEquals(expected, rendered) + } @Test fun json_merging() { diff --git a/crates/core/src/dom/mod.rs b/crates/core/src/dom/mod.rs index fd81cc13..caaa362a 100644 --- a/crates/core/src/dom/mod.rs +++ b/crates/core/src/dom/mod.rs @@ -491,17 +491,6 @@ impl Document { printer.print(writer) } - pub fn merge_fragment(&mut self, root_diff: RootDiff) -> Result { - let root = if let Some(root) = &self.fragment_template { - root.clone().merge(root_diff)? - } else { - let new_root : Root = root_diff.try_into()?; - new_root - }; - self.fragment_template = Some(root.clone()); - Ok(root) - } - /// Parses a `RootDiff` and returns a `Document` pub fn parse_fragment_json( input: String, @@ -524,23 +513,21 @@ impl Document { let root = if let Some(root) = &self.fragment_template { root.clone().merge(fragment)? } else { - self.fragment_template = Some(fragment.try_into()?); - return Ok(()); + fragment.try_into()? + //self.fragment_template = Some(fragment.try_into()?); + //return Ok(()); }; self.fragment_template = Some(root.clone()); let rendered_root : String = root.clone().try_into()?; + log::debug!("Rendered root: {rendered_root}"); let new_doc = Self::parse(rendered_root)?; let patches = crate::diff::diff(self, &new_doc); if patches.is_empty() { return Ok(()); } - let handler = if let Some(handler) = self.event_callback.clone() { - handler - } else { - return Ok(()) - }; + let handler = self.event_callback.clone(); let mut editor = self.edit(); let mut stack = vec![]; @@ -552,26 +539,34 @@ impl Document { match patch_result { None => (), Some(PatchResult::Add { node, parent }) => { - handler.handle(context.into(), ChangeType::Add, node.into(), Some(parent.into())); + if let Some(ref handler) = handler { + handler.handle(context.into(), ChangeType::Add, node.into(), Some(parent.into())); + } } Some(PatchResult::Remove { node, parent }) => { - handler.handle( - context.into(), - ChangeType::Remove, - node.into(), - Some(parent.into()), - ); + if let Some(ref handler) = handler { + handler.handle( + context.into(), + ChangeType::Remove, + node.into(), + Some(parent.into()), + ); + } } Some(PatchResult::Change { node }) => { - handler.handle(context.into(), ChangeType::Change, node.into(), None); + if let Some(ref handler) = handler { + handler.handle(context.into(), ChangeType::Change, node.into(), None); + } } Some(PatchResult::Replace { node, parent }) => { - handler.handle( - context.into(), - ChangeType::Replace, - node.into(), - Some(parent.into()), - ); + if let Some(ref handler) = handler { + handler.handle( + context.into(), + ChangeType::Replace, + node.into(), + Some(parent.into()), + ); + } } } } From 4ae4a03e03422de71a639f218faff9cbeadd767d Mon Sep 17 00:00:00 2001 From: Sebastian Imlay Date: Thu, 4 Apr 2024 12:17:23 -0400 Subject: [PATCH 09/23] More refactors --- Cargo.toml | 2 +- Makefile.toml | 104 ++++-------------- crates/core/Cargo.toml | 11 +- .../core/build.gradle.kts | 4 +- crates/core/src/bin/uniffi-bindgen.rs | 3 - 5 files changed, 30 insertions(+), 94 deletions(-) delete mode 100644 crates/core/src/bin/uniffi-bindgen.rs diff --git a/Cargo.toml b/Cargo.toml index ade1a79c..f91609a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" members = [ - "crates/core", + "crates/core", "crates/uniffi-bindgen", ] [workspace.package] diff --git a/Makefile.toml b/Makefile.toml index dfa5c372..e9fa580a 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -127,7 +127,7 @@ args = [ "${CARGO_TARGET_DIR}/universal/tvos-sim/", ] -[tasks.build-macos] +[tasks.build-apple-std-targets] workspace = false category = "Build" description = "Compiles for all targets needed to produce a universal library for macOS" @@ -138,62 +138,38 @@ args = [ "@@remove-empty(CARGO_MAKE_CARGO_VERBOSE_FLAGS)", "--target", "aarch64-apple-darwin", "--target", "x86_64-apple-darwin", - "-p", "liveview-native-core" -] -dependencies = ["install-targets"] -[tasks.build-ios] -workspace = false -category = "Build" -description = "Compiles for all targets needed to produce a universal library for iOS" -command = "rustup" -args = [ - "run", "${CARGO_MAKE_TOOLCHAIN}", - "cargo", "build", - "@@remove-empty(CARGO_MAKE_CARGO_VERBOSE_FLAGS)", "--target", "aarch64-apple-ios", "--target", "aarch64-apple-ios-sim", - "--target", "x86_64-apple-ios", "-p", - "liveview-native-core" + "--target", "x86_64-apple-ios", + "-p", "liveview-native-core" ] dependencies = ["install-targets"] -[tasks.build-watchos] + +[tasks.build-apple-no-std-targets] workspace = false category = "Build" -description = "Compiles for all targets needed to produce a universal library for watchOS" +description = "Compiles for all targets needed to produce a universal library for watchOS and tvOS" command = "rustup" args = [ "run", "${CARGO_MAKE_TOOLCHAIN}", "cargo", "build", "@@remove-empty(CARGO_MAKE_CARGO_VERBOSE_FLAGS)", "-Z", "build-std", + "--target", "arm64_32-apple-watchos", "--target", "aarch64-apple-watchos-sim", "--target", "x86_64-apple-watchos-sim", - "-p", "liveview-native-core" -] -dependencies = ["install-targets"] -[tasks.build-tvos] -workspace = false -category = "Build" -description = "Compiles for all targets needed to produce a universal library for tvOS" -command = "rustup" -args = [ - "run", "${CARGO_MAKE_TOOLCHAIN}", - "cargo", "build", - "@@remove-empty(CARGO_MAKE_CARGO_VERBOSE_FLAGS)", - "-Z", "build-std", "--target", "aarch64-apple-tvos", "--target", "aarch64-apple-tvos-sim", "--target", "x86_64-apple-tvos", "-p", "liveview-native-core" ] -dependencies = ["install-targets"] [tasks.lipo-macos] -dependencies = ["create-lipo-universal-directories", "build-macos"] +dependencies = ["create-lipo-universal-directories", "build-apple-std-targets"] workspace = false category = "Build" description = "Combines macOS targets into a universal binary" @@ -201,12 +177,12 @@ command = "xcrun" args = [ "lipo", "-create", "${CARGO_TARGET_DIR}/aarch64-apple-darwin/debug/libliveview_native_core.a", - "${CARGO_TARGET_DIR}/x86_64-apple-darwin/debug/libliveview_native_core.a", + "${CARGO_TARGET_DIR}/x86_64-apple-darwin/debug/libliveview_native_core.a", "-output", "${CARGO_TARGET_DIR}/universal/macos/libliveview_native_core.a" ] [tasks.lipo-ios-sim] -dependencies = ["create-lipo-universal-directories", "build-ios"] +dependencies = ["create-lipo-universal-directories", "build-apple-std-targets"] workspace = false category = "Build" description = "Combines iOS simulator targets into a universal binary" @@ -215,12 +191,12 @@ args = [ "lipo", "-create", "${CARGO_TARGET_DIR}/aarch64-apple-ios-sim/debug/libliveview_native_core.a", - "${CARGO_TARGET_DIR}/x86_64-apple-ios/debug/libliveview_native_core.a", + "${CARGO_TARGET_DIR}/x86_64-apple-ios/debug/libliveview_native_core.a", "-output", "${CARGO_TARGET_DIR}/universal/ios-sim/libliveview_native_core.a" ] [tasks.lipo-tvos-sim] -dependencies = ["create-lipo-universal-directories", "build-tvos"] +dependencies = ["create-lipo-universal-directories", "build-apple-no-std-targets"] workspace = false category = "Build" description = "Combines iOS simulator targets into a universal binary" @@ -234,7 +210,7 @@ args = [ ] [tasks.lipo-watchos-sim] -dependencies = ["create-lipo-universal-directories", "build-watchos"] +dependencies = ["create-lipo-universal-directories", "build-apple-no-std-targets"] workspace = false category = "Build" description = "Combines watchOS simulator targets into a universal binary" @@ -295,22 +271,6 @@ description = "Run cargo-bloat" command = "cargo" args = ["bloat", "${@}"] -[tasks.uniffi-swift-generate-from-udl] -workspace = false -category = "Packaging" -description = "" -command = "cargo" -args = [ - "run", - "--bin", - "uniffi-bindgen", - "--", - "generate", - "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/crates/core/src/uniffi.udl", - "--language=swift", - "--out-dir=${CARGO_TARGET_DIR}/uniffi/swift/generated", -] - [tasks.uniffi-swift-generate] workspace = false category = "Packaging" @@ -318,7 +278,7 @@ description = "" command = "cargo" args = [ "run", - "--bin", + "-p", "uniffi-bindgen", "--", "generate", @@ -327,29 +287,11 @@ args = [ "--language=swift", "--out-dir=${CARGO_TARGET_DIR}/uniffi/swift/generated", ] -dependencies = ["build-macos"] - -[tasks.uniffi-kotlin-generate] -workspace = false -category = "Packaging" -description = "" -command = "cargo" -args = [ - "run", - "--bin", - "uniffi-bindgen", - "--", - "generate", - "--library", - "${CARGO_TARGET_DIR}/aarch64-apple-darwin/debug/libliveview_native_core.dylib", - "--language=kotlin", - "--out-dir=${CARGO_TARGET_DIR}/uniffi/kotlin/generated", -] -dependencies = ["build-macos"] +dependencies = ["build-apple-std-targets"] [tasks.uniffi-swift-modulemap] workspace = false -category = "Packaging" +category = "Packaging the module maps for FFIs" description = "" script_runner = "@shell" script = ''' @@ -363,13 +305,13 @@ dependencies = ["uniffi-swift-generate"] [tasks.uniffi-swift-package] workspace = false category = "Packaging" -description = "Generates the LiveViewNativeCore.xcframework package" +description = "Generates the swift package from the liveview native core and phoenix-channels-clients bindings" dependencies = ["uniffi-swift-modulemap", "uniffi-swift-package-lvn", "uniffi-swift-package-phx"] [tasks.uniffi-swift-package-lvn] workspace = false category = "Packaging" -description = "Generates the LiveViewNativeCore.xcframework package" +description = "Copy the generated swift bindings for LiveViewNativeCore to the correct location." command = "cp" args = [ "${CARGO_TARGET_DIR}/uniffi/swift/generated/LiveViewNativeCore.swift", @@ -378,7 +320,7 @@ args = [ [tasks.uniffi-swift-package-phx] workspace = false category = "Packaging" -description = "Generates the LiveViewNativeCore.xcframework package" +description = "Copy the swift bindings for PhoenixChannelsClient to the right location." command = "cp" args = [ "${CARGO_TARGET_DIR}/uniffi/swift/generated/PhoenixChannelsClient.swift", @@ -419,16 +361,14 @@ args = [ "-headers", "${CARGO_TARGET_DIR}/uniffi/swift/generated", ] dependencies = [ - "build-macos", + "build-apple-std-targets", "lipo-macos", - "build-ios", "lipo-ios-sim", - "build-watchos", + "build-apple-no-std-targets", "lipo-watchos-sim", - "build-tvos", "lipo-tvos-sim", "remove-existing-uniffi-xcframework", - "uniffi-swift-package" + "uniffi-swift-generate", ] [tasks.remove-existing-uniffi-xcframework] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 88e773cf..5a6b4341 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -39,25 +39,24 @@ serde_json = { version = "1.0" } smallstr = { version = "0.3", features = ["union"] } smallvec = { version = "1.10", features = ["union", "const_generics", "specialization"] } thiserror = "1.0" -uniffi = { version = "0.26", features = ["cli", "tokio"] } +uniffi = { version = "0.27", features = ["tokio"] } async-compat = "0.2.3" futures = "0.3.29" log = "0.4" -reqwest = { version = "0.12.0", features = ["native-tls-vendored"] } -tokio = { version = "1.35", features = ["full"] } +reqwest = { version = "0.12.2", features = ["native-tls-vendored"] } [build-dependencies] Inflector = "0.11" -uniffi = { version = "0.26", features = [ "build" ] } +uniffi = { version = "0.27", features = [ "build" ] } [dev-dependencies] paste = { version = "1.0" } pretty_assertions = { version = "1.4.0" } text-diff = { version = "0.4.0" } -uniffi = { version = "0.26", features = ["bindgen-tests", "tokio", "cli"]} +uniffi = { version = "0.27", features = ["bindgen-tests", "tokio"]} tokio = { version = "1.35", features = ["full"] } env_logger = "0.11.1" # For image generation for tests -image = "0.25.0" +image = "0.25.1" tempfile = "3.9.0" diff --git a/crates/core/liveview-native-core-jetpack/core/build.gradle.kts b/crates/core/liveview-native-core-jetpack/core/build.gradle.kts index bde087ad..2b3fd9d1 100644 --- a/crates/core/liveview-native-core-jetpack/core/build.gradle.kts +++ b/crates/core/liveview-native-core-jetpack/core/build.gradle.kts @@ -74,7 +74,7 @@ android { commandLine( "cargo", "run", - "--bin", + "-p", "uniffi-bindgen", "--", "generate", @@ -147,7 +147,7 @@ publishing { register("release") { groupId = "org.phoenixframework" artifactId = "liveview-native-core-jetpack" - version = "0.1.0-pre-alpha-09" + version = "0.2.0-pre-alpha-01" afterEvaluate { from(components["release"]) diff --git a/crates/core/src/bin/uniffi-bindgen.rs b/crates/core/src/bin/uniffi-bindgen.rs deleted file mode 100644 index f6cff6cf..00000000 --- a/crates/core/src/bin/uniffi-bindgen.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - uniffi::uniffi_bindgen_main() -} From ce3d497f06eff667d6980bd5d141495cdf163d08 Mon Sep 17 00:00:00 2001 From: Sebastian Imlay Date: Thu, 4 Apr 2024 13:59:47 -0400 Subject: [PATCH 10/23] Added uniffi-bindgen workspace project --- crates/uniffi-bindgen/Cargo.toml | 17 +++++++++++++++++ crates/uniffi-bindgen/src/main.rs | 3 +++ 2 files changed, 20 insertions(+) create mode 100644 crates/uniffi-bindgen/Cargo.toml create mode 100644 crates/uniffi-bindgen/src/main.rs diff --git a/crates/uniffi-bindgen/Cargo.toml b/crates/uniffi-bindgen/Cargo.toml new file mode 100644 index 00000000..e95d42c2 --- /dev/null +++ b/crates/uniffi-bindgen/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "uniffi-bindgen" +version.workspace = true +rust-version.workspace = true +authors.workspace = true +description.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +readme.workspace = true +edition.workspace = true +publish = false + +[dependencies] +uniffi = { version = "0.27", features = ["cli"] } diff --git a/crates/uniffi-bindgen/src/main.rs b/crates/uniffi-bindgen/src/main.rs new file mode 100644 index 00000000..f6cff6cf --- /dev/null +++ b/crates/uniffi-bindgen/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} From 5b5872a018de74e6b8910bdb8f2829ce8cf8b36b Mon Sep 17 00:00:00 2001 From: Sebastian Imlay Date: Tue, 9 Apr 2024 20:58:55 -0400 Subject: [PATCH 11/23] More fixes --- Makefile.toml | 6 +- .../liveview_jetpack/DocumentTest.kt | 3 +- .../Sources/LiveViewNativeCore/Support.swift | 22 ++-- .../LiveViewNativeCoreSocketTests.swift | 2 +- .../LiveViewNativeCoreTests.swift | 100 ++++++++++++++++-- crates/core/src/diff/patch.rs | 62 ++++++----- crates/core/src/dom/ffi.rs | 1 + crates/core/src/dom/mod.rs | 40 ++++--- crates/core/src/dom/node.rs | 2 +- tests/support/test_server/config/dev.exs | 2 +- 10 files changed, 173 insertions(+), 67 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index e9fa580a..5a6d9ee3 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -219,8 +219,6 @@ args = [ "lipo", "-create", "${CARGO_TARGET_DIR}/aarch64-apple-watchos-sim/debug/libliveview_native_core.a", "${CARGO_TARGET_DIR}/x86_64-apple-watchos-sim/debug/libliveview_native_core.a", - # This isn't a simulator but putting it in the xcframework task doesn't work. - "${CARGO_TARGET_DIR}/arm64_32-apple-watchos/debug/libliveview_native_core.a", "-output", "${CARGO_TARGET_DIR}/universal/watchos-sim/libliveview_native_core.a" ] @@ -350,6 +348,10 @@ args = [ # watchOS sim "-library", "${CARGO_TARGET_DIR}/universal/watchos-sim/libliveview_native_core.a", + "-headers", "${CARGO_TARGET_DIR}/uniffi/swift/generated", + + # watchOS + "-library", "${CARGO_TARGET_DIR}/arm64_32-apple-watchos/debug/libliveview_native_core.a", "-headers", "${CARGO_TARGET_DIR}/uniffi/swift/generated", # tvOS diff --git a/crates/core/liveview-native-core-jetpack/core/src/test/java/org/phoenixframework/liveview_jetpack/DocumentTest.kt b/crates/core/liveview-native-core-jetpack/core/src/test/java/org/phoenixframework/liveview_jetpack/DocumentTest.kt index 8de079cb..d6fafcc0 100644 --- a/crates/core/liveview-native-core-jetpack/core/src/test/java/org/phoenixframework/liveview_jetpack/DocumentTest.kt +++ b/crates/core/liveview-native-core-jetpack/core/src/test/java/org/phoenixframework/liveview_jetpack/DocumentTest.kt @@ -19,7 +19,7 @@ import java.util.Base64; class SocketTest { @Test fun simple_connect() = runTest { - var live_socket = LiveSocket("http://127.0.0.1:4000/upload?_lvn[format]=swiftui", Duration.ofDays(10)); + var live_socket = LiveSocket("http://127.0.0.1:4001/upload?_lvn[format]=swiftui", Duration.ofDays(10)); var live_channel = live_socket.joinLiveviewChannel() var phx_id = live_channel.getPhxRefFromUploadJoinPayload() // This is a PNG located at crates/core/tests/support/tinycross.png @@ -36,7 +36,6 @@ class SimpleChangeHandler: DocumentChangeHandler { } override fun `handle`( - `context`: Document, `changeType`: ChangeType, `nodeRef`: NodeRef, `optionNodeRef`: NodeRef?, diff --git a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift index cf832581..c27168d5 100644 --- a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift +++ b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift @@ -1,22 +1,22 @@ import Foundation -class SimpleHandler: DocumentChangeHandler { - var callback: (Document, NodeRef) -> () +final class SimpleHandler: DocumentChangeHandler { + let callback: (NodeRef, NodeData, NodeRef?) -> () init ( - _ callback: @escaping (Document, NodeRef) -> () + _ callback: @escaping (NodeRef, NodeData, NodeRef?) -> () ) { self.callback = callback } - func handle(_ context: Document, _ changeType: ChangeType, _ node: NodeRef, _ parent: NodeRef?) { + func handle(_ changeType: ChangeType, _ node: NodeRef, _ data: NodeData, _ parent: NodeRef?) { switch changeType { case .add: - self.callback(context, parent!) + self.callback(parent!, data, parent) case .remove: - self.callback(context, parent!) + self.callback(parent!, data, parent) case .change: - self.callback(context, node) + self.callback(node, data, parent) case .replace: - self.callback(context, parent!) + self.callback(parent!, data, parent) } } @@ -28,20 +28,20 @@ extension Document { return Node(self, ref, data) } public static func parseFragmentJson(payload: [String: Any]) throws -> Document { - let jsonData = try JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) + let jsonData = try JSONSerialization.data(withJSONObject: payload) let payload = String(data: jsonData, encoding: .utf8)! return try Document.parseFragmentJson(payload) } public func mergeFragmentJson( _ payload: [String: Any] ) throws { - let jsonData = try JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) + let jsonData = try JSONSerialization.data(withJSONObject: payload) let payload = String(data: jsonData, encoding: .utf8)! return try self.mergeFragmentJson(payload) } - public func on(_ event: EventType, _ callback: @escaping (Document, NodeRef) -> ()) { + public func on(_ event: EventType, _ callback: @escaping (NodeRef, NodeData, NodeRef?) -> ()) { let simple = SimpleHandler(callback) self.setEventHandler(simple) diff --git a/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreSocketTests.swift b/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreSocketTests.swift index 893277ff..cb702ca8 100644 --- a/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreSocketTests.swift +++ b/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreSocketTests.swift @@ -4,7 +4,7 @@ import XCTest import SystemConfiguration #endif -let url = "http://127.0.0.1:4000/upload?_lvn[format]=swiftui"; +let url = "http://127.0.0.1:4001/upload?_lvn[format]=swiftui"; let timeout = TimeInterval(10.0) diff --git a/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift b/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift index 2c0d208e..cfcacbd9 100644 --- a/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift +++ b/crates/core/liveview-native-core-swift/Tests/LiveViewNativeCoreTests/LiveViewNativeCoreTests.swift @@ -1,11 +1,93 @@ import XCTest @testable import LiveViewNativeCore -class SimpleHandler: DocumentChangeHandler { - func handle(_ doc: Document, _ changeType: ChangeType, _ nodeRef: NodeRef, _ parent: NodeRef?) { +final class SimpleHandler: DocumentChangeHandler { + func handle(_ changeType: ChangeType, _ nodeRef: NodeRef, _ nodeData: NodeData, _ parent: NodeRef?) { + print("Handler:", changeType, ", node:", nodeRef.ref()); } } final class LiveViewNativeCoreTests: XCTestCase { + func testForSwiftUIClientBug() throws { + let initial_json = """ + { + "s" : [ + "", + "" + ], + "0" : { + "0" : "", + "s" : [ + "\\n ", + "\\n \\n \\n" + ], + "r" : 1 + } + } + """ + let doc = try Document.parseFragmentJson(initial_json) + let simple = SimpleHandler() + doc.setEventHandler(simple) + print("initial:\n", doc.render()) + var expected = """ + + + + + """ + XCTAssertEqual(expected, doc.render()) + + let first_increment = """ + { + "0" : { + "0" : { + "s" : [ + " Temperature: ", + " " + ], + "d" : [ + ["Increment"] + ] + } + } + } + """ + try doc.mergeFragmentJson(first_increment) + expected = """ + + + Temperature: Increment + + + + + """ + print("first:\n", doc.render()) + XCTAssertEqual(expected, doc.render()) + let second_increment = """ + { + "0" : { + "0" : { + "d" : [ ] + } + } + } + """ + try doc.mergeFragmentJson(second_increment) + print("second:\n", doc.render()) + let third_increment = """ + { "0" : { "0" : { "d" : [ [ "Increment" ] ] } } } + """ + try doc.mergeFragmentJson(third_increment) + print("third:\n", doc.render()) + } func testIntegration() throws { let input = """ @@ -51,9 +133,9 @@ final class LiveViewNativeCoreTests: XCTestCase { } """ let simple = SimpleHandler() - let initial_document = try Document.parseFragmentJson(initial_json) - initial_document.setEventHandler(simple) - let initial_rendered = initial_document.render() + let doc = try Document.parseFragmentJson(initial_json) + doc.setEventHandler(simple) + let initial_rendered = doc.render() var expected = """