Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion crates/stream-dom-dioxus/src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,14 @@ impl MutationWriter {
{
let name = self.intern(name);
let ns = self.intern_opt(*namespace);
let value = match asset_handle(value) {
Some(h) => proto::template_attr::Value::Asset(h),
None => proto::template_attr::Value::Text((*value).to_string()),
};
out_attrs.push(proto::TemplateAttr {
name,
ns,
value: Some(proto::template_attr::Value::Text((*value).to_string())),
value: Some(value),
});
}
}
Expand Down Expand Up @@ -558,6 +562,15 @@ impl WriteMutations for MutationWriter {
return;
};

// An asset handle is never a property, never a style: check before
// either branch below.
if let Some(h) = text.as_deref().and_then(asset_handle) {
let name = self.intern(name);
let ns = self.intern_opt(ns);
self.batch.set_attribute_asset(nid, name, ns, &h);
return;
}

if ns == Some("style") {
// The protocol carries one `style` attribute, not a style map, so
// Dioxus's per-property writes accumulate and re-serialize whole.
Expand Down Expand Up @@ -678,6 +691,32 @@ impl WriteMutations for MutationWriter {
}
}

/// The Dioxus producer's spelling for an asset handle (docs/design.md
/// "Assets are handles, not bytes and not URLs"). Dioxus attribute values are
/// strings; the protocol's `asset` arm wants opaque bytes, so a Dioxus app
/// names a handle as `asset:<hex>` and this decodes it back. Anything not
/// matching the grammar — including a bare `"asset:"` prefix — is left as
/// text, unchanged.
///
/// Returns `None` unless `value` is `asset:` followed by a non-empty,
/// even-length run of hex digits (either case).
fn asset_handle(value: &str) -> Option<Vec<u8>> {
let hex = value.strip_prefix("asset:")?;
if hex.is_empty() || hex.len() % 2 != 0 {
return None;
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
let digits = hex.as_bytes();
let mut i = 0;
while i < digits.len() {
let hi = (digits[i] as char).to_digit(16)?;
let lo = (digits[i + 1] as char).to_digit(16)?;
bytes.push((hi as u8) << 4 | lo as u8);
i += 2;
}
Some(bytes)
}

/// Reduce an `AttributeValue` to the string dioxus's own renderer hands its
/// interpreter, before any attribute-vs-property decision is made
/// (dioxus-interpreter-js-0.7.10 src/write_native_mutations.rs
Expand Down
83 changes: 83 additions & 0 deletions crates/stream-dom-dioxus/tests/writer_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,89 @@ fn attribute_property_table_matches_dioxus_web() {
}
}

/// A static template attribute matching the `asset:<hex>` convention, plus
/// four dynamic attributes exercising the boundary: a real handle and three
/// values that merely resemble the prefix (bare, non-hex, odd-length).
fn asset_app() -> Element {
let a = use_signal(|| "asset:deadbeef".to_string());
let b = use_signal(|| "asset:".to_string());
let c = use_signal(|| "asset:xyz".to_string());
let d = use_signal(|| "asset:abc".to_string());
rsx! {
link { rel: "stylesheet", href: "asset:0a0B" }
input { "data-a": "{a}" }
input { "data-b": "{b}" }
input { "data-c": "{c}" }
input { "data-d": "{d}" }
}
}

/// Both the static-template and dynamic `set_attribute` paths recognize the
/// `asset:<hex>` convention (writer.rs `asset_handle`; docs/design.md
/// "Assets are handles, not bytes and not URLs") and only that grammar: a
/// bare prefix, non-hex digits, or an odd-length hex run all stay `Text`.
#[test]
fn asset_convention_marks_the_asset_arm_and_only_the_asset_arm() {
let interner = Rc::new(RefCell::new(Interner::new()));
let mut writer = MutationWriter::new(interner.clone());
let mut dom = VirtualDom::new(asset_app);
dom.rebuild(&mut writer);
let bytes = writer.batch.finish().expect("mount produces a batch");
let frames = decode_all(&bytes);

let mut template_href = None;
for f in &frames {
if let Some(proto::frame::Op::RegisterTemplate(t)) = &f.op {
for node in &t.nodes {
if let Some(proto::template_node::Kind::Element(e)) = &node.kind {
for a in &e.attrs {
if interner.borrow().resolve(a.name) == Some("href") {
template_href = a.value.clone();
}
}
}
}
}
}
assert_eq!(
template_href,
Some(proto::template_attr::Value::Asset(vec![0x0a, 0x0b])),
"static href=\"asset:0a0B\" must decode to Value::Asset([0x0a, 0x0b])"
);

// Dynamic `data-*` attrs, keyed by name so each signal's fate is checked
// independently of frame order.
let mut dynamic: std::collections::HashMap<String, Option<proto::set_attribute::Value>> =
std::collections::HashMap::new();
for f in &frames {
if let Some(proto::frame::Op::SetAttribute(s)) = &f.op {
if let Some(name) = interner.borrow().resolve(s.name) {
if name.starts_with("data-") {
dynamic.insert(name.to_string(), s.value.clone());
}
}
}
}
assert_eq!(
dynamic.get("data-a"),
Some(&Some(proto::set_attribute::Value::Asset(vec![
0xde, 0xad, 0xbe, 0xef
]))),
"data-a=\"asset:deadbeef\" must decode to Value::Asset; got {dynamic:?}"
);
for (name, text) in [
("data-b", "asset:"),
("data-c", "asset:xyz"),
("data-d", "asset:abc"),
] {
assert_eq!(
dynamic.get(name),
Some(&Some(proto::set_attribute::Value::Text(text.to_string()))),
"{name}={text:?} does not match the asset grammar and must stay Text; got {dynamic:?}"
);
}
}

/// Each row's `button` is a template interior with its own node id, so
/// removing a row must forget the button too.
fn removable_list() -> Element {
Expand Down
7 changes: 7 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,13 @@ wire rule.
clone. A receiver with no `resolveAsset` configured treats an asset value
as an error.

Dioxus attribute values are always strings, with no `asset` arm of their
own to write into — so `stream-dom-dioxus` spells a handle as the string
`asset:<hex>` (an even-length run of hex digits) and decodes it back to
the `asset` arm at both the static-template and dynamic `set-attribute`
sites. Anything not matching that grammar, including a value that merely
resembles the prefix, is text, exactly as before.

## Events

Dispatch: `handle-event(target, name, payload: list<u8>, ev)` export.
Expand Down
Loading