REEEL: функции и аргументы в выражениях
Этот документ — справочник по вычисляемым выражениям
{...}
в формате REEEL: встроенным функциям, вызову функций, callback-выражениям с аргументами и регистрации пользовательских функций.
Общее описание синтаксиса
{expression}
— см. раздел
«Вычисляемые выражения»
в основном README.
Источник истины
— файл
packages/workspace/reeel_element_parser/lib/src/reeel_expression_function.dart
. Если документ расходится с кодом, верьте коду и обновите этот файл.
1. Вызов функций
Внутри выражения
{...}
можно вызывать функцию по её имени:
Text text:{upperCase(userName)}
Синтаксис — как в Dart: позиционные аргументы через запятую, именованные — как
name:value
(двоеточие, без обязательного пробела):
Text text:{plural(count, one:"файл", few:"файла", many:"файлов", other:"файлов")}
Позиционные аргументы передаются в функцию под целочисленными ключами
0, 1, 2…
в порядке записи, именованные — под строковыми ключами. Позиционные всегда идут до именованных.
Функцию можно использовать и внутри шаблонной строки:
State @userName:"ivan" @count:5
Text text:$"Привет, ${upperCase(userName)}! У тебя ${plural(count, one:"сообщение", few:"сообщения", many:"сообщений", other:"сообщений")}."
Поведение на ошибках
Функции спроектированы так, чтобы шаблоны не крашили приложение при неверных данных: в debug-сборке нарушения типов ловятся через
assert
, в release функция возвращает
null
. Индексы у
substring
автоматически clamp'ятся в границы строки.
⚠ Имена функций перекрываются переменными
Парсер сначала ищет идентификатор среди переменных текущей области видимости и только затем — среди зарегистрированных функций. Если в родительском элементе объявлена переменная с таким же именем, как у встроенной функции (
trim
,
length
,
replace
,
isEmpty
, …), функция в этой области станет недоступна.
Не называйте переменные именами встроенных функций.
Вызов методов значений (
$obj.method(args)
)
Вызывать можно не только зарегистрированную функцию по имени, но и
callable-значение
, полученное через
доступ к членам по точке
: если
obj.method
резолвится в вызываемое значение,
obj.method(args)
его вызывает с теми же позиционными/именованными аргументами.
// $theme/$strings — ambient-объекты, члены которых это и значения, и действия:
Button onPressed:(){ theme.set("dark") } // переключить тему
Button onPressed:(){ strings.set("ru") } // переключить язык
Text {strings.t("greeting", name:"REEEL")} // строка + подстановка
Ambient-объектов девять:
theme
,
strings
,
records
,
storage
,
navigator
,
input
,
file
,
consent
и
analytics
. Первые два переключают тему и язык;
`records`
— это ЗАПИСЬ в коллекции (чтение — сам элемент
Records
),
storage
— СБРОС переменных
Storage
(обычная запись — это присваивание
{goal = 2500}
).
navigator
— переходы и диалоги,
input
и
file
— ввод и файлы.
`consent`
— cookie-согласие на сайте:
$consent.analytics
/
$consent.marketing
читают выбор,
consent.open()
/
acceptAll()
/
rejectAll()
его меняют; в приложении чтения отвечают «не дано», вызовы ничего не делают.
analytics
— цели в счётчики сайта, объявленные в
project.json
(
settings.analytics
):
analytics.goal("order_sent")
или с параметрами —
analytics.goal("order_sent", mapOf("price", 1200))
; в приложении вызов — no-op.
Button onPressed:(){ records.add("intake", mapOf("ml", 250, "at", now())) }
// удалить по условию — один вызов, а не map/filter ради побочного эффекта:
Button onPressed:(){ records.removeWhere("intake", (r){dateKey(r['at'] ?? 0) == dateKey(today())}) }
Button onPressed:(){ records.clearAll(); storage.reset() } // «удалить все мои данные»
storage.reset()
возвращает объявленные
Storage
-ключи к их значениям по умолчанию и намеренно НЕ чистит хранилище целиком: токен авторизации, тема и локаль лежат там же и разметкой не объявлялись.
Если значение не вызываемое (опечатка/отсутствует) — выражение деградирует в
null
, а не падает (прикрывайте
??
). Для четырёх ambient-объектов опечатка в имени метода ловится валидацией (
unknown_namespace_method
). Подробнее —
Тема и локализация
.
2. Утилитарные функции
print(...)
Принимает только позиционные аргументы, печатает их через пробел в debug-лог и возвращает
null
. Предназначена для отладки.
Button onTap:{print("clicked", counter)}
plural(count, one:…, few:…, many:…, other:…, [zero:…, two:…])
ARB-совместимая плюрализация. Использует русские CLDR-правила (
one
/
few
/
many
/
other
); английский — их подмножество (только
one
и
other
). Необязательные
zero
и
two
— жёсткие override'ы для
count == 0
и
count == 2
соответственно (проверяются до категорий). Если нужная категория отсутствует, используется
other
.
// Русский
Text text:{plural(count, one:"сезон", few:"сезона", many:"сезонов", other:"сезонов")}
// Английский (достаточно one + other)
Text text:{plural(count, one:"item", other:"items")}
// Override на нуле
Text text:{plural(count, zero:"пусто", one:"один", few:"несколько", many:"много", other:"много")}
3. Строковые функции
Все строковые функции первым позиционным аргументом принимают строку.
| Функция | Сигнатура | Результат | Кратко |
|---|---|---|---|
lowerCase
|
lowerCase(s)
|
String | Приводит к нижнему регистру |
upperCase
|
upperCase(s)
|
String | Приводит к ВЕРХНЕМУ регистру |
length
|
length(s)
|
int | Длина строки |
trim
|
trim(s)
|
String | Удаляет пробелы по краям |
isEmpty
|
isEmpty(s)
|
bool | Пустая ли строка |
startsWith
|
startsWith(s, prefix)
|
bool | Начинается ли с префикса |
endsWith
|
endsWith(s, suffix)
|
bool | Заканчивается ли суффиксом |
contains
|
contains(s, needle)
|
bool | Содержит ли подстроку |
substring
|
substring(s, start, [end])
|
String |
Подстрока; индексы clamp'ятся в
[0, length]
|
replace
|
replace(s, from, to)
|
String | Литеральная замена всех вхождений (не regex) |
slugify
|
slugify(s)
/
slugify(s, maxLength)
|
String |
Отображаемое имя в адрес-слаг: кириллица транслитерируется, регистр опускается, всё прочее сводится в одиночные дефисы, края срезаются. С
maxLength
— обрезка без висящего дефиса. Пустой результат означает, что пригодного не осталось: чем заменить, решает вызывающий — общее слово вышло бы одинаковым у всех
|
split
|
split(s, separator)
|
List<String> | Разбиение на список по разделителю |
padLeft
|
padLeft(s, width, [pad=" "])
|
String |
Дополняет слева до
width
|
padRight
|
padRight(s, width, [pad=" "])
|
String |
Дополняет справа до
width
|
Примеры
// Регистр
Text text:{upperCase(title)}
Text text:{lowerCase(email)}
// Длина и проверка пустоты
Text text:$"Осталось: ${length(text)} символов"
Visibility visible:{!isEmpty(trim(query))}
// Поиск подстроки
Icon name:{startsWith(url, "https://") ? "lock" : "warning"}
Chip visible:{contains(tags, "new")}
// Подстрока и форматирование
Text text:{substring(fullName, 0, 1)}
Text text:{padLeft(minutes, 2, "0") + ":" + padLeft(seconds, 2, "0")}
// Замена
Text text:{replace(template, "{name}", userName)}
// Комбинация с тернарным оператором
Text text:{isEmpty(query) ? "Введите запрос" : "Найдено: " + length(query)}
3а. Функции преобразования
Явное приведение значения к типу. Текст из поля ввода — строка даже при
keyboardType:number
; агрегаты (
sum
,
sumBy
,
avg
) строки пропускают, поэтому записанное как текст число молча даёт ноль в сумме. Преобразование делается там, где значение записывается.
| Функция | Сигнатура | Результат | Кратко |
|---|---|---|---|
num
|
num(x)
|
num |
Число: число как есть,
true
/
false
→
1
/
0
, текст разбирается как литерал (
"12.5"
,
" 42 "
,
"1e3"
); целый текст остаётся int
|
int
|
int(x)
|
int |
num(x)
, усечённое к нулю:
int("12.7")
→
12
,
int(-3.9)
→
-3
|
double
|
double(x)
|
double |
num(x)
как дробное:
double("12")
→
12.0
|
string
|
string(x)
|
String |
Текст значения: число как записано (целый double без
.0
), флаг как
true
/
false
, список или словарь — JSON
|
bool
|
bool(x)
|
bool |
Строгая таблица:
true
/
false
, слова
"true"
/
"false"
(любой регистр), числа
1
/
0
; остальное —
null
|
Отказ —
null
, а не
0
.
num("abc")
,
bool("yes")
,
bool(2)
возвращают
null
и докладывают о неподходящем аргументе;
num(null)
—
null
без доклада (незагруженное значение — не ошибка). Там, где значение обязательно, ставьте
??
.
bool(x)
— намеренно
не
та истинность, что у
if:
: под
if:
текст
"yes"
и число
2
истинны, под
bool()
это отказы. Одно приводит записанное или введённое значение, другое решает, показывать ли элемент.
Арифметика по-прежнему сама разбирает числовую строку (
amount * 2
работает, не-число читается как
0
с диагностикой) — так раньше писали
amount * 1
вместо преобразования. Работает и сейчас, но читается как трюк.
+
— единственный оператор, который никогда не приводит: со строкой с любой стороны он склеивает,
"250" + 0
→
"2500"
.
State @amount:"250" @flag:"true" @tags:["a", "b"]
Column
Text text:{num(amount) + 50}
Text text:{int(amount) ?? 0}
Icon name:{bool(flag) == true ? "check" : "close"}
Text text:{string(tags)}
Появились в словаре 40. Схема записей (
Field … type:number
в
data/records/schema.reeel
) применяет то же правило при каждой записи и чтении — на обоих рендерах.
4. Callback с аргументами:
(args){body}
Callback — выражение
{...}
, значение которого вычисляется
при вызове
из Dart-кода (например,
onTap
,
onChanged
,
itemBuilder
), а не в момент построения элемента. Обычно callback либо изменяет реактивные переменные (
{scale=1.5;opacity=0.8}
), либо читает их.
Если Dart-код, вызывающий callback, передаёт
значения-аргументы
(
index
у билдера списка,
value
у
onChanged
и т. п.), эти значения можно принять, объявив список имён в скобках
перед
телом
{...}
:
// Принимает два аргумента "a" и "b", возвращает их сумму
Button onTap:(a, b){a + b}
// Билдер списка получает index от Dart-кода
ListBuilder itemBuilder:(index){$"Item #${index}"}
// TextField.onChanged передаёт новое значение
TextField onChanged:(value){text=value}
// Явно пустой список аргументов — Dart-код может передать что угодно, тело это игнорирует
Button onTap:(){counter=counter+1}
Откуда приходят значения
Парсер создаёт объект
ReeelCallbackVariable
, у которого заполнен
argumentNames
— список имён из
(args)
. Когда Dart-код вызывает callback, он передаёт
Map<String, dynamic>
с позиционными значениями, уложенными под эти имена:
// Внутри рендерера / платформенного кода
callback.call({'index': 42}); // внутри тела доступно как `index`
callback.call({'a': 2, 'b': 3}); // внутри доступны `a` и `b`
Область видимости
Аргументы, объявленные в
(args)
,
перекрывают
одноимённые переменные из родительских элементов. Это нужно, чтобы callback не путал переданное значение с внешней переменной:
@Row index:99
Button onTap:(index){index} // вернёт переданный index, а не 99
$Row
Многооператорное тело
Внутри тела можно записать несколько выражений через
;
. Результатом callback'а становится значение
последнего
выражения:
Button onTap:(x){x * 2; x + 1} // вернёт x + 1
Отличие от
{key=val;…}
-
{scale=1.5;opacity=0.8}— мутация переменных без объявления аргументов, значения «извне» не принимаются. -
(value){text=value}— явное объявление аргументаvalue, который Dart-код передаёт в callback (например,onChangedу поля ввода).
5. Регистрация пользовательских функций
Раздел для разработчиков, встраивающих REEEL-парсер в приложение. Авторам
.reeel
-файлов он не нужен.
Новую функцию можно добавить, реализовав интерфейс
ReeelExpressionFunction
и зарегистрировав её в
ReeelExpressionContext
. Аргументы приходят единой картой: позиционные по ключам
0, 1, 2…
, именованные по строковым ключам.
import 'package:reeel_element_parser/reeel_element_parser.dart';
class UpperFunction implements ReeelExpressionFunction {
@override
String get name => 'upper';
@override
dynamic call(Map<dynamic, dynamic> arguments) {
final s = arguments[0];
if (s is! String) return null;
return s.toUpperCase();
}
}
void registerCustomFunctions() {
ReeelExpressionContext.registerFunction(UpperFunction());
}
Регистрация глобальная — функция становится доступна во всех последующих парсингах, поэтому регистрируйте её один раз при старте приложения. Имя функции не должно пересекаться с именами, которые авторы могут использовать как переменные (см. предупреждение в разделе 1).
См. также
- REEEL Формат — основной README
-
Тема и локализация
— ambient
$theme/$strings - REEEL Format — English mirror
-
Исходник:
packages/workspace/reeel_element_parser/lib/src/reeel_expression_function.dart -
Тесты:
packages/workspace/reeel_element_parser/test/expression_functions_test.dart,packages/workspace/reeel_element_parser/test/callback_variable_test.dart,packages/workspace/reeel_element_parser/test/dotted_member_access_test.dart