1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
use serde::Serialize;
/// Escapes text and attribute values for HTML. User data always goes through here.
pub fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
escape_into(&mut out, s);
out
}
/// Appends `s` to `out` escaped for HTML, as [`escape`] does, without a new string: runs without
/// a character to escape are copied in one piece.
pub fn escape_into(out: &mut String, s: &str) {
let mut clean = 0;
for (i, b) in s.bytes().enumerate() {
let entity = match b {
b'&' => "&",
b'<' => "<",
b'>' => ">",
b'"' => """,
b'\'' => "'",
_ => continue,
};
out.push_str(&s[clean..i]);
out.push_str(entity);
clean = i + 1;
}
out.push_str(&s[clean..]);
}
/// Serializes a value to JSON and escapes it for use inside an attribute.
pub fn attr<T: Serialize>(value: &T) -> String {
escape(&serde_json::to_string(value).expect("value must serialize to JSON"))
}
/// Simplifies a label for test ids, including Turkish letters: `"Yeni Ürün"` -> `"yeni-urun"`.
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
}
/// What a component needs while rendering: test id scope and the user's language.
#[derive(Clone, Copy)]
pub struct Render<'a> {
pub scope: &'a str,
pub locale: &'a str,
pub i18n: &'a crate::i18n::I18n,
/// Zone in which moments (`DateTime`) are shown.
pub tz: crate::time::TimeZone,
/// Who the page is for. Components leave out actions this identity may not run. `None` (tests
/// and tools) shows everything; the server always sets it.
pub identity: Option<&'a crate::ctx::Identity>,
/// The request, when rendering for a user: decides who may reveal masked values and records
/// reads of sensitive records. `None` treats the viewer as having no permissions.
pub ctx: Option<&'a crate::ctx::Ctx>,
/// Path of the node being rendered, such as `1.0.2`; empty outside a node.
pub path: &'a str,
/// First pages of grids loaded before rendering, by node path.
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,
}
}
/// Sets who the page is for, so components can leave out what this user may not do.
pub fn with_identity(mut self, identity: &'a crate::ctx::Identity) -> Self {
self.identity = Some(identity);
self
}
/// Whether the page's user passes an access rule. Without an identity, true.
pub fn can(&self, access: &crate::action::Access) -> bool {
self.identity
.is_none_or(|i| i.can(access))
}
/// Renders for the user of this request.
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
}
/// English with the built in catalogs. For tests and tools.
pub fn english(scope: &'a str) -> Render<'a> {
Render::new(scope, "en", crate::i18n::I18n::builtin())
}
/// Resolves a text and escapes it for HTML.
pub fn text(&self, text: &crate::i18n::Text) -> String {
escape(&self.i18n.text(self.locale, text))
}
/// Field label in the user's language, escaped.
pub fn label(&self, meta: &crate::model::FieldMeta) -> String {
self.text(&meta.label_text())
}
/// Value as shown to the user, escaped: money and yes/no in the user's language, choices by label.
pub fn value(&self, value: &crate::value::Value, kind: &crate::value::FieldKind) -> String {
let mut out = String::new();
self.value_into(&mut out, value, kind);
out
}
/// Appends [`Render::value`] to `out` without building a string for the value first, for
/// renderers that write many values, such as grid rows.
pub fn value_into(
&self,
out: &mut String,
value: &crate::value::Value,
kind: &crate::value::FieldKind,
) {
use crate::i18n::Text;
use crate::value::{FieldKind, Value};
use std::fmt::Write;
match (value, kind) {
(Value::Null, _) => {}
(Value::Bool(true), _) => escape_into(
out,
&self
.i18n
.text(self.locale, &Text::key("zeyn.yes")),
),
(Value::Bool(false), _) => escape_into(
out,
&self
.i18n
.text(self.locale, &Text::key("zeyn.no")),
),
(Value::Int(n), _) => {
let _ = write!(out, "{n}");
}
(Value::Decimal(x), _) => {
let hundredths = (x * 100.0).round();
if hundredths.is_finite() {
let plain = crate::value::Money::new(hundredths as i64, Default::default())
.format_plain(self.locale);
escape_into(out, &plain);
}
}
(Value::Text(s), FieldKind::Choices(_)) => {
escape_into(out, &crate::ui::choices_text(s, kind, self))
}
(Value::Text(s), _) => escape_into(out, s),
(Value::Money(m), _) => escape_into(out, &m.format(self.locale)),
(Value::Number(d), _) => escape_into(out, &d.format(self.locale)),
(Value::Date(d), _) => {
let _ = write!(
out,
r#"<time datetime="{d}">{}</time>"#,
d.format(self.locale)
);
}
(Value::Time(t), _) => {
let _ = write!(
out,
r#"<time datetime="{t}">{}</time>"#,
t.format(self.locale)
);
}
(Value::LocalDateTime(l), _) => {
let _ = write!(
out,
r#"<time datetime="{l}">{}</time>"#,
l.format(self.locale)
);
}
(Value::DateTime(d), _) => {
let _ = write!(
out,
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) => escape_into(
out,
&self
.i18n
.text(self.locale, &Text::key_or(o.key, o.label)),
),
None => escape_into(out, v),
}
}
(Value::Choice(v), _) => escape_into(out, v),
}
}
}