Skip to content

Commit bc5d2c7

Browse files
committed
Add tests and fix some edge cases
Namely, fixing the cursor position after insert, and adding more parsing post processing to keep the dom in an expected state with wrap_inline_nodes_into_paragraphs_if_needed and join_nodes_in_container. Also fixing some tests.
1 parent 7810381 commit bc5d2c7

File tree

7 files changed

+144
-78
lines changed

7 files changed

+144
-78
lines changed

crates/wysiwyg/src/composer_model/example_format.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ mod test {
903903
#[test]
904904
fn selection_across_lists_roundtrips() {
905905
assert_that!(
906-
"<ol><li>1{1</li><li>22</li></ol><ol><li>33</li><li>4}|4</li></ol>"
906+
"<ol><li>1{1</li><li>22</li></ol><p>a</p><ol><li>33</li><li>4}|4</li></ol>"
907907
)
908908
.roundtrips();
909909
}
@@ -915,6 +915,7 @@ mod test {
915915
<li>1{1</li>\
916916
<li>22</li>\
917917
</ol>\
918+
<p>a</p>\
918919
<ol>\
919920
<li>33</li>\
920921
<li>4}|4</li>\

crates/wysiwyg/src/composer_model/replace_html.rs

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ use regex::Regex;
99
use crate::dom::html_source::HtmlSource;
1010
use crate::dom::nodes::ContainerNode;
1111
use crate::dom::parser::parse_from_source;
12-
13-
use crate::{ComposerModel, ComposerUpdate, DomNode, Location, UnicodeString};
12+
use crate::{ComposerModel, ComposerUpdate, DomNode, Location, UnicodeString}; // Import the trait for to_tree
1413

1514
impl<S> ComposerModel<S>
1615
where
@@ -49,16 +48,25 @@ where
4948

5049
// We should only have 1 dom node, so add the children under a paragraph to take advantage of the exisitng
5150
// insert_node_at_cursor api and then delete the paragraph node promoting it's the children up a level.
52-
let p = DomNode::Container(ContainerNode::new_paragraph(
53-
doc_node.into_container().unwrap().take_children(),
54-
));
55-
let new_cursor_index = start + p.text_len();
51+
let new_children = doc_node.into_container().unwrap().take_children();
52+
let child_count = new_children.len();
53+
let p = DomNode::Container(ContainerNode::new_paragraph(new_children));
54+
5655
let handle = self.state.dom.insert_node_at_cursor(&range, p);
5756
self.state.dom.replace_node_with_its_children(&handle);
57+
self.state.dom.wrap_inline_nodes_into_paragraphs_if_needed(
58+
&self.state.dom.parent(&handle).handle(),
59+
);
5860

59-
// manually move the cursor to the end of the html
60-
self.state.start = Location::from(new_cursor_index);
61+
// Track the index of the last inserted node for placing the cursor
62+
let last_index = handle.index_in_parent() + child_count - 1;
63+
let last_handle = handle.parent_handle().child_handle(last_index);
64+
let location = self.state.dom.location_for_node(&last_handle);
65+
66+
self.state.start =
67+
Location::from(location.position + location.length - 1);
6168
self.state.end = self.state.start;
69+
// add a trailing space in cases when we do not have a next sibling
6270
self.create_update_replace_all()
6371
}
6472
}
@@ -121,4 +129,68 @@ mod test {
121129
let html_str = html.to_string();
122130
assert_eq!(html_str, "<p><strong>test</strong></p>");
123131
}
132+
133+
#[test]
134+
fn test_replace_html_with_existing_selection() {
135+
let mut model = cm("Hello{world}|test");
136+
let new_html = "<p><em>replacement</em></p>";
137+
138+
let _ =
139+
model.replace_html(new_html.into(), HtmlSource::UnknownExternal);
140+
141+
let html = model.get_content_as_html();
142+
let html_str = html.to_string();
143+
assert_eq!(
144+
html_str,
145+
"<p>Hello</p><p><em>replacement</em></p><p>test</p>"
146+
);
147+
}
148+
149+
#[test]
150+
fn test_replace_html_cursor_position_after_insert() {
151+
let mut model = cm("Start|");
152+
let new_html = "<strong>Bold text</strong>";
153+
let _ = model.replace_html(new_html.into(), HtmlSource::Matrix);
154+
// Cursor should be positioned after the inserted content
155+
let (start, end) = model.safe_selection();
156+
assert_eq!(start, end); // No selection, just cursor
157+
model.bold();
158+
model.enter();
159+
// Insert more text to verify cursor position
160+
let _ = model.replace_text("End".into());
161+
let html = model.get_content_as_html();
162+
let html_str = html.to_string();
163+
assert_eq!(
164+
html_str,
165+
"<p>Start</p><p><strong>Bold text</strong></p><p>End</p>"
166+
);
167+
}
168+
169+
#[test]
170+
fn test_replace_html_multiple_meta_tags() {
171+
let mut model = cm("|");
172+
let html_with_multiple_metas = r#"<meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta http-equiv="X-UA-Compatible" content="IE=edge"><p>Content after metas</p>"#;
173+
174+
let _ = model.replace_html(
175+
html_with_multiple_metas.into(),
176+
HtmlSource::UnknownExternal,
177+
);
178+
179+
let html = model.get_content_as_html();
180+
let html_str = html.to_string();
181+
assert!(!html_str.contains("<meta"));
182+
assert_eq!(html_str, "<p>Content after metas</p>");
183+
}
184+
185+
#[test]
186+
fn test_replace_html_empty_content() {
187+
let mut model = cm("Existing content|");
188+
let empty_html = "";
189+
190+
let _ = model.replace_html(empty_html.into(), HtmlSource::Matrix);
191+
192+
let html = model.get_content_as_html();
193+
let html_str = html.to_string();
194+
assert_eq!(html_str, "<p>Existing content</p>");
195+
}
124196
}

crates/wysiwyg/src/dom/dom_struct.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::dom::nodes::{ContainerNode, DomNode};
1111
use crate::dom::to_html::ToHtmlState;
1212
use crate::dom::to_markdown::{MarkdownError, MarkdownOptions, ToMarkdown};
1313
use crate::dom::unicode_string::UnicodeStrExt;
14+
use crate::dom::DomLocation;
1415
use crate::dom::{
1516
find_range, to_raw_text::ToRawText, DomHandle, Range, ToTree, UnicodeString,
1617
};
@@ -295,14 +296,24 @@ where
295296
find_range::find_range(self, start, end)
296297
}
297298

298-
pub fn find_range_by_node(&self, node_handle: &DomHandle) -> Range {
299-
let result = find_range::find_pos(self, node_handle, 0, usize::MAX);
299+
pub fn location_for_node(&self, node_handle: &DomHandle) -> DomLocation {
300+
let locations = find_range::find_range(self, 0, usize::MAX);
301+
return locations.find_location(node_handle).unwrap().clone();
302+
}
300303

301-
let locations = match result {
304+
pub fn locations_for_node(
305+
&self,
306+
node_handle: &DomHandle,
307+
) -> Vec<DomLocation> {
308+
let result = find_range::find_pos(self, &node_handle, 0, usize::MAX);
309+
match result {
302310
FindResult::Found(locations) => locations,
303311
_ => panic!("Node does not exist"),
304-
};
312+
}
313+
}
305314

315+
pub fn find_range_by_node(&self, node_handle: &DomHandle) -> Range {
316+
let locations = self.locations_for_node(node_handle);
306317
let leaves = locations.iter().filter(|l| l.is_leaf());
307318

308319
let s = leaves.clone().map(|l| l.position).min().unwrap();

crates/wysiwyg/src/dom/insert_node_at_cursor.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ where
6767
};
6868
}
6969

70+
self.wrap_inline_nodes_into_paragraphs_if_needed(
71+
&self.parent(&inserted_handle).handle(),
72+
);
73+
7074
#[cfg(any(test, feature = "assert-invariants"))]
7175
self.assert_invariants();
7276

crates/wysiwyg/src/dom/parser/parse.rs

Lines changed: 23 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use regex::Regex;
99
use crate::dom::dom_creation_error::HtmlParseError;
1010
use crate::dom::html_source::HtmlSource;
1111
use crate::dom::nodes::dom_node::DomNodeKind::{self};
12-
use crate::dom::nodes::{container_node, ContainerNode, ContainerNodeKind};
12+
use crate::dom::nodes::{ContainerNode, ContainerNodeKind};
1313
use crate::dom::Dom;
1414
use crate::{DomHandle, DomNode, UnicodeString};
1515

@@ -1190,30 +1190,25 @@ mod sys {
11901190
r#"
11911191

11921192
├>ol
1193-
│ └>li
1194-
│ └>p
1195-
│ └>i
1196-
│ └>"Italic"
1197-
├>ol
1198-
│ └>li
1199-
│ └>p
1200-
│ └>b
1201-
│ └>"Bold"
1202-
├>ol
1203-
│ └>li
1204-
│ └>p
1205-
│ └>"Unformatted"
1206-
├>ol
1207-
│ └>li
1208-
│ └>p
1209-
│ └>del
1210-
│ └>"Strikethrough"
1211-
├>ol
1212-
│ └>li
1213-
│ └>p
1214-
│ └>u
1215-
│ └>"Underlined"
1216-
├>ol
1193+
│ ├>li
1194+
│ │ └>p
1195+
│ │ └>i
1196+
│ │ └>"Italic"
1197+
│ ├>li
1198+
│ │ └>p
1199+
│ │ └>b
1200+
│ │ └>"Bold"
1201+
│ ├>li
1202+
│ │ └>p
1203+
│ │ └>"Unformatted"
1204+
│ ├>li
1205+
│ │ └>p
1206+
│ │ └>del
1207+
│ │ └>"Strikethrough"
1208+
│ ├>li
1209+
│ │ └>p
1210+
│ │ └>u
1211+
│ │ └>"Underlined"
12171212
│ └>li
12181213
│ └>p
12191214
│ └>a "https://matrix.org/"
@@ -1253,49 +1248,15 @@ fn post_process_adjacent_text<S: UnicodeString>(
12531248
fn post_process_for_block_and_inline_siblings<S: UnicodeString>(
12541249
mut dom: Dom<S>,
12551250
) -> Dom<S> {
1256-
let continer_handles = find_containers_with_inline_and_block_children(&dom);
1257-
for handle in continer_handles.iter().rev() {
1258-
dom = post_process_container_for_block_and_inline_siblings(dom, handle);
1259-
}
1260-
dom
1261-
}
1262-
1263-
fn find_containers_with_inline_and_block_children<S: UnicodeString>(
1264-
dom: &Dom<S>,
1265-
) -> Vec<DomHandle> {
1266-
dom.iter_containers()
1267-
.filter(|n| {
1268-
if n.children().is_empty() {
1269-
return false; // Skip empty containers
1270-
}
1271-
let all_nodes_are_inline =
1272-
n.children().iter().all(|n| !n.is_block_node());
1273-
let all_nodes_are_block =
1274-
n.children().iter().all(|n| n.is_block_node());
1275-
!all_nodes_are_inline && !all_nodes_are_block
1276-
})
1277-
.map(|n| n.handle())
1278-
.collect::<Vec<_>>()
1279-
}
1280-
1281-
fn post_process_container_for_block_and_inline_siblings<S: UnicodeString>(
1282-
mut dom: Dom<S>,
1283-
handle: &DomHandle,
1284-
) -> Dom<S> {
1285-
// upate the container node by grouping inline nodes, to avoid
1286-
// having inline nodes as siblings of block nodes.
1287-
let container_node =
1288-
dom.lookup_node_mut(handle).as_container_mut().unwrap();
1289-
let new_children =
1290-
group_inline_nodes(container_node.remove_children().to_vec());
1291-
container_node.insert_children(0, new_children.clone());
1251+
dom.wrap_inline_nodes_into_paragraphs_if_needed(&DomHandle::root());
12921252
dom
12931253
}
12941254

12951255
fn post_process_blocks<S: UnicodeString>(mut dom: Dom<S>) -> Dom<S> {
12961256
let block_handles = find_blocks(&dom);
12971257
for handle in block_handles.iter().rev() {
12981258
dom = post_process_block_lines(dom, handle);
1259+
dom.join_nodes_in_container(&handle);
12991260
}
13001261
dom
13011262
}
@@ -1575,6 +1536,7 @@ mod js {
15751536
self.webdom_to_dom(document, html_source)
15761537
.map_err(to_dom_creation_error)
15771538
.map(post_process_blocks)
1539+
.map(post_process_for_block_and_inline_siblings)
15781540
.map(post_process_for_adjacent_text)
15791541
}
15801542

crates/wysiwyg/src/dom/range.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use crate::dom::dom_handle::DomHandle;
88
use crate::dom::nodes::dom_node::DomNodeKind;
99
use std::cmp::{min, Ordering};
10+
use std::fmt;
1011

1112
/// Represents the relative position of a DomLocation towards
1213
/// the range start and end.
@@ -202,6 +203,21 @@ impl DomLocation {
202203
}
203204
}
204205

206+
impl fmt::Display for DomLocation {
207+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208+
write!(
209+
f,
210+
"DomLocation[node_handle: {:?}, position: {}, start_offset: {}, end_offset: {}, length: {}, kind: {:?}]",
211+
self.node_handle.raw(),
212+
self.position,
213+
self.start_offset,
214+
self.end_offset,
215+
self.length,
216+
self.kind
217+
)
218+
}
219+
}
220+
205221
impl PartialOrd<Self> for DomLocation {
206222
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
207223
Some(self.cmp(other))

crates/wysiwyg/src/tests/test_deleting.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,7 @@ fn backspace_immutable_link_from_inside_link() {
878878
#[test]
879879
fn backspace_immutable_link_multiple() {
880880
let mut model = cm(
881-
"<a contenteditable=\"false\" href=\"https://matrix.org\">first</a><a contenteditable=\"false\" href=\"https://matrix.org\">second|</a>",
881+
"<a contenteditable=\"false\" href=\"https://matrix.org\">first</a><a contenteditable=\"false\" href=\"https://element.io.org\">second|</a>",
882882
);
883883
model.backspace();
884884
assert_eq!(
@@ -956,12 +956,12 @@ fn delete_mention_from_start() {
956956
#[test]
957957
fn delete_first_immutable_link_of_multiple() {
958958
let mut model = cm(
959-
"<a contenteditable=\"false\" href=\"https://matrix.org\">|first</a><a contenteditable=\"false\" href=\"https://matrix.org\">second</a>",
959+
"<a contenteditable=\"false\" href=\"https://matrix.org\">|first</a><a contenteditable=\"false\" href=\"https://element.io\">second</a>",
960960
);
961961
model.delete();
962962
assert_eq!(
963963
restore_whitespace(&tx(&model)),
964-
"<a contenteditable=\"false\" href=\"https://matrix.org\">|second</a>"
964+
"<a contenteditable=\"false\" href=\"https://element.io\">|second</a>"
965965
);
966966
model.delete();
967967
assert_eq!(restore_whitespace(&tx(&model)), "|");
@@ -984,7 +984,7 @@ fn delete_first_mention_of_multiple() {
984984
#[test]
985985
fn delete_second_immutable_link_of_multiple() {
986986
let mut model = cm(
987-
"<a contenteditable=\"false\" href=\"https://matrix.org\">first</a><a contenteditable=\"false\" href=\"https://matrix.org\">second|</a>",
987+
"<a contenteditable=\"false\" href=\"https://matrix.org\">first</a><a contenteditable=\"false\" href=\"https://element.io\">second|</a>",
988988
);
989989
model.backspace();
990990
assert_eq!(

0 commit comments

Comments
 (0)