Этот документ описывает ровно то, что этот конвейер принимает и выполняет. Там, где он расходится с разделом 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, объявляет единственные локальные переменные, которые принимает шаблон (почти всегда партиал):
<%# 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 %>.
Вызовы методов
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.
Операторы, приоритет (от низкого к высокому)
?: 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.
Условия
<% if cond %>...<% elsif other %>...<% else %>...<% end %>
<% unless cond %>...<% else %>...<% end %>
unless/else/elsif следуют правилам Ruby; elsif после else, лишние else/elsif/end или блок, так и не закрытый end, — всё это SyntaxError с указанием строки открытия.
Циклы
<% @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»):
<%= 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| %> отбрасывает. Привязывается всегда ровно один параметр блока; блок хелпера никогда не получает индекс или второе значение.
Присваивание
<% 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] (затем экранируется, поэтому кавычки становятся "); 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):
- Пять встроенных, доступных всегда:
id,persisted?,new_record?,to_param(id.to_s),errors(Errors, Таблица методов по типам). read_attribute(:name)— сгенерированный столбец.read_association(:name)— сгенерированная ассоциацияhas_many/belongs_to.call_view_method(:name)— написанный вручнуюdefбез аргументов, который генератор моделей нашёл вapp/models/**/*.rb(никогда с аргументами).
<%= 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:
<title><%= yield :title %></title>
<%= yield %>
yield выводит env["__content"]; yield :key выводит env["__content_key"] или ничего, если значение не задано. yield обрабатывается особо прямо в интерпретаторе и не может быть переопределён как хелпер. Страница заполняет слот через content_for в любом месте своего тела (порядок относительно yield не важен — страница полностью отрисовывается раньше макета):
<% content_for :title do %>Post: <%= @post.title %><% end %>
<% content_for :title, " (draft)" %> <%# appends: calls accumulate, they don't replace %>
Партиалы и render
<%= 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 |
Только английский, локали не поддерживаются |