CyberTrainРуководстваСправочник по языку шаблонов Cybertrain

Справочник по языку шаблонов Cybertrain

Представления Cybertrain (app/views/**/*.html.erb) выглядят как ERB из Rails, но не являются Ruby: они разбираются во время выполнения (в режиме разработки и в тестах — из app/views/ на диске; в продакшен-бинарнике — из таблицы строк, которую cybertrain build встраивает через gen/views.rb), разбиваются на токены и разбираются в AST, один раз компилируются в плоское дерево узлов и обходятся интерпретатором, проходящим по дереву.

Этот документ описывает ровно то, что этот конвейер принимает и выполняет. Там, где он расходится с разделом 7 docs/design.md (исходным черновиком спецификации) или с ожиданиями разработчика на Rails, прав код, и этот документ об этом говорит (ищите «As built»).

Что здесь является Ruby, а что нет

Шаблоны не являются Ruby. В Spinel (AOT-компиляторе Ruby от Matz, на который нацелен cybertrain) нет eval, поэтому вариант «интерпретировать собственный Ruby представления во время запроса» даже не рассматривался (D9 в docs/design.md); вместо этого выполняется небольшая замкнутая грамматика, которая лишь внешне похожа на синтаксис ERB/Ruby. Внутри тега нет произвольного Ruby — только то, что ExprParser (parser.rb) распознаёт как инструкцию или выражение. Нет eval, send, instance_eval, определений методов/классов/модулей, require, исключений (begin/rescue), case/when, диапазонов, регулярных выражений и множественного присваивания. Единственные блоки — each, each_with_index и вызов хелпера (form_with(...) do |f| ... end): нет блоков { }, нет map/select/любого другого метода Enumerable с блоком. Каждый вызов метода (recv.name) разрешается написанными вручную таблицами диспетчеризации case в interpreter.rb по классу Ruby получателя во время выполнения; имя, которого нет в таблице этого типа, приводит к RuntimeError — нет ни method_missing, ни рефлексии, и (согласно Interpreter#call_method) именно отсутствие send в Spinel объясняет, почему это case, а не поиск через метапрограммирование. Шаблон может делать только то, что описано в этом документе.

Где это находится в исходникахвосемь файлов конвейера шаблонов

Конвейер находится в cybertrain/template/:

Файл Роль
lexer.rb Делит исходный текст на теги <% %>/<%= %>/<%== %>/<%# %> и обычный текст
parser.rb TreeBuilder вкладывает теги в AST; ExprParser разбирает грамматику выражений внутри одного тега
ast.rb Классы узлов AST, которые создаёт парсер
inode.rb Compile уплощает AST в мономорфные INode (целочисленный «вид» плюс типизированные слоты) для быстрой интерпретации
interpreter.rb Обходит INode по env (Hash<String, value>); содержит все таблицы «какие методы существуют» для каждого типа
helpers.rb link_to, form_with, render, pluralize, ... — всё, что шаблон вызывает без получателя
form_builder.rb f, передаваемый в form_with(...) do |f| ... end
engine.rb Загружает исходники .html.erb — с диска из корневого каталога или из встроенной таблицы gen/views.rb (Engine.embedded), — разбирает их (и при необходимости кэширует), отрисовывает с макетом

Теги

Тег Значение
<% code %> Выполняет инструкцию; ничего не выводит
<%= expr %> Вычисляет expr и выводит его с HTML-экранированием
<%== expr %> Вычисляет expr и выводит его без экранирования
<%# comment %> Игнорируется (кроме формы locals: в строке 1, см. ниже)

Управление пробельными символами (Lexer.tokenize):

  • Тег <% %>/<%# %>, который является единственным содержимым своей строки (по обе стороны до переводов строки — только пробелы/табуляции), молча поглощает свой отступ и завершающий перевод строки, как режим обрезки ERB в Rails. <%= %>/<%== %> никогда не обрезаются так автоматически, даже если стоят на строке одни.
  • <%- явно удаляет пробелы/табуляции перед тегом, а -%> удаляет один перевод строки сразу после него — для тега любого вида. Оба ничего не делают, если с этой стороны строки есть что-то кроме пробельных символов.
  • <%% в тексте — буквальный <%; %%> внутри тега — буквальный %>, который не закрывает тег.
  • <% # a Ruby-style comment %> (тег кода, содержимое которого начинается с #) игнорируется, как <%# %>. Комментарий после настоящего кода в том же теге не поддерживается — <% x = 1 # note %> даёт SyntaxError.
  • Незакрытые теги вызывают ошибку на этапе лексического анализа: "posts/show:2: unterminated <% tag".

Комментарий строгих локальных переменных

Тег комментария, содержимое которого начинается с locals: и который является самым первым тегом файла, в строке 1, объявляет единственные локальные переменные, которые принимает шаблон (почти всегда партиал):

erb
<%# locals: (post:, comment:) %>
<%= post.title %>
  • <%# locals: () %> (пустой список) означает «этот партиал не принимает локальных переменных».
  • Каждая запись должна быть голым name: в нижнем регистре (Lexer.identifier? принимает только [a-z_][a-z0-9_]*). As built: в отличие от Rails 7.1, записи не могут иметь значение по умолчанию — <%# locals: (post:, comment: nil) %>, ровно та форма, которую поддерживает Rails 7.1, даёт SyntaxError: "t:1: bad strict locals entry 'comment: nil' (expected name:)", потому что comment: nil не оканчивается на :. Фактически каждая объявленная локальная переменная обязательна. Некорректная спецификация (без скобок) вызывает "t:1: bad strict locals comment (expected locals: (name:, ...))".
  • Без этого комментария шаблон не объявляет списка локальных переменных: render передаёт любые полученные локальные переменные без проверки, и шаблон может также читать всё, что уже есть в окружении вызывающего (см. Макеты, yield, content_for, партиалы).
  • Если перед комментарием на более ранней строке что-то есть (даже одна пустая строка), это просто обычный, ни на что не влияющий комментарий, а не объявление локальных переменных.

Выражения

Литералы

Литерал Синтаксис Примечания
Integer 1, 1_000 подчёркивания разрешены; нет шестнадцатеричной/восьмеричной/двоичной записи и экспоненты
Float 2.5, 12_3.4_5 цифра обязательна с обеих сторон .; экспоненциальной формы нет
String "...", '...' двойные кавычки поддерживают интерполяцию и escape-последовательности; одинарные понимают только \' и \\
Symbol :title только символы идентификатора — нет :"quoted" и символов-операторов вроде :+
nil / true / false    
Array [1, "a", :b], [] элементы — полноценные выражения
Hash { a: 1, b: "x" }, {} только ключи в стиле символов, той же формы, что и именованные аргументы; без "key" => value (hashrocket)

Строки в двойных кавычках интерполируют #{...} (любое полноценное выражение, допускается вложение) и поддерживают \n \t \r \0 \e \s, \", \\; любой другой экранированный символ проходит без изменений. Строка без #{} компилируется в простой литерал; строка с интерполяцией — в последовательность литеральных фрагментов и выражений.

Переменные

  • @ivar напрямую читает env["ivar"] — @ лишь внешний синтаксис; согласно D9, «post — это и @post, и локальная post» в одном Hash окружения. Отсутствующая переменная экземпляра — просто nil: [<%= @missing %>] выводит [], никогда не ошибку.
  • Голое имя локальной переменной (post, title) тоже читает env["name"], но если такого ключа вовсе нет, интерпретатор считает это вызовом хелпера без аргументов. Если ключ существует (даже привязанный к nil — скажем, объявленная, но не переданная строгая локальная переменная), возвращается его значение, без перехода к хелперу. Поэтому <%= csrf_meta_tags %> и <%= flash %> работают без скобок, а <%= nope %> завершается ошибкой "undefined helper 'nope'", а не «нет такой локальной переменной».
  • Локальные переменные берутся из параметров блоков each/each_with_index, параметра блока form_with, строгих локальных переменных партиала и <% name = expr %>.

Вызовы методов

erb
recv.method
recv.method(arg1, key: value)
recv&.method             # безопасно для nil: если recv равен nil, сразу даёт nil, аргументы не вычисляются
method(arg1, key: value) # вызов хелпера: без получателя
method arg1, key: value  # тот же вызов без скобок («командный» синтаксис)
  • Список аргументов без скобок («командный») открывается только для самого внешнего вызова в выражении: link_to "Show", post_path(post) работает, а link_to "Show", post_path post даёт SyntaxError — внутренним вызовам нужны собственные скобки.
  • Между целью вызова и её открывающей ( пробел не допускается: работают и link_to("x"), и link_to "x"; link_to ("x") даёт SyntaxError.
  • yield и yield :key всегда разбираются как вызовы (см. Макеты, yield, content_for, партиалы).
  • Голое слово с заглавной буквы сразу отвергается — "constants are not supported: 'Time'": шаблоны не могут обращаться к константам Ruby.
  • Эти слова зарезервированы и отвергаются с отдельным сообщением ("'x' is not supported in templates"): and or not do end if unless elsif else then while until case when def class module begin rescue return.

Операторы, приоритет (от низкого к высокому)

text
?:            ternary, right-associative on both branches
||
&&
==  !=
<  >  <=  >=
+  -
*  /  %
!  -            (unary, prefix)
.  &.  []       (method call / safe-nav / indexing, left-to-right chaining)

&&/|| возвращают операнд, а не приведённое булево значение (nothing || title вычисляется в title, а не в true). Условия проверяют истинность: ложны только nil и false, как в Ruby, — 0, "" и пустые массивы/хеши истинны (<% if 0 %>yes<% end %> выводит yes).

Индексирование — это target[index] без пробела перед [ (items [0] — синтаксическая ошибка, items[0] — нет), и оно образует цепочки, как любой постфикс: h["k"].first, params[:id].

Инструкции

Тег <% %> содержит ровно одну инструкцию: присваивание или выражение, вычисляемое ради побочного эффекта (значение отбрасывается). <%= %>/<%== %> всегда содержат только выражение — <%= x = 1 %> даёт SyntaxError.

Условия

erb
<% if cond %>...<% elsif other %>...<% else %>...<% end %>
<% unless cond %>...<% else %>...<% end %>

unless/else/elsif следуют правилам Ruby; elsif после else, лишние else/elsif/end или блок, так и не закрытый end, — всё это SyntaxError с указанием строки открытия.

Циклы

erb
<% @posts.each do |post| %>...<% end %>
<% @counts.each do |name, n| %>...<% end %>          <%# Hash: name=key, n=value %>
<% items.each_with_index do |item, i| %>...<% end %>
  • each/each_with_index распознаются только как суффикс заголовка блока (x.each do |...|). As built: оба требуют объявить один или два параметра блока — xs.each do end даёт SyntaxError («takes one or two block parameters»), тогда как обычный Ruby допускает ноль.
  • Array + один параметр: элемент. Array + два параметра: элемент-Array деструктурируется (a, b = item[0], item[1]); иначе второй — nil.
  • each_with_index на Array: item, i. На Hash: первый параметр — пара [key, value], второй — индекс.
  • each на Hash с двумя параметрами: непосредственно key, value; с одним параметром — пара [key, value].
  • Перебор nil, Time или любого скаляра — это RuntimeError ("undefined method 'each' for Time"), никогда не молчаливое бездействие.
  • Параметр блока затеняет одноимённую внешнюю локальную переменную только на время цикла и после него удаляется (refute env.key?("item") после окончания цикла). Переменная, существовавшая до блока, внутри него — та же самая переменная (видна и после цикла); переменная, впервые присвоенная внутри блока, локальна для блока — nil в начале каждой итерации и исчезает после.

Вызовы с блоком (хелперы)

do ... end — это также способ передать блок вызову хелпера без получателя — единственное другое допустимое использование блока, и только когда получателя нет (f.each do |x| end отвергается: «blocks are only supported on each, each_with_index and helper calls»):

erb
<%= form_with(model: @post) do |f| %>...<% end %>   <%# printed: form_with returns the <form> HTML %>
<% content_for :title do %>Hi<% end %>              <%# not printed: content_for returns "" %>

Выводится ли результат блока, зависит от тега: <%= helper do |x| %> его выводит, <% helper do |x| %> отбрасывает. Привязывается всегда ровно один параметр блока; блок хелпера никогда не получает индекс или второе значение.

Присваивание

erb
<% total = 0 %><% total = total + 2 %><%= total %>   <%# => 2 %>

Только name = expr для одной простой локальной переменной — нет @ivar = ..., нет arr[0] = .../h[:k] = ..., нет a, b = .... То же правило локальности для блока, что и у each, применяется к присваиваниям внутри if/unless, вложенных в блок.

Таблица методов по типам

nil?, present? и blank? проверяются до диспетчеризации по типу, поэтому работают на любом значении, включая модели и объекты, возвращённые хелперами (blank? равен false для всего, кроме nil/false/пустых SafeString/String/Array/Hash/Errors/Params). Арифметика (+ - * / %) и сравнение (== != < > <= >=) существуют только как инфиксные операторы, никогда как вызовы .method — в этой грамматике символ оператора не может стоять после ..

Тип Методы Пример
SafeString to_s html_safe? size/length empty? raw('<b>').size
String upcase downcase capitalize strip size/length empty? to_s to_i html_safe html_safe? include?(s) start_with?(s) end_with?(s) title.strip.upcase
Integer to_s to_i to_f zero? positive? negative? abs post.comments.size.zero?
Float to_s to_i to_f zero? positive? negative? abs round round(n) floor ceil price.round(2)
Time year month day hour min sec strftime(fmt) to_s to_i post.created_at.strftime('%Y-%m-%d')
Array size/length/count empty? any? first last reverse include?(x) join(sep) to_a post.comments.any?
Hash key?(k) fetch(k, default) size/length/count empty? any? keys values, а также h[key] flash.key?(:notice)
Errors any? empty? count/size full_messages key?(a)/include?(a), а также errors[:attr] post.errors.full_messages.join(', ')
Params key?(k) empty?, а также params[key] params[:id]
Page (Relation#page) number per row_offset next? prev? next_number prev_number @page.next?
true/false to_s published.to_s
nil to_s (возвращает "")  

Индексирование (x[key]) — отдельное правило грамматики (Выражения), а не вызов .[]: Array/String принимают целочисленный индекс, Hash сопоставляет любой ключ по его to_s (h[:k] и h["k"] — одна и та же запись), Errors — по символу, Params — по to_s.

Вывод (<%= %>/интерполяция) помимо to_s выше: Array выводится как список в духе inspect — [1, "a", nil] выводит текст [1, "a", nil] (затем экранируется, поэтому кавычки становятся &quot;); Hash откатывается к собственному Hash#to_s Ruby; Cybertrain::Model выводится как #<Post id: 7> (никогда не его атрибуты); всё остальное выводится через собственный to_s. Неперечисленный метод у любого из этих типов — это RuntimeError ("undefined method 'first' for Time"). Целочисленное деление или остаток от деления на ноль для Integer — "divided by 0"; Float вместо этого следует IEEE-754 (Infinity/NaN, без ошибки).

Модели в шаблонах

Подкласс Cybertrain::Model (сгенерированный Post, Note, ...) разрешает голое имя в таком порядке (Interpreter#model_method):

  1. Пять встроенных, доступных всегда: id, persisted?, new_record?, to_param (id.to_s), errors (Errors, Таблица методов по типам).
  2. read_attribute(:name) — сгенерированный столбец.
  3. read_association(:name) — сгенерированная ассоциация has_many/belongs_to.
  4. call_view_method(:name) — написанный вручную def без аргументов, который генератор моделей нашёл в app/models/**/*.rb (никогда с аргументами).
erb
<%= post.title %>            <%# read_attribute %>
<% post.comments.each do |c| %>...<% end %>   <%# read_association %>
<%= post.summary %>          <%# call_view_method %>
<%= post.errors.full_messages.join(", ") %>

Если все четыре отвечают nil, attribute_or_method?(:name) решает, было ли это допустимым именем, которое просто содержит nil (возвращается nil), или опечаткой ("undefined method 'bogus' for Post").

As built — настоящий пробел, который отмечает сам файл интерпретатора (см. комментарий TODO(integrator) над class Model в interpreter.rb): базовый Cybertrain::Model#attribute_or_method? всегда возвращает true. Только сгенерированная модель, переопределяющая его явным case (как это делают написанные вручную тестовые модели), действительно возбуждает ошибку при опечатке; без такого переопределения атрибут или метод с опечаткой молча выводится пустым вместо ошибки. Не рассчитывайте, что опечатки отлавливаются, если не знаете, что модель переопределяет этот хук.

Две модели сравниваются через == по классу и id (Model#==), доступному как обычный оператор == (встроенная диспетчеризация равенства, а не call_method).

Хелперы и построитель форм

Каждый вызов без получателя идёт в Helpers#helper_call (helpers.rb), который сопоставляет его с одним из встроенных хелперов ниже, считает имя, оканчивающееся на _path/_url, сгенерированным хелпером маршрута (через Cybertrain::Views.url_resolver, возбуждая "no routes", пока приложение его не установит), или возбуждает "undefined helper 'name'".

Хелпер Сигнатура Примечания / пример
link_to link_to(text, href, class:, id:, data_confirm:, data: {}) text экранируется, если это ещё не SafeString; нет method: — возбуждает ошибку, предлагая использовать button_to (никакой имитации ссылок DELETE через rails-ujs/turbo)
button_to button_to(text, action, method: :post, class:, data: {}) <form class="button_to"> с одной кнопкой; скрытый _method для любого глагола, кроме get/post, плюс поле CSRF, когда есть сессия
form_with form_with(model:, url:, method:, class:, multipart:) do |f| ... end нужен блок; новая model: → маршрут коллекции + POST, сохранённая → маршрут элемента + PATCH; [parent, child] для вложенного маршрута; url:/method: переопределяют выведенные действие/глагол; multipart: true добавляет enctype="multipart/form-data" (обязательно для f.file_field; никогда не угадывается по блоку и возбуждает ArgumentError на GET-форме)
render render "name" / render "name", k: v / render partial: "name", locals: { k: v } см. Макеты, yield, content_for, партиалы
h, escape h(text) экранирует; SafeString проходит без экранирования
raw raw(text) оборачивает текст в SafeString (выводится без экранирования)
pluralize pluralize(count, singular, plural = nil) pluralize(1, "comment") → "1 comment"; pluralize(2, "mouse") → "2 mice" (английские правила словоизменения Rails по умолчанию, через Cybertrain::Inflector; правила, определённые приложением, не поддерживаются); явный третий аргумент всегда побеждает
truncate truncate(text, length: 30, omission: "...") truncate("Hello world", length: 8) → "Hello..."
number_with_delimiter number_with_delimiter(n) 1234567 → "1,234,567"; -1000 → "-1,000"; 1234.5 → "1,234.5"
time_ago_in_words time_ago_in_words(time) интервалы в стиле Rails, без точности до секунд: "less than a minute" … "about 1 hour" … "3 days" … "about 1 year"
content_for / content_for? content_for(:key) do...end, content_for(:key, "text"), content_for?(:key) см. Макеты, yield, content_for, партиалы
csrf_meta_tags / csrf_token   пусто / отсутствует без сессии контроллера
flash   похож на Hash: flash[:notice]
params   текущие Cybertrain::Params
request_path   текущий путь, "" без контроллера
url_for url_for(target) String как есть, Model → маршрут элемента, [parent, child] → вложенный маршрут
paginate paginate(page, path) page — это Cybertrain::Page (из Relation#page); выводит <nav class="pager"> с «Newer» (rel="prev"), <span class="pager-current">Page N</span> и «Older» (rel="next"); ссылка опускается, если такой страницы нет. Каждая ссылка — это path плюс строка запроса текущего запроса с заменённым page (для страницы 1 он убирается), заново закодированная и HTML-экранированная; path с собственным ?query использует вместо этого его. Page в шаблоне также отвечает на number, per, row_offset, next?, prev?, next_number, prev_number (<% if @page.next? %>)
*_path / *_url например, post_path(post) генерируются приложением, разрешаются через Cybertrain::Views.url_resolver

Каждый текстовый аргумент хелпера следует одному правилу: SafeString проходит без экранирования, всё остальное экранируется — link_to raw('<b>raw</b>'), post выводит тег жирного шрифта буквально, а link_to post.title, post его экранирует.

FormBuilder (f в form_with ... do |f|)

Каждый метод возвращает SafeString; неперечисленный — это RuntimeError ("undefined method 'color_wheel' for FormBuilder").

Метод Выводит
f.label :title / f.label :title, "Text" <label for="post_title">Title</label> — текст по умолчанию — «очеловеченный» атрибут (published_at → «Published at», author_id → «Author»); явный текст экранируется, если это не SafeString
f.text_field :title (также email_field, password_field, number_field, date_field, hidden_field) <input type="..." name="post[title]" id="post_title" value="...">; password_field никогда не выводит value=
f.file_field :cover (также accept: "image/*", required: true) <input type="file" name="post[cover]" id="post_cover"> — никогда не выводит value=, и атрибут не обязан быть столбцом; форма должна быть form_with ..., multipart: true; multiple: true возбуждает ArgumentError (один файл на поле)
f.text_area :body <textarea name="post[body]" id="post_body">\n...</textarea> (ведущий \n, как в Rails, — браузеры съедают первый перевод строки)
f.check_box :published скрытый input с value="0", затем настоящий флажок, поэтому неотмеченный флажок всё равно отправляет "0"
f.submit / f.submit "Text" текст по умолчанию "Create Post"/"Update Post" (по persisted?) или "Save changes" без модели

С моделью поля называются post[title]/id="post_title" (Inflector.underscore(model.model_name) как область видимости); с form_with(url: ...) без модели для обоих используется голое имя атрибута. Дополнительные именованные аргументы становятся HTML-атрибутами в заданном порядке: true → attr="attr", false/nil отбрасываются, всё остальное → attr="value" (с экранированием), подчёркивания становятся дефисами (data_confirm: "Sure?" → data-confirm="Sure?"), а class: объединяется с автоматическим классом field_with_errors, который добавляется, когда model.errors.key?(attr).

Макеты, yield, content_for, партиалы

Engine#render_with_layout(name, layout, env, helpers) отрисовывает name в env["__content"] (SafeString), затем отрисовывает layout с тем же env:

erb
<title><%= yield :title %></title>
<%= yield %>

yield выводит env["__content"]; yield :key выводит env["__content_key"] или ничего, если значение не задано. yield обрабатывается особо прямо в интерпретаторе и не может быть переопределён как хелпер. Страница заполняет слот через content_for в любом месте своего тела (порядок относительно yield не важен — страница полностью отрисовывается раньше макета):

erb
<% content_for :title do %>Post: <%= @post.title %><% end %>
<% content_for :title, " (draft)" %>   <%# appends: calls accumulate, they don't replace %>

Партиалы и render

erb
<%= render "form", post: @post %>
<%= render partial: "form", locals: { post: @post } %>
<%= render "comments/comment", comment: c %>
  • Голое имя без / разрешается в собственном каталоге текущего шаблона (через ключ окружения, который устанавливает механизм отрисовки, __template_dir): render "form" из posts/show ищет posts/_form.html.erb. Имя, содержащее /, — явный путь; префикс _ получает только его последний сегмент ("comments/comment" → comments/_comment.html.erb).
  • Партиал получает копию всего окружения вызывающего (переменные экземпляра и все локальные переменные, уже находящиеся в области видимости) плюс явные локальные переменные, переданные в render, — он не изолирован только тем, что было передано. Строгие локальные переменные проверяют по объявленному списку лишь имена, явно переданные в этот вызов render; они не мешают партиалу читать и какое-то другое имя, унаследованное из окружения вызывающего.
  • Из области видимости партиала в область вызывающего копируются обратно только ключи content_for (__content_*); обычные локальные переменные, которые он присваивает, наружу не утекают.
  • Отсутствующие или лишние локальные переменные относительно объявления <%# locals: %> — это RuntimeError: "missing local 'post' for posts/_form.html.erb", "unknown local 'extra' for posts/_form.html.erb".

Разрешение файлов (engine.rb)

"posts/show" и "posts/show.html.erb" называют один и тот же шаблон. Режим разработки читает app/views/ с диска с cache: false (файл перечитывается и заново разбирается, когда меняются его mtime или размер); тестовое окружение читает с диска с cache: true (разбор один раз). Продакшен-бинарник никогда не обращается к диску: cybertrain build запускает spin run gen -- --embed-views, который записывает каждый *.erb из app/views/ (скрытые файлы и каталоги-символические ссылки пропускаются) в gen/views.rb, а Engine.embedded(sources) разбирает каждую запись один раз (сообщения об ошибках сохраняют ту же форму posts/show.html.erb:12); бинарный файл с пустой таблицей отказывается запускаться в production. Отсутствующий шаблон возбуждает Cybertrain::Template::MissingTemplate — "Missing template <full path>" с диска, "Missing template posts/nope.html.erb (embedded)" из таблицы.

Поиск файлов разрешает корень представлений и путь к шаблону через символические ссылки и загружает только обычные файлы внутри этого корня. Пути, выходящие за его пределы (в том числе через символические ссылки), возбуждают MissingTemplate; exists? возвращает для таких путей false.

Ошибки

Два класса исключений, оба в пространстве имён Cybertrain::Template из ast.rb (отличны от ::SyntaxError/::RuntimeError):

  • SyntaxError — возбуждается при разбиении на токены и разборе, до какой-либо интерпретации. Форма: "<template>:<line>: <what>", например "posts/show:2: 'if' without 'end'", "posts/show:1: unterminated <% tag", "posts/show:1: unexpected ')' in 'foo(1))'". Возбуждается напрямую в Engine#template (а значит, в #render/#render_with_layout), когда файл страницы или макета не удаётся разобрать, — интерпретатор никогда его не перехватывает и не переоборачивает.
  • RuntimeError — возбуждается при вычислении, та же форма сообщения, но <template> — это шаблон, который отрисовывается в данный момент (Interpreter#render подменяет @name на входе и выходе), — собственная ошибка партиала сохраняет имя и строку партиала, никогда не вызывающего: "posts/_row:1: undefined method 'bogus' for String".

Любая обычная StandardError из вызова хелпера или метода FormBuilder/пользовательского объекта (голый raise "text", ArgumentError, KeyError, даже Cybertrain::Template::RuntimeError из вложенного render) перехватывается и «перемещается»: переписывается в "<current template>:<current line>: <original message>", если только это уже не в точности последнее перемещённое сообщение (вложенный render уже проставил своё, более точное место), — в этом случае оно проходит без изменений. Cybertrain::Template::SyntaxError, возбуждённая хелпером (партиал, который не удалось разобрать посреди отрисовки), никогда не переоборачивается — она сохраняет собственное сообщение "<partial>:<line>: ..." и класс, поэтому сломанный партиал всегда сообщается как SyntaxError о нём самом, а не как RuntimeError о том, кто его отрисовал.

MissingTemplate (StandardError, engine.rb) возбуждается без обёртки, без префикса template:line, из вызова верхнего уровня Engine#template/#render/#render_with_layout (например, когда контроллер отрисовывает отсутствующий шаблон действия или макет): "Missing template <path>". То же исключение, возбуждённое внутри шаблона при поиске партиала хелпером render, проходит через перемещение и выходит как RuntimeError: "posts/inline:1: Missing template test/fixtures/views/posts/_nope.html.erb".

Отличия от ERB в Rails

ERB в Rails Cybertrain
Компилируется в метод Ruby; работает любой Ruby, любой объект, любой метод Интерпретируется по фиксированным таблицам диспетчеризации для каждого типа; вызывать можно только то, что перечислено в таблице методов по типам
ActiveSupport::SafeBuffer — подкласс String SafeString — отдельный класс-обёртка: у него можно вызывать только to_s/html_safe?/size/length/empty?
Блоки: each, { }, map, любой метод Enumerable Только do...end и только на each/each_with_index/вызове хелпера
xs.each { } — ноль параметров блока допустим each/each_with_index требуют объявить 1 или 2 параметра блока
link_to text, path, method: :delete (через JS) У link_to нет method: — используйте button_to для настоящей не-GET формы
Строгие локальные переменные принимают name: default_expr (Rails 7.1) Только голое name: (обязательное); запись со значением по умолчанию — SyntaxError
Неопределённая переменная экземпляра/локальная разрешается через Ruby / методы контекста представления @x — простой, никогда не падающий поиск в окружении; голая неопределённая локальная — явная попытка вызова хелпера из замкнутого набора ("undefined helper", если совпадения нет)
and/or/not, case/when, диапазоны, регулярные выражения, множественное присваивание — всё работает Ничего этого нет; зарезервированные слова дают конкретное сообщение «not supported in templates», а не общую ошибку разбора
Числовые литералы: шестнадцатеричные, восьмеричные, двоичные, экспоненты, рациональные Только десятичные целые/дробные с необязательными разделителями _
:"quoted symbol", символы-операторы :+ Символы состоят только из символов идентификатора
foo (x), foo [x] (пробел допускается, иногда с предупреждением) Оба — жёсткие SyntaxError: никаких пробелов перед ( вызова или [ индекса
Вложенные вызовы без скобок разрешает собственный парсер Ruby Аргументы без скобок получает только самый внешний вызов в выражении
Необъявленный атрибут или атрибут с опечаткой надёжно возбуждает NoMethodError Молча выводится пустым, если конкретная сгенерированная модель не переопределяет attribute_or_method? (известный пробел, а не гарантия)
1 / 0 и 1.0 / 0 оба возбуждают ZeroDivisionError Целочисленное деление/остаток на ноль для Integer возбуждает "divided by 0"; Float следует IEEE-754 (Infinity/NaN), без ошибки
distance_of_time_in_words/плюрализация поддерживают локали i18n Только английский, локали не поддерживаются