use crate::html::{escape_html, truncate_chars, SafeHtml};
use crate::value::Value;
pub const MAX_NAME: usize = 120;
pub const MAX_KEY: usize = 64;
pub const MAX_PROPS: usize = 30;
pub const MAX_VALUE: usize = 500;
pub const MAX_CLICK_NAME: usize = 100;
pub fn event_attrs<I, K, V>(name: &str, props: I) -> SafeHtml
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: Into<Value>,
{
attrs_to_html(&event_data(name, props))
}
pub fn event_data<I, K, V>(name: &str, props: I) -> Vec<(String, String)>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: Into<Value>,
{
let name = truncate_chars(name.trim(), MAX_NAME).trim();
if name.is_empty() {
return Vec::new();
}
let mut attrs = vec![("data-privatus-event".to_owned(), name.to_owned())];
for (key, value) in normalize(props) {
let text = string_value(&value);
attrs.push((
format!("data-privatus-prop-{key}"),
truncate_chars(&text, MAX_VALUE).to_owned(),
));
}
attrs
}
pub fn click_attr(name: &str) -> SafeHtml {
attrs_to_html(&click_data(name))
}
pub fn click_data(name: &str) -> Vec<(String, String)> {
if valid_click(name) {
vec![("data-privatus-click".to_owned(), name.to_owned())]
} else {
warn_invalid_click(name);
Vec::new()
}
}
pub fn valid_click(name: &str) -> bool {
!name.is_empty()
&& name.len() <= MAX_CLICK_NAME
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b':' | b'-'))
}
pub fn attrs_to_html(attrs: &[(String, String)]) -> SafeHtml {
let parts: Vec<String> = attrs
.iter()
.map(|(name, value)| format!("{name}=\"{}\"", escape_html(value)))
.collect();
SafeHtml::from_trusted(parts.join(" "))
}
#[cfg(debug_assertions)]
fn warn_invalid_click(name: &str) {
eprintln!(
"[privatus-analytics] click map name {name:?} is not valid. Use letters, digits and \
_ . : - only, at most 100 characters. Nothing was printed."
);
}
#[cfg(not(debug_assertions))]
fn warn_invalid_click(_name: &str) {}
pub fn normalize_key(key: &str) -> String {
key.to_lowercase()
.chars()
.map(|c| {
if c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' {
c
} else {
'_'
}
})
.take(MAX_KEY)
.collect()
}
pub(crate) fn normalize<I, K, V>(props: I) -> Vec<(String, Value)>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: Into<Value>,
{
let mut clean: Vec<(String, Value)> = Vec::new();
for (key, value) in props {
let key = normalize_key(key.as_ref());
let value = value.into();
if key.is_empty() || value == Value::Null {
continue;
}
match clean.iter_mut().find(|(k, _)| *k == key) {
Some(slot) => slot.1 = value,
None => clean.push((key, value)),
}
}
clean.truncate(MAX_PROPS);
clean
}
pub(crate) fn string_value(value: &Value) -> String {
match value {
Value::Null => String::new(),
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::UInt(n) => n.to_string(),
Value::Float(n) => n.to_string(),
Value::Str(s) => s.clone(),
Value::List(items) => items
.iter()
.filter(|item| **item != Value::Null)
.map(string_value)
.collect::<Vec<_>>()
.join(","),
}
}