use std::{borrow::Cow, fmt::Display, marker::PhantomData};
use serde::Deserialize;
use crate::{
context::{AttributeValue, DatastarSource, ScriptSource},
render::{
Buffer, Render, push_datastar_source_to_html_attribute,
push_js_single_quoted_string_to_html_attribute,
},
signal_path::{is_bare_signal_path_segment, parse_signal_path},
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct ElementId(pub(crate) String);
impl<'de> Deserialize<'de> for ElementId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(ElementId(s))
}
}
impl Display for ElementId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl AsRef<ElementId> for ElementId {
fn as_ref(&self) -> &ElementId {
self
}
}
impl ElementId {
#[doc(hidden)]
pub fn __dynamic(s: String) -> Self {
Self(s)
}
}
impl Render<AttributeValue> for ElementId {
fn render_to(&self, buffer: &mut Buffer<AttributeValue>) {
self.0.render_to(buffer);
}
}
impl Render<DatastarSource> for ElementId {
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
self.0.render_to(buffer);
}
}
impl Render<ScriptSource> for ElementId {
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
self.0.render_to(buffer);
}
}
#[inline]
fn push_signal_object_key(dst: &mut String, segment: &str) {
if is_bare_signal_path_segment(segment) {
push_datastar_source_to_html_attribute(dst, segment);
} else {
push_js_single_quoted_string_to_html_attribute(dst, segment);
}
}
fn push_signal_object_prefix(path: &str, dst: &mut String) -> Option<usize> {
let mut segments = parse_signal_path(path).into_iter();
let first_segment = segments.next()?;
push_signal_object_key(dst, &first_segment);
let mut close_count = 0;
for segment in segments {
close_count += 1;
dst.push_str(":{");
push_signal_object_key(dst, &segment);
}
Some(close_count)
}
#[inline]
fn close_signal_object(dst: &mut String, count: usize) {
for _ in 0..count {
dst.push('}');
}
}
#[derive(Debug)]
pub struct Signal<T> {
path: Cow<'static, str>,
ty: PhantomData<T>,
}
impl<T> Signal<T> {
#[doc(hidden)]
pub const fn __static(path: &'static str) -> Self {
Self {
path: Cow::Borrowed(path),
ty: PhantomData::<T>,
}
}
#[doc(hidden)]
pub fn __scoped_with_component(
name: String,
component_id: String,
file: &'static str,
line: u32,
column: u32,
) -> Self {
let hash = hash_component_location(&component_id, file, line, column).to_string();
let mut path = String::with_capacity(name.len() + hash.len() + 1);
path.push('_');
path.push_str(&name);
path.push_str(&hash);
Self {
path: Cow::Owned(path),
ty: PhantomData::<T>,
}
}
#[doc(hidden)]
pub fn __string(path: String) -> Self {
Signal {
path: Cow::Owned(path),
ty: PhantomData::<T>,
}
}
#[doc(hidden)]
pub fn __path(&self) -> &str {
self.path.as_ref()
}
#[doc(hidden)]
pub fn __computed_open(&self, buffer: &mut Buffer<DatastarSource>) -> usize {
let Some(close_count) =
push_signal_object_prefix(self.__path(), buffer.dangerously_get_string())
else {
return 0;
};
buffer.dangerously_get_string().push_str(":()=>");
close_count
}
}
impl Signal<()> {
#[doc(hidden)]
pub fn __computed_close(count: usize, buffer: &mut Buffer<DatastarSource>) {
close_signal_object(buffer.dangerously_get_string(), count);
}
}
impl<T: Render<DatastarSource>> Signal<T> {
#[doc(hidden)]
pub fn __assign(&self, buffer: &mut Buffer<DatastarSource>, v: T) {
let Some(close_count) = ({
let s = buffer.dangerously_get_string();
push_signal_object_prefix(self.__path(), s)
}) else {
return;
};
buffer.dangerously_get_string().push(':');
v.render_to(buffer);
close_signal_object(buffer.dangerously_get_string(), close_count);
}
}
impl<T> Render<DatastarSource> for Signal<T> {
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
let s = buffer.dangerously_get_string();
s.push('$');
push_datastar_source_to_html_attribute(s, self.__path());
}
}
#[derive(Debug, Clone, Copy)]
pub struct FormName(&'static str);
impl FormName {
#[doc(hidden)]
pub const fn __static(s: &'static str) -> Self {
Self(s)
}
}
impl Render<AttributeValue> for FormName {
fn render_to(&self, buffer: &mut Buffer<AttributeValue>) {
self.0.render_to(buffer);
}
}
impl Render<DatastarSource> for FormName {
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
self.0.render_to(buffer);
}
}
impl Render<ScriptSource> for FormName {
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
self.0.render_to(buffer);
}
}
#[doc(hidden)]
pub trait FormComponent {
type Form;
type FormNames;
const __FORM_NAMES: Self::FormNames;
}
const fn hash_component_location(
component_id: &str,
file: &'static str,
line: u32,
column: u32,
) -> u32 {
const FNV_OFFSET_BASIS_32: u32 = 0x811c9dc5;
const FNV_PRIME_32: u32 = 0x01000193;
const fn hash_bytes(mut hash: u32, bytes: &[u8]) -> u32 {
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u32;
hash = hash.wrapping_mul(FNV_PRIME_32);
i += 1;
}
hash
}
const fn hash_u32(hash: u32, value: u32) -> u32 {
hash_bytes(hash, &value.to_ne_bytes())
}
const fn hash_location(file: &'static str, line: u32, column: u32) -> u32 {
let hash = hash_bytes(FNV_OFFSET_BASIS_32, file.as_bytes());
let hash = hash_u32(hash, line);
hash_u32(hash, column)
}
hash_bytes(hash_location(file, line, column), component_id.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn element_id_renders_as_js_string() {
let id = ElementId::__dynamic("row-4<&\"'".to_string());
let mut buffer = Buffer::<DatastarSource>::new();
id.render_to(&mut buffer);
assert_eq!(
buffer.rendered().into_inner(),
r#"'row-4<&"\''"#
);
}
#[test]
fn form_name_renders_as_js_string() {
let name = FormName::__static("email");
let mut buffer = Buffer::<DatastarSource>::new();
name.render_to(&mut buffer);
assert_eq!(buffer.rendered().into_inner(), "'email'");
}
#[test]
fn signal_object_string_value() {
let signal = Signal::<&str>::__string("user['name']".to_string());
let mut buffer = Buffer::<DatastarSource>::new();
signal.__assign(&mut buffer, "Nick");
assert_eq!(buffer.rendered().into_inner(), r#"user:{name:'Nick'}"#);
}
#[test]
fn signal_object_number_value() {
let signal = Signal::<f64>::__string("user['age']".to_string());
let mut buffer = Buffer::<DatastarSource>::new();
signal.__assign(&mut buffer, -42.0);
assert_eq!(buffer.rendered().into_inner(), r#"user:{age:-42.0}"#);
}
#[test]
fn signal_object_unsafe_segment() {
let signal = Signal::<&str>::__string("project['user.123']['name']".to_string());
let mut buffer = Buffer::<DatastarSource>::new();
signal.__assign(&mut buffer, "Nick");
assert_eq!(
buffer.rendered().into_inner(),
r#"project:{'user.123':{name:'Nick'}}"#
);
}
#[test]
fn hash_different_locations() {
const HASH1: u32 = hash_component_location("diff", "src/main.rs", 10, 5);
const HASH2: u32 = hash_component_location("diff", "src/main.rs", 10, 6);
assert_ne!(HASH1, HASH2);
}
#[test]
fn hash_same_locations() {
const HASH1: u32 = hash_component_location("same", "src/main.rs", 42, 13);
const HASH2: u32 = hash_component_location("same", "src/main.rs", 42, 13);
assert_eq!(HASH1, HASH2);
}
}