2020-11-20 09:52:28 +01:00
|
|
|
//! Module implementing custom syntax for [`Engine`].
|
2020-07-23 12:40:42 +02:00
|
|
|
|
2020-10-29 04:37:51 +01:00
|
|
|
use crate::ast::Expr;
|
2021-06-16 06:24:36 +02:00
|
|
|
use crate::dynamic::Variant;
|
2020-12-13 07:31:24 +01:00
|
|
|
use crate::engine::EvalContext;
|
2020-11-16 16:10:14 +01:00
|
|
|
use crate::fn_native::SendSync;
|
2021-06-16 06:24:36 +02:00
|
|
|
use crate::r#unsafe::unsafe_try_cast;
|
2020-11-16 16:10:14 +01:00
|
|
|
use crate::token::{is_valid_identifier, Token};
|
|
|
|
use crate::{
|
2021-06-16 06:24:36 +02:00
|
|
|
Engine, Identifier, ImmutableString, LexError, ParseError, Position, RhaiResult, Shared,
|
|
|
|
StaticVec, INT,
|
2020-11-16 16:10:14 +01:00
|
|
|
};
|
2021-06-16 06:24:36 +02:00
|
|
|
use std::any::TypeId;
|
2021-04-17 09:15:54 +02:00
|
|
|
#[cfg(feature = "no_std")]
|
|
|
|
use std::prelude::v1::*;
|
2020-07-09 13:54:28 +02:00
|
|
|
|
2021-07-10 09:50:31 +02:00
|
|
|
/// Collection of special markers for custom syntax definition.
|
|
|
|
pub mod markers {
|
|
|
|
/// Special marker for matching an expression.
|
|
|
|
pub const CUSTOM_SYNTAX_MARKER_EXPR: &str = "$expr$";
|
|
|
|
/// Special marker for matching a statements block.
|
|
|
|
pub const CUSTOM_SYNTAX_MARKER_BLOCK: &str = "$block$";
|
|
|
|
/// Special marker for matching an identifier.
|
|
|
|
pub const CUSTOM_SYNTAX_MARKER_IDENT: &str = "$ident$";
|
|
|
|
/// Special marker for matching a single symbol.
|
|
|
|
pub const CUSTOM_SYNTAX_MARKER_SYMBOL: &str = "$symbol$";
|
|
|
|
/// Special marker for matching a string literal.
|
|
|
|
pub const CUSTOM_SYNTAX_MARKER_STRING: &str = "$string$";
|
|
|
|
/// Special marker for matching an integer number.
|
|
|
|
pub const CUSTOM_SYNTAX_MARKER_INT: &str = "$int$";
|
|
|
|
/// Special marker for matching a floating-point number.
|
|
|
|
#[cfg(not(feature = "no_float"))]
|
|
|
|
pub const CUSTOM_SYNTAX_MARKER_FLOAT: &str = "$float$";
|
|
|
|
/// Special marker for matching a boolean value.
|
|
|
|
pub const CUSTOM_SYNTAX_MARKER_BOOL: &str = "$bool$";
|
|
|
|
}
|
2020-12-13 07:31:24 +01:00
|
|
|
|
2020-07-23 09:49:09 +02:00
|
|
|
/// A general expression evaluation trait object.
|
2020-07-09 13:54:28 +02:00
|
|
|
#[cfg(not(feature = "sync"))]
|
2021-03-02 08:02:28 +01:00
|
|
|
pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression]) -> RhaiResult;
|
2020-07-23 09:49:09 +02:00
|
|
|
/// A general expression evaluation trait object.
|
2020-07-09 13:54:28 +02:00
|
|
|
#[cfg(feature = "sync")]
|
2021-03-02 08:02:28 +01:00
|
|
|
pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression]) -> RhaiResult + Send + Sync;
|
2020-07-09 13:54:28 +02:00
|
|
|
|
2020-10-25 14:57:18 +01:00
|
|
|
/// A general expression parsing trait object.
|
|
|
|
#[cfg(not(feature = "sync"))]
|
2020-12-13 07:31:24 +01:00
|
|
|
pub type FnCustomSyntaxParse =
|
2020-12-15 12:23:30 +01:00
|
|
|
dyn Fn(&[ImmutableString], &str) -> Result<Option<ImmutableString>, ParseError>;
|
2020-10-25 14:57:18 +01:00
|
|
|
/// A general expression parsing trait object.
|
|
|
|
#[cfg(feature = "sync")]
|
|
|
|
pub type FnCustomSyntaxParse =
|
2020-12-15 12:23:30 +01:00
|
|
|
dyn Fn(&[ImmutableString], &str) -> Result<Option<ImmutableString>, ParseError> + Send + Sync;
|
2020-10-25 14:57:18 +01:00
|
|
|
|
2020-11-20 09:52:28 +01:00
|
|
|
/// An expression sub-tree in an [`AST`][crate::AST].
|
2020-11-13 11:32:18 +01:00
|
|
|
#[derive(Debug, Clone)]
|
2020-07-22 11:05:13 +02:00
|
|
|
pub struct Expression<'a>(&'a Expr);
|
|
|
|
|
|
|
|
impl<'a> From<&'a Expr> for Expression<'a> {
|
2020-10-08 16:25:50 +02:00
|
|
|
#[inline(always)]
|
2020-07-22 11:05:13 +02:00
|
|
|
fn from(expr: &'a Expr) -> Self {
|
|
|
|
Self(expr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Expression<'_> {
|
2020-11-20 09:52:28 +01:00
|
|
|
/// If this expression is a variable name, return it. Otherwise [`None`].
|
2020-10-08 16:25:50 +02:00
|
|
|
#[inline(always)]
|
2021-06-12 16:47:43 +02:00
|
|
|
#[must_use]
|
2020-07-22 11:05:13 +02:00
|
|
|
pub fn get_variable_name(&self) -> Option<&str> {
|
2021-04-05 17:59:15 +02:00
|
|
|
self.0.get_variable_name(true)
|
2020-07-22 11:05:13 +02:00
|
|
|
}
|
|
|
|
/// Get the expression.
|
2020-10-08 16:25:50 +02:00
|
|
|
#[inline(always)]
|
2021-06-12 16:47:43 +02:00
|
|
|
#[must_use]
|
2021-06-29 12:41:03 +02:00
|
|
|
pub(crate) const fn expr(&self) -> &Expr {
|
2020-07-22 11:05:13 +02:00
|
|
|
&self.0
|
|
|
|
}
|
|
|
|
/// Get the position of this expression.
|
2020-10-08 16:25:50 +02:00
|
|
|
#[inline(always)]
|
2021-06-12 16:47:43 +02:00
|
|
|
#[must_use]
|
2021-06-29 12:41:03 +02:00
|
|
|
pub const fn position(&self) -> Position {
|
2020-07-22 11:05:13 +02:00
|
|
|
self.0.position()
|
|
|
|
}
|
2021-06-10 04:16:39 +02:00
|
|
|
/// Get the value of this expression if it is a literal constant.
|
2021-06-16 06:24:36 +02:00
|
|
|
/// Supports [`INT`][crate::INT], [`FLOAT`][crate::FLOAT], `()`, `char`, `bool` and
|
|
|
|
/// [`ImmutableString`][crate::ImmutableString].
|
|
|
|
///
|
|
|
|
/// Returns [`None`] also if the constant is not of the specified type.
|
2021-08-14 09:10:37 +02:00
|
|
|
#[inline]
|
2021-06-12 16:47:43 +02:00
|
|
|
#[must_use]
|
2021-06-16 06:24:36 +02:00
|
|
|
pub fn get_literal_value<T: Variant>(&self) -> Option<T> {
|
|
|
|
// Coded this way in order to maximally leverage potentials for dead-code removal.
|
|
|
|
|
|
|
|
if TypeId::of::<T>() == TypeId::of::<INT>() {
|
|
|
|
return match self.0 {
|
|
|
|
Expr::IntegerConstant(x, _) => unsafe_try_cast(*x).ok(),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
#[cfg(not(feature = "no_float"))]
|
|
|
|
if TypeId::of::<T>() == TypeId::of::<crate::FLOAT>() {
|
|
|
|
return match self.0 {
|
|
|
|
Expr::FloatConstant(x, _) => unsafe_try_cast(*x).ok(),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if TypeId::of::<T>() == TypeId::of::<char>() {
|
|
|
|
return match self.0 {
|
|
|
|
Expr::CharConstant(x, _) => unsafe_try_cast(*x).ok(),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if TypeId::of::<T>() == TypeId::of::<ImmutableString>() {
|
|
|
|
return match self.0 {
|
|
|
|
Expr::StringConstant(x, _) => unsafe_try_cast(x.clone()).ok(),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if TypeId::of::<T>() == TypeId::of::<bool>() {
|
|
|
|
return match self.0 {
|
|
|
|
Expr::BoolConstant(x, _) => unsafe_try_cast(*x).ok(),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if TypeId::of::<T>() == TypeId::of::<()>() {
|
|
|
|
return match self.0 {
|
|
|
|
Expr::Unit(_) => unsafe_try_cast(()).ok(),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
None
|
2021-06-10 04:16:39 +02:00
|
|
|
}
|
2020-07-22 11:05:13 +02:00
|
|
|
}
|
|
|
|
|
2021-06-29 17:17:31 +02:00
|
|
|
impl EvalContext<'_, '_, '_, '_, '_, '_, '_, '_> {
|
2020-11-25 02:36:06 +01:00
|
|
|
/// Evaluate an [expression tree][Expression].
|
2020-10-11 15:58:11 +02:00
|
|
|
///
|
2021-01-02 16:30:10 +01:00
|
|
|
/// # WARNING - Low Level API
|
2020-10-11 15:58:11 +02:00
|
|
|
///
|
2020-11-20 09:52:28 +01:00
|
|
|
/// This function is very low level. It evaluates an expression from an [`AST`][crate::AST].
|
2020-10-11 15:58:11 +02:00
|
|
|
#[inline(always)]
|
2021-03-02 08:02:28 +01:00
|
|
|
pub fn eval_expression_tree(&mut self, expr: &Expression) -> RhaiResult {
|
2020-10-20 12:09:26 +02:00
|
|
|
self.engine.eval_expr(
|
2020-10-19 13:11:55 +02:00
|
|
|
self.scope,
|
2020-10-11 15:58:11 +02:00
|
|
|
self.mods,
|
|
|
|
self.state,
|
2020-10-20 12:09:26 +02:00
|
|
|
self.lib,
|
2020-10-11 15:58:11 +02:00
|
|
|
self.this_ptr,
|
|
|
|
expr.expr(),
|
2020-10-20 12:09:26 +02:00
|
|
|
self.level,
|
2020-10-11 15:58:11 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-06 09:27:40 +01:00
|
|
|
/// Definition of a custom syntax definition.
|
2020-07-09 13:54:28 +02:00
|
|
|
pub struct CustomSyntax {
|
2021-08-26 17:58:41 +02:00
|
|
|
/// A parsing function to return the next token in a custom syntax based on the
|
|
|
|
/// symbols parsed so far.
|
2020-10-25 14:57:18 +01:00
|
|
|
pub parse: Box<FnCustomSyntaxParse>,
|
2020-11-06 09:27:40 +01:00
|
|
|
/// Custom syntax implementation function.
|
2020-07-09 13:54:28 +02:00
|
|
|
pub func: Shared<FnCustomSyntaxEval>,
|
2021-05-11 04:58:28 +02:00
|
|
|
/// Any variables added/removed in the scope?
|
2021-07-04 10:31:01 +02:00
|
|
|
pub scope_may_be_changed: bool,
|
2020-07-09 13:54:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Engine {
|
2020-11-20 09:52:28 +01:00
|
|
|
/// Register a custom syntax with the [`Engine`].
|
2020-10-11 15:58:11 +02:00
|
|
|
///
|
2021-08-26 17:58:41 +02:00
|
|
|
/// * `symbols` holds a slice of strings that define the custom syntax.
|
2021-07-04 10:40:15 +02:00
|
|
|
/// * `scope_may_be_changed` specifies variables _may_ be added/removed by this custom syntax.
|
2020-10-11 15:58:11 +02:00
|
|
|
/// * `func` is the implementation function.
|
2021-01-14 12:07:03 +01:00
|
|
|
///
|
2021-08-26 17:58:41 +02:00
|
|
|
/// ## Note on `symbols`
|
|
|
|
///
|
|
|
|
/// * Whitespaces around symbols are stripped.
|
|
|
|
/// * Symbols that are all-whitespace or empty are ignored.
|
|
|
|
/// * If `symbols` does not contain at least one valid token, then the custom syntax registration
|
|
|
|
/// is simply ignored.
|
|
|
|
///
|
2021-07-04 10:40:15 +02:00
|
|
|
/// ## Note on `scope_may_be_changed`
|
2021-01-14 12:07:03 +01:00
|
|
|
///
|
2021-07-04 10:40:15 +02:00
|
|
|
/// If `scope_may_be_changed` is `true`, then _size_ of the current [`Scope`][crate::Scope]
|
|
|
|
/// _may_ be modified by this custom syntax.
|
2021-01-14 12:07:03 +01:00
|
|
|
///
|
2021-07-04 10:40:15 +02:00
|
|
|
/// Adding new variables and/or removing variables count.
|
2021-05-11 04:58:28 +02:00
|
|
|
///
|
2021-07-04 10:40:15 +02:00
|
|
|
/// Simply modifying the values of existing variables does NOT count, as the _size_ of the
|
|
|
|
/// current [`Scope`][crate::Scope] is unchanged, so `false` should be passed.
|
2021-05-11 04:58:28 +02:00
|
|
|
///
|
2021-07-04 10:40:15 +02:00
|
|
|
/// Replacing one variable with another (i.e. adding a new variable and removing one variable at
|
|
|
|
/// the same time so that the total _size_ of the [`Scope`][crate::Scope] is unchanged) also
|
|
|
|
/// does NOT count, so `false` should be passed.
|
2021-03-29 05:36:02 +02:00
|
|
|
pub fn register_custom_syntax<S: AsRef<str> + Into<Identifier>>(
|
2020-07-09 13:54:28 +02:00
|
|
|
&mut self,
|
2021-08-26 17:58:41 +02:00
|
|
|
symbols: &[S],
|
2021-07-04 10:31:01 +02:00
|
|
|
scope_may_be_changed: bool,
|
2021-03-02 08:02:28 +01:00
|
|
|
func: impl Fn(&mut EvalContext, &[Expression]) -> RhaiResult + SendSync + 'static,
|
2020-08-05 04:00:20 +02:00
|
|
|
) -> Result<&mut Self, ParseError> {
|
2021-07-10 09:50:31 +02:00
|
|
|
use markers::*;
|
|
|
|
|
2021-09-11 13:40:40 +02:00
|
|
|
let mut segments = StaticVec::<ImmutableString>::new();
|
2020-07-09 13:54:28 +02:00
|
|
|
|
2021-08-26 17:58:41 +02:00
|
|
|
for s in symbols {
|
2020-07-22 11:05:13 +02:00
|
|
|
let s = s.as_ref().trim();
|
|
|
|
|
2021-08-26 17:58:41 +02:00
|
|
|
// Skip empty symbols
|
2020-07-22 11:05:13 +02:00
|
|
|
if s.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-12-26 16:21:09 +01:00
|
|
|
let token = Token::lookup_from_syntax(s);
|
|
|
|
|
2020-07-22 11:05:13 +02:00
|
|
|
let seg = match s {
|
2020-07-09 13:54:28 +02:00
|
|
|
// Markers not in first position
|
2021-06-28 12:06:05 +02:00
|
|
|
CUSTOM_SYNTAX_MARKER_IDENT
|
2021-07-10 09:50:31 +02:00
|
|
|
| CUSTOM_SYNTAX_MARKER_SYMBOL
|
2021-06-28 12:06:05 +02:00
|
|
|
| CUSTOM_SYNTAX_MARKER_EXPR
|
|
|
|
| CUSTOM_SYNTAX_MARKER_BLOCK
|
|
|
|
| CUSTOM_SYNTAX_MARKER_BOOL
|
|
|
|
| CUSTOM_SYNTAX_MARKER_INT
|
|
|
|
| CUSTOM_SYNTAX_MARKER_STRING
|
2021-06-10 04:16:39 +02:00
|
|
|
if !segments.is_empty() =>
|
|
|
|
{
|
|
|
|
s.into()
|
|
|
|
}
|
|
|
|
// Markers not in first position
|
|
|
|
#[cfg(not(feature = "no_float"))]
|
2021-06-28 12:06:05 +02:00
|
|
|
CUSTOM_SYNTAX_MARKER_FLOAT if !segments.is_empty() => s.into(),
|
2020-10-25 14:57:18 +01:00
|
|
|
// Standard or reserved keyword/symbol not in first position
|
2021-08-26 17:58:41 +02:00
|
|
|
_ if !segments.is_empty() && token.is_some() => {
|
2020-12-26 16:21:09 +01:00
|
|
|
// Make it a custom keyword/symbol if it is disabled or reserved
|
|
|
|
if (self.disabled_symbols.contains(s)
|
2021-08-26 17:58:41 +02:00
|
|
|
|| token.map_or(false, |v| v.is_reserved()))
|
2020-12-26 16:21:09 +01:00
|
|
|
&& !self.custom_keywords.contains_key(s)
|
|
|
|
{
|
2020-10-25 14:57:18 +01:00
|
|
|
self.custom_keywords.insert(s.into(), None);
|
2020-07-10 16:01:47 +02:00
|
|
|
}
|
|
|
|
s.into()
|
|
|
|
}
|
2021-08-13 13:23:20 +02:00
|
|
|
// Standard keyword in first position but not disabled
|
|
|
|
_ if segments.is_empty()
|
|
|
|
&& token.as_ref().map_or(false, |v| v.is_standard_keyword())
|
|
|
|
&& !self.disabled_symbols.contains(s) =>
|
2020-10-25 14:57:18 +01:00
|
|
|
{
|
2020-11-21 08:08:18 +01:00
|
|
|
return Err(LexError::ImproperSymbol(
|
|
|
|
s.to_string(),
|
|
|
|
format!(
|
|
|
|
"Improper symbol for custom syntax at position #{}: '{}'",
|
|
|
|
segments.len() + 1,
|
|
|
|
s
|
|
|
|
),
|
|
|
|
)
|
2021-07-24 08:11:16 +02:00
|
|
|
.into_err(Position::NONE));
|
2020-10-25 14:57:18 +01:00
|
|
|
}
|
|
|
|
// Identifier in first position
|
2021-08-13 13:23:20 +02:00
|
|
|
_ if segments.is_empty() && is_valid_identifier(s.chars()) => {
|
2020-12-26 16:21:09 +01:00
|
|
|
// Make it a custom keyword/symbol if it is disabled or reserved
|
2021-08-13 13:23:20 +02:00
|
|
|
if self.disabled_symbols.contains(s) || token.map_or(false, |v| v.is_reserved())
|
2020-12-26 16:21:09 +01:00
|
|
|
{
|
2021-08-13 13:23:20 +02:00
|
|
|
if !self.custom_keywords.contains_key(s) {
|
|
|
|
self.custom_keywords.insert(s.into(), None);
|
|
|
|
}
|
2020-07-09 13:54:28 +02:00
|
|
|
}
|
|
|
|
s.into()
|
|
|
|
}
|
|
|
|
// Anything else is an error
|
2020-07-22 11:05:13 +02:00
|
|
|
_ => {
|
2020-11-21 08:08:18 +01:00
|
|
|
return Err(LexError::ImproperSymbol(
|
|
|
|
s.to_string(),
|
|
|
|
format!(
|
|
|
|
"Improper symbol for custom syntax at position #{}: '{}'",
|
|
|
|
segments.len() + 1,
|
|
|
|
s
|
|
|
|
),
|
|
|
|
)
|
2021-07-24 08:11:16 +02:00
|
|
|
.into_err(Position::NONE));
|
2020-07-22 11:05:13 +02:00
|
|
|
}
|
2020-07-09 13:54:28 +02:00
|
|
|
};
|
|
|
|
|
2020-10-27 02:56:37 +01:00
|
|
|
segments.push(seg);
|
2020-07-09 13:54:28 +02:00
|
|
|
}
|
|
|
|
|
2021-08-26 17:58:41 +02:00
|
|
|
// If the syntax has no symbols, just ignore the registration
|
2020-07-22 11:05:13 +02:00
|
|
|
if segments.is_empty() {
|
|
|
|
return Ok(self);
|
|
|
|
}
|
|
|
|
|
2020-10-25 14:57:18 +01:00
|
|
|
// The first keyword is the discriminator
|
|
|
|
let key = segments[0].clone();
|
|
|
|
|
|
|
|
self.register_custom_syntax_raw(
|
|
|
|
key,
|
|
|
|
// Construct the parsing function
|
2020-12-15 12:23:30 +01:00
|
|
|
move |stream, _| {
|
2020-10-25 14:57:18 +01:00
|
|
|
if stream.len() >= segments.len() {
|
|
|
|
Ok(None)
|
|
|
|
} else {
|
|
|
|
Ok(Some(segments[stream.len()].clone()))
|
|
|
|
}
|
|
|
|
},
|
2021-07-04 10:31:01 +02:00
|
|
|
scope_may_be_changed,
|
2020-10-25 14:57:18 +01:00
|
|
|
func,
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(self)
|
|
|
|
}
|
2020-11-20 09:52:28 +01:00
|
|
|
/// Register a custom syntax with the [`Engine`].
|
2020-10-25 14:57:18 +01:00
|
|
|
///
|
2021-01-02 16:30:10 +01:00
|
|
|
/// # WARNING - Low Level API
|
2020-10-25 14:57:18 +01:00
|
|
|
///
|
|
|
|
/// This function is very low level.
|
|
|
|
///
|
2021-07-04 10:31:01 +02:00
|
|
|
/// * `scope_may_be_changed` specifies variables have been added/removed by this custom syntax.
|
2020-10-25 14:57:18 +01:00
|
|
|
/// * `parse` is the parsing function.
|
|
|
|
/// * `func` is the implementation function.
|
|
|
|
///
|
2021-08-26 17:58:41 +02:00
|
|
|
/// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`].
|
|
|
|
/// Otherwise, they won't be recognized.
|
2020-10-25 14:57:18 +01:00
|
|
|
pub fn register_custom_syntax_raw(
|
|
|
|
&mut self,
|
2021-03-29 05:36:02 +02:00
|
|
|
key: impl Into<Identifier>,
|
2020-12-15 12:23:30 +01:00
|
|
|
parse: impl Fn(&[ImmutableString], &str) -> Result<Option<ImmutableString>, ParseError>
|
2020-12-13 07:31:24 +01:00
|
|
|
+ SendSync
|
|
|
|
+ 'static,
|
2021-07-04 10:31:01 +02:00
|
|
|
scope_may_be_changed: bool,
|
2021-03-02 08:02:28 +01:00
|
|
|
func: impl Fn(&mut EvalContext, &[Expression]) -> RhaiResult + SendSync + 'static,
|
2020-10-25 14:57:18 +01:00
|
|
|
) -> &mut Self {
|
2021-05-03 07:45:41 +02:00
|
|
|
self.custom_syntax.insert(
|
|
|
|
key.into(),
|
2021-06-29 12:25:20 +02:00
|
|
|
CustomSyntax {
|
2021-05-03 07:45:41 +02:00
|
|
|
parse: Box::new(parse),
|
|
|
|
func: (Box::new(func) as Box<FnCustomSyntaxEval>).into(),
|
2021-07-04 10:31:01 +02:00
|
|
|
scope_may_be_changed,
|
2021-06-29 12:25:20 +02:00
|
|
|
}
|
|
|
|
.into(),
|
2021-05-03 07:45:41 +02:00
|
|
|
);
|
2020-10-25 14:57:18 +01:00
|
|
|
self
|
2020-07-09 13:54:28 +02:00
|
|
|
}
|
|
|
|
}
|