feat: new styles
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
|
||||
pub mod opcodes;
|
||||
pub mod reader;
|
||||
pub mod rhei;
|
||||
pub mod style;
|
||||
pub mod types;
|
||||
|
||||
use std::borrow::Cow;
|
||||
pub use rhei::RheiContext;
|
||||
use style::StyleSheet as SS;
|
||||
pub use types::{ComponentDef, Document, Element, InterpError};
|
||||
@@ -11,7 +13,7 @@ pub use types::{ComponentDef, Document, Element, InterpError};
|
||||
use opcodes::*;
|
||||
use reader::Reader;
|
||||
use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX};
|
||||
use style::ComputedStyle;
|
||||
use style::{AncestorInfo, ComputedStyle, StructuralContext};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
@@ -21,8 +23,8 @@ static RE_VAR: OnceLock<Regex> = OnceLock::new();
|
||||
pub struct Interpreter;
|
||||
|
||||
impl Interpreter {
|
||||
pub fn run(bytecode: &[u8]) -> Result<Document, InterpError> {
|
||||
if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); }
|
||||
pub fn run<'a>(bytecode: &'a [u8]) -> Result<Document<'a>, InterpError> {
|
||||
if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); }
|
||||
if &bytecode[..4] != MAGIC { return Err(InterpError::BadMagic); }
|
||||
|
||||
let mut r = Reader::new(bytecode);
|
||||
@@ -41,20 +43,20 @@ impl Interpreter {
|
||||
true,
|
||||
)?;
|
||||
|
||||
let rhei_ctx = RheiContext::new(&rhei_scripts);
|
||||
rhei_ctx.initialize(&mut variables);
|
||||
//let rhei_ctx = RheiContext::new(&rhei_scripts);
|
||||
//rhei_ctx.initialize(&mut variables);
|
||||
|
||||
Ok(Document { roots, components, variables, rhei_scripts, stylesheet })
|
||||
}
|
||||
|
||||
fn parse_block_elements(
|
||||
r: &mut Reader,
|
||||
variables: &mut HashMap<String, String>,
|
||||
components: &mut HashMap<String, ComponentDef>,
|
||||
rhei_scripts: &mut Vec<String>,
|
||||
stylesheet: &mut SS,
|
||||
is_root: bool,
|
||||
) -> Result<Vec<Element>, InterpError> {
|
||||
fn parse_block_elements<'a>(
|
||||
r: &mut Reader<'a>,
|
||||
variables: &mut HashMap<String, String>,
|
||||
components: &mut HashMap<String, ComponentDef<'a>>,
|
||||
rhei_scripts: &mut Vec<String>,
|
||||
stylesheet: &mut SS,
|
||||
is_root: bool,
|
||||
) -> Result<Vec<Element<'a>>, InterpError> {
|
||||
let mut roots: Vec<Element> = Vec::new();
|
||||
let mut stack: Vec<Element> = Vec::new();
|
||||
|
||||
@@ -63,7 +65,7 @@ impl Interpreter {
|
||||
if !is_root && op == OP_END_BLOCK { break; }
|
||||
|
||||
match op {
|
||||
OP_ELEM_PUSH => stack.push(Element::new(r.read_string()?)),
|
||||
OP_ELEM_PUSH => stack.push(Element::new(r.read_str_ref()?)),
|
||||
|
||||
OP_ELEM_POP => {
|
||||
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
|
||||
@@ -90,53 +92,63 @@ impl Interpreter {
|
||||
|
||||
OP_CONTENT => {
|
||||
let vop = r.read_byte()?;
|
||||
if let Some(value) = r.read_value_as_string(vop)? {
|
||||
if vop == OP_PROP_RHEI {
|
||||
let expr_ref = r.read_str_ref()?;
|
||||
let mut val = String::with_capacity(RHEI_PREFIX.len() + expr_ref.len());
|
||||
val.push_str(RHEI_PREFIX);
|
||||
val.push_str(expr_ref);
|
||||
if let Some(el) = stack.last_mut() {
|
||||
let stored = if vop == OP_PROP_RHEI {
|
||||
format!("{RHEI_PREFIX}{value}")
|
||||
} else {
|
||||
value
|
||||
};
|
||||
el.properties.insert("text".to_string(), stored);
|
||||
el.push_prop("text".to_string(), val);
|
||||
}
|
||||
} else if let Some(value) = r.read_value_as_string(vop)? {
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.push_prop("text".to_string(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OP_PROP_STR => {
|
||||
let (key, val) = (r.read_string()?, r.read_string()?);
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
let key = r.read_str_ref()?;
|
||||
let val = r.read_str_ref()?;
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
}
|
||||
OP_PROP_VAR => {
|
||||
let key = r.read_string()?;
|
||||
let val = format!("${}", r.read_string()?);
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
let key = r.read_str_ref()?;
|
||||
let val_ref = r.read_str_ref()?;
|
||||
let mut val = String::with_capacity(val_ref.len() + 1);
|
||||
val.push('$');
|
||||
val.push_str(val_ref);
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
}
|
||||
OP_PROP_INT => {
|
||||
let key = r.read_string()?;
|
||||
let key = r.read_str_ref()?;
|
||||
let val = r.read_i64()?.to_string();
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
}
|
||||
OP_PROP_FLOAT => {
|
||||
let key = r.read_string()?;
|
||||
let key = r.read_str_ref()?;
|
||||
let val = r.read_f64()?.to_string();
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
}
|
||||
OP_PROP_BOOL => {
|
||||
let key = r.read_string()?;
|
||||
let key = r.read_str_ref()?;
|
||||
let val = (r.read_byte()? != 0).to_string();
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
}
|
||||
OP_PROP_RHEI => {
|
||||
let key = r.read_string()?;
|
||||
let expr = r.read_string()?;
|
||||
let key = r.read_str_ref()?;
|
||||
let expr_ref = r.read_str_ref()?;
|
||||
let mut val = String::with_capacity(RHEI_PREFIX.len() + expr_ref.len());
|
||||
val.push_str(RHEI_PREFIX);
|
||||
val.push_str(expr_ref);
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.properties.insert(key, format!("{RHEI_PREFIX}{expr}"));
|
||||
el.push_prop(key, val);
|
||||
}
|
||||
}
|
||||
OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
|
||||
let key = r.read_string()?;
|
||||
let key = r.read_str_ref()?;
|
||||
if let Some(val) = r.read_value_as_string(op)? {
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +157,8 @@ impl Interpreter {
|
||||
if is_root && stack.is_empty() {
|
||||
rhei_scripts.push(script);
|
||||
} else {
|
||||
let mut text_el = Element::new("#text".to_string());
|
||||
text_el.properties.insert(
|
||||
let mut text_el = Element::new("#text");
|
||||
text_el.push_prop(
|
||||
"text".to_string(),
|
||||
format!("{RHEI_PREFIX}{script}"),
|
||||
);
|
||||
@@ -187,12 +199,12 @@ impl Interpreter {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut if_el = Element::new("@if".to_string());
|
||||
if_el.properties.insert("condition".to_string(), cond_val);
|
||||
let mut if_el = Element::new("@if");
|
||||
if_el.push_prop("condition", cond_val);
|
||||
if_el.children = true_children;
|
||||
|
||||
if !false_children.is_empty() {
|
||||
let mut else_el = Element::new("@else".to_string());
|
||||
let mut else_el = Element::new("@else");
|
||||
else_el.children = false_children;
|
||||
if_el.children.push(else_el);
|
||||
}
|
||||
@@ -214,9 +226,9 @@ impl Interpreter {
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
)?;
|
||||
|
||||
let mut each_el = Element::new("@each".to_string());
|
||||
each_el.properties.insert("var_name".to_string(), var_name);
|
||||
each_el.properties.insert("source".to_string(), source_val);
|
||||
let mut each_el = Element::new("@each");
|
||||
each_el.push_prop("var_name".to_string(), var_name);
|
||||
each_el.push_prop("source".to_string(), source_val);
|
||||
each_el.children = block_children;
|
||||
Self::attach(&mut stack, &mut roots, each_el);
|
||||
}
|
||||
@@ -245,7 +257,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.properties.insert(
|
||||
el.push_prop(
|
||||
format!("__on:{event_name}"),
|
||||
handler_script,
|
||||
);
|
||||
@@ -278,20 +290,54 @@ impl Interpreter {
|
||||
Ok(roots)
|
||||
}
|
||||
|
||||
pub fn evaluate_vdom(
|
||||
templates: &[Element],
|
||||
variables: &HashMap<String, String>,
|
||||
components: &HashMap<String, ComponentDef>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
) -> Vec<Element> {
|
||||
pub fn evaluate_vdom<'a>(
|
||||
templates: &[Element<'a>],
|
||||
variables: &mut HashMap<String, String>,
|
||||
components: &HashMap<String, ComponentDef<'a>>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
ancestors: &[AncestorInfo],
|
||||
) -> Vec<Element<'a>> {
|
||||
let mut output = Vec::with_capacity(templates.len());
|
||||
|
||||
// Precompute sibling info for structural pseudo-classes and sibling combinators
|
||||
let sibling_infos: Vec<AncestorInfo> = templates.iter().map(|el| {
|
||||
AncestorInfo::new_with_id(
|
||||
el.type_name,
|
||||
el.id().map(String::from),
|
||||
el.get_prop("class")
|
||||
.map(|s| s.split_whitespace().map(String::from).collect())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}).collect();
|
||||
|
||||
// Precompute type totals for structural pseudo-class context
|
||||
let mut type_counts: HashMap<&str, usize> = HashMap::new();
|
||||
for el in templates {
|
||||
match el.type_name.as_str() {
|
||||
*type_counts.entry(el.type_name).or_insert(0) += 1;
|
||||
}
|
||||
let mut type_seen: HashMap<&str, usize> = HashMap::new();
|
||||
|
||||
for (i, el) in templates.iter().enumerate() {
|
||||
let type_idx = type_seen.entry(el.type_name).or_insert(0);
|
||||
let type_total = *type_counts.get(el.type_name).unwrap_or(&0);
|
||||
|
||||
let structural = StructuralContext {
|
||||
sibling_index: i,
|
||||
sibling_total: templates.len(),
|
||||
type_index: *type_idx,
|
||||
type_total,
|
||||
has_children: !el.children.is_empty()
|
||||
|| el.properties.iter().any(|(k, v)| k == "text" && !v.is_empty()),
|
||||
is_root: ancestors.is_empty(),
|
||||
};
|
||||
|
||||
*type_idx += 1;
|
||||
|
||||
match el.type_name {
|
||||
"@if" => {
|
||||
let cond = el.properties.get("condition").cloned().unwrap_or_default();
|
||||
let is_true = Self::evaluate_condition(&cond, variables, rhei);
|
||||
let cond = el.get_prop("condition").unwrap_or_default();
|
||||
let is_true = Self::evaluate_condition(cond, variables, rhei);
|
||||
|
||||
let mut active_branch = Vec::new();
|
||||
for child in &el.children {
|
||||
@@ -302,21 +348,19 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
output.extend(Self::evaluate_vdom(
|
||||
&active_branch, variables, components, rhei, stylesheet,
|
||||
&active_branch, variables, components, rhei, stylesheet, ancestors,
|
||||
));
|
||||
}
|
||||
|
||||
"@each" => {
|
||||
let var_name = el.properties.get("var_name").cloned().unwrap_or_default();
|
||||
let source_expr = el.properties.get("source").cloned().unwrap_or_default();
|
||||
let var_name = el.get_prop("var_name").unwrap_or_default();
|
||||
let source_expr = el.get_prop("source").unwrap_or_default();
|
||||
|
||||
let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) {
|
||||
rhei.eval_expr(expr, variables)
|
||||
.map(|s| Self::normalize_rhai_array(&s))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Self::resolve_string(&source_expr, variables)
|
||||
};
|
||||
Self::normalize_rhai_array(&rhei.eval_expr(expr, variables))
|
||||
} else {
|
||||
Self::resolve_string(source_expr, variables).into_owned()
|
||||
};
|
||||
|
||||
let items: Vec<String> = if resolved_source.is_empty() {
|
||||
vec![]
|
||||
@@ -325,73 +369,84 @@ impl Interpreter {
|
||||
};
|
||||
|
||||
for item in items {
|
||||
let mut local_vars = variables.clone();
|
||||
local_vars.insert(var_name.clone(), item);
|
||||
let old_val = variables.insert(var_name.to_string(), item);
|
||||
|
||||
output.extend(Self::evaluate_vdom(
|
||||
&el.children, &local_vars, components, rhei, stylesheet,
|
||||
&el.children, variables, components, rhei, stylesheet, ancestors,
|
||||
));
|
||||
|
||||
if let Some(old) = old_val {
|
||||
variables.insert(var_name.to_string(), old);
|
||||
} else {
|
||||
variables.remove(var_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
if let Some(comp) = components.get(&el.type_name) {
|
||||
// Expand custom component
|
||||
let mut comp_scope = variables.clone();
|
||||
if let Some(comp) = components.get(el.type_name) {
|
||||
let mut new_args = Vec::with_capacity(comp.params.len());
|
||||
for (param, _) in &comp.params {
|
||||
if let Some(arg) = el.properties.get(param) {
|
||||
comp_scope.insert(
|
||||
if let Some(arg) = el.get_prop(param) {
|
||||
new_args.push((
|
||||
param.clone(),
|
||||
Self::resolve_prop(arg, variables, rhei),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut vcomp = Element::new(el.type_name.clone());
|
||||
let mut old_vals = Vec::with_capacity(new_args.len());
|
||||
for (k, v) in new_args {
|
||||
old_vals.push((k.clone(), variables.insert(k, v)));
|
||||
}
|
||||
|
||||
let mut vcomp = Element::new(el.type_name);
|
||||
for (k, v) in &el.properties {
|
||||
if k.starts_with("__on:") {
|
||||
vcomp.properties.insert(k.clone(), Self::resolve_string(v, variables));
|
||||
vcomp.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned());
|
||||
} else {
|
||||
vcomp.properties.insert(k.clone(), Self::resolve_prop(v, variables, rhei));
|
||||
vcomp.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei));
|
||||
}
|
||||
}
|
||||
|
||||
vcomp.computed_style = ComputedStyle::compute(
|
||||
&vcomp.properties,
|
||||
stylesheet.resolve(&vcomp.type_name),
|
||||
);
|
||||
let matched_sheets = Self::collect_matching_styles(&vcomp, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]);
|
||||
vcomp.computed_style = ComputedStyle::compute(&vcomp.properties, &matched_sheets);
|
||||
|
||||
let child_ancestors = Self::build_ancestor_chain(ancestors, &vcomp);
|
||||
vcomp.children = Self::evaluate_vdom(
|
||||
&comp.children, &comp_scope, components, rhei, stylesheet,
|
||||
&comp.children, variables, components, rhei, stylesheet, &child_ancestors,
|
||||
);
|
||||
output.push(vcomp);
|
||||
|
||||
for (k, old) in old_vals.into_iter().rev() {
|
||||
if let Some(o) = old {
|
||||
variables.insert(k, o);
|
||||
} else {
|
||||
variables.remove(&k);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut vnode = Element::new(el.type_name.clone());
|
||||
let mut vnode = Element::new(el.type_name);
|
||||
|
||||
for (k, v) in &el.properties {
|
||||
if k.starts_with("__on:") {
|
||||
vnode.properties.insert(k.clone(), Self::resolve_string(v, variables));
|
||||
vnode.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned());
|
||||
continue;
|
||||
}
|
||||
if v.starts_with('$')
|
||||
&& !v[1..].contains(|c: char| !c.is_ascii_alphanumeric() && c != '_')
|
||||
{
|
||||
vnode.properties.insert(
|
||||
format!("__bind:{k}"),
|
||||
v[1..].to_string(),
|
||||
);
|
||||
vnode.set_prop(format!("__bind:{k}"), v[1..].to_string());
|
||||
}
|
||||
vnode.properties.insert(
|
||||
k.clone(),
|
||||
Self::resolve_prop(v, variables, rhei),
|
||||
);
|
||||
vnode.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei));
|
||||
}
|
||||
|
||||
vnode.computed_style = ComputedStyle::compute(
|
||||
&vnode.properties,
|
||||
stylesheet.resolve(&el.type_name),
|
||||
);
|
||||
let matched_sheets = Self::collect_matching_styles(&vnode, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]);
|
||||
vnode.computed_style = ComputedStyle::compute(&vnode.properties, &matched_sheets);
|
||||
|
||||
let child_ancestors = Self::build_ancestor_chain(ancestors, &vnode);
|
||||
vnode.children = Self::evaluate_vdom(
|
||||
&el.children, variables, components, rhei, stylesheet,
|
||||
&el.children, variables, components, rhei, stylesheet, &child_ancestors,
|
||||
);
|
||||
output.push(vnode);
|
||||
}
|
||||
@@ -402,14 +457,46 @@ impl Interpreter {
|
||||
output
|
||||
}
|
||||
|
||||
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String {
|
||||
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
||||
rhei.eval_expr(expr, variables).unwrap_or_default()
|
||||
} else {
|
||||
Self::resolve_string(v, variables)
|
||||
}
|
||||
fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec<AncestorInfo> {
|
||||
let mut chain = ancestors.to_vec();
|
||||
let id = el.id().map(String::from);
|
||||
let classes: Vec<String> = el
|
||||
.get_prop("class")
|
||||
.map(|s| s.split_whitespace().map(String::from).collect())
|
||||
.unwrap_or_default();
|
||||
chain.push(AncestorInfo::new_with_id(el.type_name, id, classes));
|
||||
chain
|
||||
}
|
||||
|
||||
fn collect_matching_styles<'a>(
|
||||
el: &Element,
|
||||
stylesheet: &'a SS,
|
||||
active_pseudo: &[&str],
|
||||
structural: &StructuralContext,
|
||||
ancestors: &[AncestorInfo],
|
||||
preceding_siblings: &[AncestorInfo],
|
||||
) -> Vec<&'a HashMap<String, String>> {
|
||||
let el_id = el.id();
|
||||
let classes: Vec<&str> = el
|
||||
.get_prop("class")
|
||||
.map(|s| s.split_whitespace().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let el_attributes: HashMap<String, String> = el.properties.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
stylesheet.matching_rules(el.type_name, el_id, &classes, active_pseudo, structural, ancestors, preceding_siblings, &el_attributes)
|
||||
}
|
||||
|
||||
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String {
|
||||
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
||||
rhei.eval_expr(expr, variables)
|
||||
} else {
|
||||
Self::resolve_string(v, variables).into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_condition(
|
||||
cond: &str,
|
||||
variables: &HashMap<String, String>,
|
||||
@@ -476,18 +563,38 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_string(val: &str, scope: &HashMap<String, String>) -> String {
|
||||
let re = RE_VAR.get_or_init(|| Regex::new(r"\$([a-zA-Z0-9_]+)").unwrap());
|
||||
re.replace_all(val, |caps: ®ex::Captures| {
|
||||
scope.get(&caps[1])
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(&caps[0])
|
||||
.to_string()
|
||||
})
|
||||
.to_string()
|
||||
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, String>) -> Cow<'a, str> {
|
||||
if !val.contains('$') {
|
||||
return Cow::Borrowed(val);
|
||||
}
|
||||
|
||||
fn attach(stack: &mut Vec<Element>, roots: &mut Vec<Element>, el: Element) {
|
||||
let mut result = String::with_capacity(val.len() + 16);
|
||||
let mut chars = val.char_indices().peekable();
|
||||
|
||||
while let Some((_, c)) = chars.next() {
|
||||
if c == '$' {
|
||||
let mut var_name = String::new();
|
||||
while let Some(&(_, next_c)) = chars.peek() {
|
||||
if next_c.is_ascii_alphanumeric() || next_c == '_' {
|
||||
var_name.push(chars.next().unwrap().1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(resolved) = scope.get(&var_name) {
|
||||
result.push_str(resolved);
|
||||
} else {
|
||||
result.push('$');
|
||||
result.push_str(&var_name);
|
||||
}
|
||||
} else {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
Cow::Owned(result)
|
||||
}
|
||||
|
||||
fn attach<'a>(stack: &mut Vec<Element<'a>>, roots: &mut Vec<Element<'a>>, el: Element<'a>) {
|
||||
match stack.last_mut() {
|
||||
Some(parent) => parent.children.push(el),
|
||||
None => roots.push(el),
|
||||
|
||||
Reference in New Issue
Block a user