use serde::Serialize;
pub fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
pub fn attr<T: Serialize>(value: &T) -> String {
escape(&serde_json::to_string(value).expect("value must serialize to JSON"))
}
pub fn slug(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
let mapped = match c {
'ç' | 'Ç' => 'c',
'ğ' | 'Ğ' => 'g',
'ı' | 'I' | 'İ' | 'i' => 'i',
'ö' | 'Ö' => 'o',
'ş' | 'Ş' => 's',
'ü' | 'Ü' => 'u',
c if c.is_ascii_alphanumeric() => c.to_ascii_lowercase(),
_ => '-',
};
if mapped == '-' {
if !out.is_empty() && !out.ends_with('-') {
out.push('-');
}
} else {
out.push(mapped);
}
}
while out.ends_with('-') {
out.pop();
}
out
}
#[derive(Clone, Copy)]
pub struct Render<'a> {
pub scope: &'a str,
pub locale: &'a str,
pub i18n: &'a crate::i18n::I18n,
pub tz: crate::time::TimeZone,
pub identity: Option<&'a crate::ctx::Identity>,
pub ctx: Option<&'a crate::ctx::Ctx>,
pub path: &'a str,
pub loaded: Option<&'a std::collections::HashMap<String, crate::screen::LoadedPage>>,
}
impl<'a> Render<'a> {
pub fn new(scope: &'a str, locale: &'a str, i18n: &'a crate::i18n::I18n) -> Self {
Self { scope, locale, i18n, tz: crate::time::TimeZone::UTC, identity: None, ctx: None, path: "", loaded: None }
}
pub fn with_identity(mut self, identity: &'a crate::ctx::Identity) -> Self {
self.identity = Some(identity);
self
}
pub fn can(&self, access: &crate::action::Access) -> bool {
self.identity.is_none_or(|i| i.can(access))
}
pub fn with_ctx(mut self, ctx: &'a crate::ctx::Ctx) -> Self {
self.ctx = Some(ctx);
self
}
pub fn with_tz(mut self, tz: crate::time::TimeZone) -> Self {
self.tz = tz;
self
}
pub fn english(scope: &'a str) -> Render<'a> {
Render::new(scope, "en", crate::i18n::I18n::builtin())
}
pub fn text(&self, text: &crate::i18n::Text) -> String {
escape(&self.i18n.text(self.locale, text))
}
pub fn label(&self, meta: &crate::model::FieldMeta) -> String {
self.text(&meta.label_text())
}
pub fn value(&self, value: &crate::value::Value, kind: &crate::value::FieldKind) -> String {
use crate::i18n::Text;
use crate::value::{FieldKind, Value};
match (value, kind) {
(Value::Null, _) => String::new(),
(Value::Bool(true), _) => self.text(&Text::key("zeyn.yes")),
(Value::Bool(false), _) => self.text(&Text::key("zeyn.no")),
(Value::Int(n), _) => n.to_string(),
(Value::Decimal(x), _) => {
let hundredths = (x * 100.0).round();
if hundredths.is_finite() {
escape(&crate::value::Money::new(hundredths as i64, Default::default()).format_plain(self.locale))
} else {
String::new()
}
}
(Value::Text(s), FieldKind::Choices(_)) => escape(&crate::ui::choices_text(s, kind, self)),
(Value::Text(s), _) => escape(s),
(Value::Money(m), _) => escape(&m.format(self.locale)),
(Value::Number(d), _) => escape(&d.format(self.locale)),
(Value::Date(d), _) => format!(r#"<time datetime="{d}">{}</time>"#, d.format(self.locale)),
(Value::Time(t), _) => format!(r#"<time datetime="{t}">{}</time>"#, t.format(self.locale)),
(Value::LocalDateTime(l), _) => format!(r#"<time datetime="{l}">{}</time>"#, l.format(self.locale)),
(Value::DateTime(d), _) => {
format!(r#"<time datetime="{d}" title="{d}">{}</time>"#, d.format(self.tz, self.locale))
}
(Value::Choice(v), FieldKind::Choice(options)) => match options.iter().find(|o| o.value == *v) {
Some(o) => self.text(&Text::key_or(o.key, o.label)),
None => escape(v),
},
(Value::Choice(v), _) => escape(v),
}
}
}