Skip to content
This repository was archived by the owner on Jul 5, 2025. It is now read-only.

Commit e8f8960

Browse files
committed
feat: text block encoder
1 parent 4aac964 commit e8f8960

File tree

24 files changed

+363
-113
lines changed

24 files changed

+363
-113
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
// "rust-analyzer.cargo.features": "all",
3-
// "rust-analyzer.cargo.target": "wasm32-unknown-unknown",
3+
"rust-analyzer.cargo.target": "wasm32-unknown-unknown",
44
"rust-analyzer.files.excludeDirs": [
55
// "crates/sweet_rsx/macros",
66
// "crates/sweet_test/macros",

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/sweet_core/src/error/parse_error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
#[cfg(not(target_arch = "wasm32"))]
23
use forky::prelude::FsError;
34
use thiserror::Error;
@@ -45,7 +46,6 @@ impl From<&str> for ParseError {
4546
fn from(e: &str) -> Self { Self::Other(e.to_string()) }
4647
}
4748

48-
4949
#[cfg(feature = "serde")]
5050
impl From<bincode::Error> for ParseError {
5151
fn from(e: bincode::Error) -> Self { Self::Serde(e.to_string()) }

crates/sweet_core/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ pub use utils::log::*;
99
pub use utils::sleep::*;
1010

1111
#[cfg(feature = "quote")]
12-
#[path = "rsx/_html_partial_quote.rs"]
13-
pub mod html_partial_quote;
12+
#[path = "rsx/_rsx_tree_quote.rs"]
13+
pub mod _rsx_tree_quote;
1414

1515
pub mod prelude {
1616
pub use crate::error::*;
1717
#[cfg(feature = "quote")]
1818
#[allow(unused_imports)]
19-
pub use crate::html_partial_quote::*;
19+
pub use crate::_rsx_tree_quote::*;
2020
pub use crate::rsx::*;
2121
pub use crate::utils::log::*;
2222
pub use crate::utils::sleep::*;

crates/sweet_core/src/rsx/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pub use self::rsx_tree_visitor::*;
1616
pub mod rust_parts;
1717
#[allow(unused_imports)]
1818
pub use self::rust_parts::*;
19+
pub mod text_block_encoder;
20+
#[allow(unused_imports)]
21+
pub use self::text_block_encoder::*;
1922
pub mod tree_position;
2023
#[allow(unused_imports)]
2124
pub use self::tree_position::*;

crates/sweet_core/src/rsx/rsx.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,16 @@ impl Rsx for RsxTree<RustParts> {
1111
impl Rsx for () {
1212
fn into_rsx_tree(self) -> RsxTree<RustParts> { Default::default() }
1313
}
14-
14+
impl Rsx for &str {
15+
fn into_rsx_tree(self) -> RsxTree<RustParts> {
16+
RsxTree::new(vec![Node::Text(self.to_string())])
17+
}
18+
}
19+
impl Rsx for String {
20+
fn into_rsx_tree(self) -> RsxTree<RustParts> {
21+
RsxTree::new(vec![Node::Text(self)])
22+
}
23+
}
1524

1625

1726
pub trait Component {

crates/sweet_core/src/rsx/rsx_tree.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub enum Node<R> {
7575
}
7676

7777
impl<R> Node<R> {
78+
7879
pub fn to_string_placeholder(&self) -> String {
7980
match self {
8081
Node::Doctype => "<!DOCTYPE html>".to_string(),
@@ -102,6 +103,13 @@ impl<R> Node<R> {
102103
_ => None,
103104
}
104105
}
106+
pub fn take_children(&mut self) -> Option<Vec<Node<R>>> {
107+
match self {
108+
Node::Element(e) => Some(std::mem::take(&mut e.children)),
109+
Node::Component(_, c) => Some(std::mem::take(c)),
110+
_ => None,
111+
}
112+
}
105113
}
106114

107115

crates/sweet_core/src/rsx/rsx_tree_visitor.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,36 @@ pub trait RsxTreeVisitorMut<R> {
5252
Ok(())
5353
}
5454
}
55+
#[allow(unused_variables)]
56+
pub trait RsxTreeVisitorOwned<R> {
57+
fn walk_nodes_dfs(&mut self, nodes: Vec<Node<R>>) -> ParseResult<()> {
58+
let nodes = self.visit_children(nodes)?;
59+
for node in nodes.into_iter() {
60+
if let Some(children) = self.visit_node(node)? {
61+
self.walk_nodes_dfs(children)?;
62+
}
63+
self.leave_node()?;
64+
}
65+
self.leave_children()?;
66+
Ok(())
67+
}
5568

69+
/// take a node, optionally returning its children
70+
fn visit_node(
71+
&mut self,
72+
mut node: Node<R>,
73+
) -> ParseResult<Option<Vec<Node<R>>>> {
74+
Ok(node.take_children())
75+
}
76+
fn leave_node(&mut self) -> ParseResult<()> { Ok(()) }
77+
fn visit_children(
78+
&mut self,
79+
children: Vec<Node<R>>,
80+
) -> ParseResult<Vec<Node<R>>> {
81+
Ok(children)
82+
}
83+
fn leave_children(&mut self) -> ParseResult<()> { Ok(()) }
84+
}
5685

5786

5887

@@ -70,6 +99,7 @@ pub struct RsxTreePositionVisitor {
7099
}
71100

72101
impl RsxTreePositionVisitor {
102+
/// The node count - 1
73103
/// # Panics
74104
/// If no nodes have been visited
75105
pub fn current_node_id(&self) -> usize { self.node_count - 1 }
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
use super::Node;
2+
use super::RustParts;
3+
use crate::error::ParseError;
4+
use crate::error::ParseResult;
5+
6+
/// This module is for handling rsx text blocks in html text node.
7+
///
8+
/// ## The Problem
9+
///
10+
/// consider the following rsx:
11+
/// ``` ignore
12+
/// # use sweet_rsx_macros::rsx;
13+
/// # use crate as sweet;
14+
/// let desc = "quick";
15+
/// let color = "brown";
16+
/// let action = "jumps over";
17+
/// let Adjective = rsx!{ lazy };
18+
/// let Phrase = rsx!{ <div>The {desc} and {color} <b>fox</b> {action} the <Adjective/> dog</div> };
19+
/// ```
20+
/// This will flatten to the following html:
21+
/// ```html
22+
/// <div>The quick and brown <b>fox</b> jumps over the lazy dog</div>
23+
/// ```
24+
/// This encoder will encode the text block positions in the text node.
25+
pub struct TextBlockEncoder;
26+
27+
impl TextBlockEncoder {
28+
/// Encoding into a 'dash comma dot' format
29+
/// Encoding for TextBlock positions, we need the following:
30+
/// - The child index of the text node
31+
/// - The string index of the block
32+
/// - The length of the TextBlock initial value
33+
/// child_index - first-block-index , first-block-length , second-block-index , second-block-length . child_index2 etc
34+
///
35+
/// ## Example
36+
/// ```html
37+
/// <div>the 10th <bold>value</bold> was 9</div>
38+
/// ```
39+
/// Output:
40+
/// 0-4,2.2-5,1
41+
pub fn encode(nodes: &Vec<Node<RustParts>>) -> String {
42+
let collapsed = CollapsedNode::from_nodes(nodes);
43+
Self::encode_text_block_positions(&collapsed)
44+
}
45+
46+
fn encode_text_block_positions(nodes: &Vec<CollapsedNode>) -> String {
47+
let mut encoded = String::new();
48+
let mut child_index = 0;
49+
let mut text_index = 0;
50+
for node in nodes {
51+
match node {
52+
CollapsedNode::StaticText(t) => {
53+
text_index += t.len();
54+
}
55+
CollapsedNode::RustText(len) => {
56+
let len = len.len();
57+
encoded.push_str(&format!(
58+
"{}-{}-{},",
59+
child_index, text_index, len
60+
));
61+
text_index += len;
62+
}
63+
CollapsedNode::Break => {
64+
child_index += 1;
65+
text_index = 0;
66+
}
67+
}
68+
}
69+
if encoded.len() > 0 {
70+
encoded.pop();
71+
}
72+
encoded
73+
}
74+
75+
pub fn decode(encoded: &str) -> ParseResult<Vec<TextBlockPosition>> {
76+
let mut out = Vec::new();
77+
let err = |_| {
78+
ParseError::Serde(format!(
79+
"Failed to decode text block positions from attribute: {}",
80+
encoded
81+
))
82+
};
83+
for block in encoded.split(",") {
84+
let mut parts = block.split("-");
85+
let child_index =
86+
parts.next().unwrap().parse::<usize>().map_err(err)?;
87+
let text_index =
88+
parts.next().unwrap().parse::<usize>().map_err(err)?;
89+
let len = parts.next().unwrap().parse::<usize>().map_err(err)?;
90+
out.push(TextBlockPosition {
91+
child_index,
92+
text_index,
93+
len,
94+
});
95+
}
96+
Ok(out)
97+
}
98+
}
99+
100+
101+
102+
#[derive(Debug, Clone, PartialEq)]
103+
enum CollapsedNode {
104+
/// static text, ie `rsx!{"foo"}`
105+
StaticText(String),
106+
/// text that can change, ie `rsx!{{val}}`
107+
RustText(String),
108+
/// doctype, comment, and element all break text node
109+
/// ie `rsx!{<div/>}`
110+
Break,
111+
}
112+
impl CollapsedNode {
113+
#[allow(unused)]
114+
pub(crate) fn as_str(&self) -> &str {
115+
match self {
116+
CollapsedNode::StaticText(val) => val,
117+
CollapsedNode::RustText(val) => val,
118+
CollapsedNode::Break => "|",
119+
}
120+
}
121+
}
122+
123+
impl CollapsedNode {
124+
fn from_nodes(nodes: &Vec<Node<RustParts>>) -> Vec<CollapsedNode> {
125+
let mut out = Vec::new();
126+
for node in nodes {
127+
match node {
128+
Node::TextBlock(RustParts::TextBlock(val)) => {
129+
out.push(CollapsedNode::RustText(val.clone()))
130+
}
131+
Node::Text(val) => {
132+
out.push(CollapsedNode::StaticText(val.clone()))
133+
}
134+
Node::Doctype => out.push(CollapsedNode::Break),
135+
Node::Comment(_) => out.push(CollapsedNode::Break),
136+
Node::Element(_) => out.push(CollapsedNode::Break),
137+
Node::Component(RustParts::Component(children), vec) => {
138+
out.append(&mut Self::from_nodes(&children.nodes));
139+
out.append(&mut Self::from_nodes(vec))
140+
}
141+
_ => {
142+
// ignore invalid nodes
143+
}
144+
}
145+
}
146+
return out;
147+
}
148+
}
149+
150+
#[derive(Debug, Clone, PartialEq)]
151+
pub struct TextBlockPosition {
152+
pub child_index: usize,
153+
pub text_index: usize,
154+
pub len: usize,
155+
}
156+
157+
#[cfg(test)]
158+
mod test {
159+
#![allow(non_snake_case)]
160+
use crate as sweet;
161+
use crate::prelude::*;
162+
use sweet_rsx_macros::rsx;
163+
use sweet_test::prelude::*;
164+
use super::*;
165+
166+
struct Adjective;
167+
impl Component for Adjective {
168+
fn render(self) -> impl Rsx {
169+
rsx! {"lazy"}
170+
}
171+
}
172+
173+
#[test]
174+
fn roundtrip() {
175+
let desc = "quick";
176+
let color = "brown";
177+
let action = "jumps over";
178+
let tree = rsx! {"The "{desc}" and "{color}<b> fox </b> {action}" the "<Adjective> and fat </Adjective>dog };
179+
let collapsed = CollapsedNode::from_nodes(&tree.nodes);
180+
181+
expect(&collapsed).to_be(&vec![
182+
CollapsedNode::StaticText("The ".into()),
183+
CollapsedNode::RustText("quick".into()),
184+
CollapsedNode::StaticText(" and ".into()),
185+
CollapsedNode::RustText("brown".into()),
186+
CollapsedNode::Break,
187+
CollapsedNode::RustText("jumps over".into()),
188+
CollapsedNode::StaticText(" the ".into()),
189+
CollapsedNode::StaticText("lazy".into()),
190+
CollapsedNode::StaticText(" and fat ".into()),
191+
CollapsedNode::StaticText("dog".into()),
192+
]);
193+
194+
// println!(
195+
// "{}",
196+
// collapsed.iter().map(|n| n.as_str()).collect::<String>()
197+
// );
198+
let encoded = TextBlockEncoder::encode_text_block_positions(&collapsed);
199+
// println!("{}", encoded);
200+
expect(&encoded).to_be("0-4-5,0-14-5,1-0-10");
201+
202+
let decoded = TextBlockEncoder::decode(&encoded).unwrap();
203+
204+
205+
expect(&decoded).to_be(&vec![
206+
TextBlockPosition {
207+
child_index: 0,
208+
text_index: 4,
209+
len: 5,
210+
},
211+
TextBlockPosition {
212+
child_index: 0,
213+
text_index: 14,
214+
len: 5,
215+
},
216+
TextBlockPosition {
217+
child_index: 1,
218+
text_index: 0,
219+
len: 10,
220+
},
221+
]);
222+
}
223+
}

0 commit comments

Comments
 (0)