Ask the assistant
0/400
Your question is processed by an AI service and kept to improve answers. Do not enter personal data.

    REEEL: функции и аргументы в выражениях

    This page has not been translated into English yet. The Russian original is below.

    Этот документ — справочник по вычисляемым выражениям {...} в формате 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
    DOCS
    dark_mode_baseline
    RU/EN
    menu_baseline