liquid_xlsx
English summary below · Полная документация — на русском, ниже.
English
Generate .xlsx files from Excel templates written in Liquid syntax.
Design the template in Excel itself, put {{ variables }}, {% for %} and
{% if %} into cells, and the gem rewrites the OOXML in place — styles,
number formats, formulas, merged cells and workbook structure all survive.
gem "liquid_xlsx"
require "liquid_xlsx"
LiquidXlsx.render(
template: "invoice_template.xlsx",
output: "invoice.xlsx",
data: {
invoice: { number: "INV-001", paid: false },
customer: { name: "Acme Ltd" },
items: [
{ title: "Development", qty: 10, price: 100 },
{ title: "Support", qty: 5, price: 50 }
]
}
)
Object API: LiquidXlsx::Template.new(path).render_to_file(data, out), or
#render(data) for a binary string.
What it does
- Liquid variables and standard filters in cells:
{{ price | round: 2 }} - Numbers and booleans stay numeric in Excel, so formulas keep working
- Row-level
{% for %}and{% if %}blocks (the tag occupies its own row) - In-cell
{% if %},{% unless %},{% case %} - One Liquid context per sheet, so
{% assign %}/{% capture %}carries over - Formulas and merged ranges shift automatically as loops expand rows
{% sheet %}— generate whole worksheets from data{% image_tag %}— embed PNG / JPEG / GIF- Errors report the sheet, row and cell they came from
Requirements: Ruby >= 3.1. Works with both rubyzip 2.x and 3.x.
Main limitations: .xlsx only; structural tags must occupy a dedicated
row; loops are row-wise (no column loops); Liquid loop modifiers (reversed,
limit:, offset:, break, continue) and hash iteration are unsupported;
merges must not straddle a block boundary; conditional formatting, named
ranges, pivot tables and Excel tables are not shifted or rebuilt. The full
list is in Ограничения.
Everything else — syntax reference, dynamic sheets, images, render options,
error handling, a complete invoice example — is documented in Russian below.
The code samples there are language-neutral, and each one is backed by a spec
in spec/integration/readme_*_spec.rb.
Документация
Генерация файлов .xlsx из Excel-шаблонов с синтаксисом Liquid.
Откройте шаблон в Excel, напишите в ячейках {{ переменные }}, {% for %},
{% if %}, и библиотека сгенерирует итоговый файл, сохранив стили, формулы,
объединённые ячейки и структуру книги. Дополнительно поддерживаются
динамические листы ({% sheet %}) и
вставка изображений ({% image_tag %}).
Все примеры из этого README автоматически проверяются спеками в
spec/integration/readme_core_spec.rb,spec/integration/readme_structural_spec.rb,spec/integration/readme_advanced_spec.rb.
Возможности
- Переменные Liquid в ячейках:
{{ customer.name }} - Стандартные фильтры Liquid:
{{ price | round: 2 }} - Сохранение числовых и булевых типов (число остаётся числом, а не строкой)
- Структурные теги
{% for %}и{% if %}на уровне целых строк {% assign %}/{% capture %}с общим контекстом в пределах листа- Внутриячеечные теги Liquid:
{% if %},{% unless %},{% case %} - Автоматическое смещение формул и объединённых ячеек в циклах
- Динамические листы
{% sheet %}— генерация листов по данным - Изображения
{% image_tag %}— PNG, JPEG, GIF - Подробные ошибки с указанием листа, строки и ячейки
Установка
Добавьте в Gemfile:
gem "liquid_xlsx"
Или установите глобально:
gem install liquid_xlsx
Требуется Ruby >= 3.1. Работает как с rubyzip 2.x, так и с 3.x.
Быстрый старт
require "liquid_xlsx"
LiquidXlsx.render(
template: "invoice_template.xlsx",
output: "invoice.xlsx",
data: {
invoice: { number: "INV-001", paid: false },
customer: { name: "ООО Ромашка" },
items: [
{ title: "Разработка", qty: 10, price: 100 },
{ title: "Поддержка", qty: 5, price: 50 }
]
}
)
Объектный API
template = LiquidXlsx::Template.new("template.xlsx")
template.render_to_file(data, "output.xlsx") # рендер + запись в файл
binary = template.render(data) # рендер, возвращает бинарную строку
Синтаксис шаблонов
Библиотека разделяет два слоя Liquid:
- Внутриячеечный Liquid — всё, что написано внутри одной ячейки
(
{{ var }},{{ var | filter }},{% if %}внутри ячейки и т.д.). Обрабатывается настоящим движком Liquid. - Структурные теги —
{% for %},{% endfor %},{% if %},{% elsif %},{% else %},{% endif %}на уровне целых строк. Такие теги должны занимать отдельную строку и обрабатываются собственным парсером.
Переменные
Любая текстовая ячейка может содержать Liquid-переменные:
Счёт № {{ invoice.number }}
Клиент: {{ customer.name }}
Город: {{ customer.address.city }}
Поддерживается точечная нотация для вложенных хешей и массивов:
{{ customer.address.city }}, {{ items.first.title }}.
Несколько выражений и литеральный текст в одной ячейке:
Привет, {{ name }}! Ваш баланс: {{ balance | round: 2 }}
Источники данных: Hash, Liquid::Drop, любой #to_liquid
Параметр data: принимает не только Hash, но и Liquid::Drop, и любой
объект, отвечающий на #to_liquid (возвращающий Hash или Drop). Это удобно
для ленивого доступа к данным, вычислимых полей и обёрток над AR-моделями:
class InvoiceDrop < Liquid::Drop
def initialize(invoice)
@invoice = invoice
end
def number
@invoice.number # строка
end
def total
@invoice.lines.sum(&:amount) # вычислимое числовое поле
end
def customer
CustomerDrop.new(@invoice.customer) # вложенный Drop
end
end
LiquidXlsx.render(
template: "invoice.xlsx",
output: "out.xlsx",
data: InvoiceDrop.new(invoice) # <- Drop вместо Hash
)
Для объекта, не являющегося Hash или Liquid::Drop, но реализующего
#to_liquid (например, AR-модель, Struct, OpenStruct), метод вызывается
один раз на входе, и результат используется как корневое окружение Liquid:
class Report < Struct.new(:title, :count)
def to_liquid
{ "title" => title, "count" => count }
end
end
LiquidXlsx.render(template: ..., output: ..., data: Report.new("Sales", 42))
Сохранение типов работает и для Drop. Если метод Drop возвращает Integer
или Float, ячейка {{ invoice.total }} запишется как число <v> (не как
строка), поэтому формулы =SUM(...) и числовые форматы продолжат работать.
BigDecimal также сохраняется как число; Rational, Complex, а также
Float::NAN/Infinity (не валидны в OOXML) автоматически рендерятся как
текст.
Ограничения:
- Корневой объект должен реализовывать
[]/key?(какHashиLiquid::Drop) либо#to_liquid, возвращающий Hash/Drop. Произвольный объект только с методами-атрибутами, но безto_liquid, работать не будет — Liquid ищет переменные через[]. - Drop-методы, используемые в
{% for %}, должны возвращать именноArray.Enumerable/ActiveRecord::Relationнапрямую не поддерживаются — материализуйте их через.to_aв самом методе. - Если Drop-метод возвращает вложенный
Hash, используйте строковые ключи:stringify_keysрекурсивно нормализует корневойHash(и результат#to_liquid→Hash), но не применяется к значениям, возвращаемым Drop-методами во время рендера. - Один и тот же Drop-метод для одной ячейки
{{ var }}может вызываться дважды (для fast-path и для основного рендера). Если у метода есть побочные эффекты или SQL-запросы, мемоизируйте результат.
Сохранение типов
Если вся ячейка целиком состоит из одной переменной {{ var }} (без фильтров,
без окружающего текста), значение подставляется напрямую из данных.
Числа остаются числами, булевы значения — булевыми:
| Шаблон | Данные | Результат в Excel |
|---|---|---|
{{ qty }} |
42 |
Числовая ячейка <v>42</v> |
{{ price }} |
19.99 |
Числовая ячейка <v>19.99</v> |
{{ active }} |
true |
Булевая ячейка t="b" <v>1</v> |
{{ name }} |
"Иван" |
Строка inlineStr |
Как только добавляется фильтр, окружающий текст или ещё одна переменная —
ячейка всегда становится строковой (inlineStr):
{{ price | round: 2 }} → строка "19.99"
Цена: {{ price }} → строка "Цена: 19.99"
Фильтры Liquid
Поддерживаются все стандартные фильтры Liquid:
{{ name | upcase }} # IVAN PETROV
{{ name | downcase }} # ivan petrov
{{ name | capitalize }} # Ivan petrov
{{ price | round: 2 }} # 19.99
{{ total | plus: 100 }} # 1100
{{ total | minus: 50 }} # 950
{{ price | times: 1.2 }} # 120.0
{{ items | size }} # 3
{{ items | join: ", " }} # яблоко, груша, слива
{{ items | first }} # яблоко
{{ items | last }} # слива
{{ name | append: "!" }} # hello!
{{ name | prepend: "Mr. " }} # Mr. hello
{{ name | replace: "old", "new" }} # hello new
{{ desc | truncate: 10 }} # this is...
{{ created_at | date: "%d.%m.%Y" }} # 15.03.2024
{{ phone | default: "—" }} # +7... или "—" если nil
{{ text | strip }} # обрезка пробелов
Примечание: арифметические фильтры над Float сохраняют дробный формат (
100.0 | plus: 10→"110.0"). Если нужно целое — добавьтеround.
assign и capture — общий контекст на листе
В пределах одного листа создаётся один Liquid::Context, общий для всех
ячеек в порядке документа (сверху вниз, слева направо). Поэтому переменная,
созданная в одной ячейке, видна в последующих:
A1: {% assign total = 100 %}
B1: {{ total }} → 100
A2: {% assign total = total | plus: 50 %}
B2: {{ total }} → 150
{% capture %} собирает блок текста в переменную:
A1: {% capture greeting %}Привет, {{ name }}!{% endcapture %}
B1: {{ greeting }} → Привет, Мир!
{% assign %} работает и внутри циклов — накопленные значения доступны
после {% endfor %} (см. пример ниже).
Внутриячеечные теги Liquid
Внутри одной ячейки можно использовать условные теги Liquid (это НЕ структурные теги уровня строк, обрабатываются самим Liquid):
{% if paid %}Оплачено{% else %}Не оплачено{% endif %}
{% if total > 1000 %}Крупный{% elsif total > 100 %}Средний{% else %}Малый{% endif %}
{% unless cancelled %}Активен{% endunless %}
{% case status %}{% when "active" %}Активен{% when "pending" %}Ожидает{% endcase %}
Поддерживаемые операторы в условиях: ==, !=, >, <, >=, <=,
contains; логические and, or; ключевые слова true, false, nil,
empty, blank.
Ограничение: внутриячеечный
{% case %}с веткой{% else %}в текущей версии не работает — структурный парсер ошибочно воспринимает{% else %}как тег уровня строки. Используйте{% case %}/{% when %}без{% else %}, либо эквивалентный{% if %}/{% elsif %}/{% else %}.
По правилам Liquid пустой массив правдив.
{% unless items %}сработает только приitems = nil/false, но не наitems = []. Для проверки пустоты используйте{% if items == empty %}.
Структурные теги
Правило отдельной строки
{% for %}, {% endfor %}, {% if %}, {% elsif %}, {% else %},
{% endif %} должны занимать отдельную строку: тег один в первой
непустой ячейке строки, остальные ячейки строки пусты.
| Допустимо | Недопустимо |
|---|---|
A1: {% for x in xs %} |
A1: Сумма: {% for x in xs %} |
A1: {% for x in xs %} B1: текст |
Нарушение правила выбрасывает LiquidXlsx::TemplateSyntaxError с указанием
листа, строки и ячейки.
Циклы {% for %}
{% for item in items %}
{{ forloop.index }}
{{ item.title }}
{{ item.qty }}
{{ item.price }}
{% endfor %}
Строки между {% for %} и {% endfor %} повторяются для каждого элемента.
Внутри тела доступны переменные цикла:
| Переменная | Значение |
|---|---|
{{ forloop.index }} |
1-based индекс (1, 2, 3, ...) |
{{ forloop.index0 }} |
0-based индекс (0, 1, 2, ...) |
{{ forloop.first }} |
true на первой итерации (булево) |
{{ forloop.last }} |
true на последней итерации |
{{ forloop.length }} |
Общее количество итераций |
Стили ячеек, формулы и объединённые ячейки в теле цикла сохраняются и корректно смещаются.
Пустые циклы с {% else %}
{% for item in items %}
{{ item.title }}
{% else %}
Список пуст
{% endfor %}
Если коллекция пуста или равна nil, отрисовывается блок {% else %}.
Цикл над nil (ключ отсутствует в данных) тоже безопасен — эквивалентен
пустому массиву.
Вложенные циклы
{% for order in orders %}
Заказ {{ order.number }}
{% for line in order.lines %}
{{ line.title }}: {{ line.qty }}
{% endfor %}
{% endfor %}
Внутренний цикл разрешает коллекцию через Liquid-контекст, в котором уже
установлена переменная внешнего цикла (order).
Условия на уровне строк {% if %}
Вся строка включается или исключается целиком:
{% if invoice.paid %}
Оплачено
{% elsif invoice.pending %}
В обработке
{% else %}
Не оплачено
{% endif %}
Условия поддерживают сравнения, and/or, contains, проверку пустоты:
{% if age >= 18 and age <= 65 %} возрастной диапазон
{% if tags contains "vip" %} наличие элемента
{% if notes == empty %} пустая коллекция
Вложенные конструкции
{% if %} внутри {% for %} и наоборот — поддерживаются в любой комбинации:
{% for item in items %}
{% if item.urgent %}
СРОЧНО: {{ item.name }}
{% endif %}
{% endfor %}
{% if show_items %}
{% for item in items %}
{{ item.name }}
{% endfor %}
{% endif %}
Накопление суммы в цикле
{% assign %} внутри тела цикла сохраняется после {% endfor %}:
{% assign sum = 0 %}
{% for item in items %}
{% assign sum = sum | plus: item.qty %}
{% endfor %}
Итого: {{ sum }} → Итого: 18 (5 + 10 + 3)
Формулы
Формулы в теле цикла автоматически смещаются по строкам:
| Шаблон | После 3 итераций |
|---|---|
=A2*2 |
=A2*2, =A3*2, =A4*2 |
Формулы-суммы под циклом, ссылающиеся на строку-шаблон, раскрываются в диапазон:
| Шаблон | После 3 итераций |
|---|---|
=SUM(A2:A2) |
=SUM(A2:A4) |
Опция recalculate_formulas: true удаляет xl/calcChain.xml и сбрасывает
кэшированные значения, заставляя Excel пересчитать всё при открытии.
Объединённые ячейки
Объединённые диапазоны в теле цикла клонируются для каждой итерии:
| Шаблон | После 3 итераций |
|---|---|
A2:B2 |
A2:B2, A3:B3, A4:B4 |
Диапазоны ниже блока автоматически смещаются. Объединённая ячейка, переходящая
через границу {% for %}/{% if %}-блока, вызывает
LiquidXlsx::UnsupportedTemplateError.
Динамические листы {% sheet %}
Тег {% sheet %} создаёт копии указанного листа — по одной на элемент
коллекции. Например, по списку счетов можно сгенерировать отдельный лист
под каждый счёт.
Требуется опция dynamic_sheets: true.
Синтаксис
{% sheet name: <имя> template: "<шаблон>" data: <данные> as: "<переменная>" %}
| Аргумент | Обязательный | Описание |
|---|---|---|
name: |
да | Имя нового листа (Liquid-выражение или строка) |
template: |
да | Имя листа-шаблона (только строковый литерал в ") |
data: |
да | Данные для этого листа (Liquid-выражение) |
as: |
нет | Имя переменной для data (по умолчанию "item") |
Важно: значения
template:и строковые литералы вname:/as:должны быть в двойных кавычках. Одинарные кавычки не поддерживаются.
Типичный сценарий
Контрольный лист Index обходит коллекцию и создаёт листы:
{% for inv in invoices %}
{% sheet name: inv.number template: "Invoice" data: inv as: "inv" %}
{% endfor %}
Лист-шаблон Invoice ссылается на локальную переменную:
A1: Счёт № {{ inv.number }}
A2: Клиент: {{ inv.client }}
Вызов:
LiquidXlsx.render(
template: "tpl.xlsx",
output: "out.xlsx",
dynamic_sheets: true,
data: {
invoices: [
{ number: "INV-001", client: "Альфа" },
{ number: "INV-002", client: "Бета" }
]
}
)
Результат: в книге появятся листы INV-001 и INV-002, заполненные из
соответствующих счетов. Контрольный лист Index будет скрыт.
Liquid-фильтры в name:
{% sheet name: inv.number | append: " - " | append: inv.client template: "Invoice" data: inv as: "inv" %}
→ имя листа INV-001 - Alpha.
Значение по умолчанию as: "item"
Если as: опущено, данные доступны как item:
{% sheet name: inv.number template: "Invoice" data: inv %}
В шаблоне: {{ item.number }}.
Нормализация имён листов
Excel запрещает в именах листов символы : \ / ? * [ ] и ограничивает длину
31 символом. Библиотека автоматически:
- удаляет запрещённые символы (
"Отчёт/За: 2024"→"ОтчётЗа 2024"); - обрезает имя до 31 символа;
- при совпадении имён добавляет суффиксы:
DUP,DUP (2),DUP (3); - пустое имя заменяет на
Sheet.
Поведение по умолчанию
hide_control_sheets: true— контрольный лист (с тегом{% sheet %}) скрывается.hide_template_sheets: false— лист-шаблон остаётся видимым. Установитеtrue, чтобы скрыть его.- Вызов
{% sheet %}безdynamic_sheets: trueвыбрасываетLiquidXlsx::RenderError. - Отсутствующий
template:выбрасываетLiquidXlsx::UnsupportedTemplateError.
Изображения {% image_tag %}
Вставка изображений (PNG, JPEG, GIF) прямо из данных. Формат распознаётся по сигнатуре файла (magic bytes).
Синтаксис
{% image_tag <источник>, <опции> %}
Источник — первый позиционный аргумент: Liquid-выражение, возвращающее бинарную строку с данными картинки.
| Опция | Тип | Описание |
|---|---|---|
colspan: |
целое > 0 | Сколько колонок вправо занимает картинка |
rowspan: |
целое > 0 | Сколько строк вниз занимает картинка |
to: |
"F10" |
Правый нижний угол (инклюзивно) |
width: |
целое > 0 px | Фиксированная ширина в пикселях |
height: |
целое > 0 px | Фиксированная высота в пикселях |
Выбор якоря
Якорь определяется в порядке приоритета:
to:— абсолютный правый нижний угол;colspan:/rowspan:— относительный охват от ячейки-якоря;- ячейка-якорь — левый верх объединённого диапазона → заполнение merge;
width:/height:— фиксированный размер (oneCellAnchor);- по умолчанию — одна ячейка (
colspan=1, rowspan=1).
При width: без height: (или наоборот) вторая размерность выводится из
натуральных пропорций картинки; для вырожденных размеров (ноль в заголовке)
используется квадрат.
Примеры
{% image_tag logo, colspan: 2, rowspan: 3 %} # заливает 2 колонки и 3 строки
{% image_tag photo, width: 120, height: 60 %} # фиксированный размер
{% image_tag cover, width: 100 %} # высота из пропорций
{% image_tag logo, to: "F10" %} # правый нижний угол — F10
Вызов:
LiquidXlsx.render(
template: "tpl.xlsx",
output: "out.xlsx",
data: { "logo" => File.binread("logo.png") }
)
Загрузка по пути/URL
Если источник — строка-путь (например, "logos/x.png"), подключите
images.loader:
LiquidXlsx.render(
template: "tpl.xlsx",
output: "out.xlsx",
images: { loader: ->(ref) { File.binread(ref) } },
data: { "logo_path" => "logos/company.png" }
)
Без loader строка-путь выбрасывает LiquidXlsx::RenderError.
В циклах
Якорь автоматически смещается на каждой итерации:
{% for item in items %}
{% image_tag item.photo, colspan: 1, rowspan: 1 %}
{% endfor %}
Идентичные картинки (одинаковый SHA-256) дедуплицируются — в архиве хранится одна медиа-запись.
Валидация
colspan:,rowspan:,width:,height:должны быть положительными целыми числами. Иначе —LiquidXlsx::RenderError.- Неподдерживаемый формат —
LiquidXlsx::UnsupportedTemplateError.
Опции рендеринга
LiquidXlsx.render(
template:,
output:,
data:,
strict_variables: false, # true — raise на отсутствующей переменной
strict_filters: false, # true — raise на неизвестном фильтре
recalculate_formulas: false, # true — удалить calcChain, форсировать пересчёт
remove_template_comments: false,# зарезервировано
dynamic_sheets: false, # true — включить {% sheet %}
hide_control_sheets: true, # скрывать контрольные листы
hide_template_sheets: false, # скрывать листы-шаблоны после клонирования
images: nil, # { loader:, default_dpi: } для {% image_tag %}
liquid_resource_limits: nil, # { render_length_limit:, render_score_limit:, ... }
filters: nil # модуль (или массив модулей) со своими Liquid-фильтрами
)
| Опция | По умолчанию | Описание |
|---|---|---|
strict_variables |
false |
При true отсутствующая переменная → MissingVariableError |
strict_filters |
false |
При true неизвестный фильтр → исключение Liquid |
recalculate_formulas |
false |
Удалить calcChain.xml, форсировать пересчёт в Excel |
dynamic_sheets |
false |
Включить обработку {% sheet %} |
hide_control_sheets |
true |
Скрывать листы с тегами {% sheet %} |
hide_template_sheets |
false |
Скрывать листы-шаблоны |
images |
nil |
{ loader:, default_dpi: } — загрузчик путей и DPI для width/height |
liquid_resource_limits |
nil |
Лимиты Liquid (render_length_limit, render_score_limit, assign_score_limit) |
filters |
nil |
Модуль или массив модулей со своими фильтрами — см. Свои фильтры |
Свои фильтры
Фильтры приложения подключаются опцией filters: и доступны в любой ячейке книги:
module MyFilters
def money2(value) = format("%.2f", value.to_f)
end
LiquidXlsx.render(template: "invoice.xlsx", output: "out.xlsx",
data: data, filters: MyFilters) # или [MyFilters, DateFilters]
{{ invoice.sum | money2 }}
Модули добавляются только в контекст рендера (Liquid::Context#add_filters),
на Liquid::Environment ничего не регистрируется — глобальная конфигурация
Liquid в приложении остаётся нетронутой.
Обработка ошибок
Все ошибки наследуются от LiquidXlsx::Error < StandardError. Там, где
применимо, объект ошибки содержит атрибуты sheet, row, cell, template.
LiquidXlsx::Error
├── TemplateSyntaxError нарушение правила отдельной строки,
│ непарные структурные теги
├── RenderError ошибка рендера (включая {% sheet %}/{% image_tag %}),
│ └── MissingVariableError при strict_variables: true
├── UnsupportedTemplateError merge через границу блока, неизвестный формат
│ картинки, отсутствие template-листа
├── InvalidXlsxError битый .xlsx на входе
└── OutputWriteError ошибка записи выходного файла
Пример сообщения:
LiquidXlsx::TemplateSyntaxError:
Structural tag must be placed on a dedicated row.
Sheet: Invoice
Row: 12
Cell: A12
Tag: {% for item in items %}
Полный пример: счёт-фактура
Шаблон invoice.xlsx:
| A | B | C | D | E |
|---|---|---|---|---|
| Счёт № invoice.number } | ||||
| Клиент | customer.name } |
| | | | | |
| № | Услуга | Кол-во | Цена | Сумма |
| {% for item in items %} | | | | |
| forloop.index } | item.title } | item.qty } | item.price } | =C6*D6 |
| {% endfor %} | | | | |
| | | | Итого | =SUM(E6:E6) |
| {% if invoice.paid %} | | | | |
| Оплачено | | | | |
| {% else %} | | | | |
| Не оплачено | | | | |
| {% endif %} | | | | |
Рендер:
LiquidXlsx.render(
template: "invoice.xlsx",
output: "invoice_001.xlsx",
data: {
invoice: { number: "INV-001", paid: false },
customer: { name: "ООО Ромашка" },
items: [
{ title: "Разработка", qty: 10, price: 100 },
{ title: "Поддержка", qty: 5, price: 50 }
]
}
)
Результат:
| A | B | C | D | E |
|---|---|---|---|---|
| Счёт № INV-001 | ||||
| Клиент | ООО Ромашка |
| | | | | |
| № | Услуга | Кол-во | Цена | Сумма |
| 1 | Разработка | 10 | 100 | =C6*D6 |
| 2 | Поддержка | 5 | 50 | =C7*D7 |
| | | | Итого | =SUM(E6:E7) |
| Не оплачено | | | | |
Ограничения
- Только формат
.xlsx(не.xls,.xlsm,.xlsb). - Структурные теги
{% for %}/{% if %}обязаны занимать отдельную строку. - Колонковые циклы не поддерживаются (только по строкам).
- Не поддерживаются Liquid-модификаторы цикла:
reversed,limit:,offset:,break,continue. Итерация по хешу (for k, v in hash) тоже не работает. - Таблица общих строк (
sharedStrings.xml) читается, но не перезаписывается — весь новый текст пишется какinlineStr. - Объединённая ячейка через границу
{% for %}/{% if %}-блока вызываетUnsupportedTemplateError. - Формулы переводятся по строкам (сдвиг колонок не поддерживается).
- Условное форматирование и именованные диапазоны не смещаются при вставке строк.
- Сводные таблицы (Pivot) и таблицы Excel (ListObject) не перестраиваются.
- Rich text (
<r>внутри<is>) в шаблонных ячейках может теряться. - Внутриячеечный
{% case %}с веткой{% else %}не работает (см. выше). - Клонирование листа-шаблона с собственными рисунками/rels не поддерживается.
Rational,Complex,Float::NAN/Infinityне могут быть записаны как числовое значение<v>(OOXML не допускает таких лексем) — они рендерятся как текст.Integer,FloatиBigDecimalсохраняются как числа.
Разработка
bundle install # установка зависимостей
bundle exec rspec # полный набор тестов (420 примеров)
bundle exec rspec spec/integration/ # только интеграционные
bundle exec rubocop # линтер
bundle exec rubocop -a # автоисправление
bundle exec rake # rspec + rubocop
Основной Gemfile.lock резолвит rubyzip 3.x. Вторая ветка совместимости
(rubyzip 2.x) проверяется отдельным бандлом:
BUNDLE_GEMFILE=gemfiles/rubyzip2.gemfile bundle install
BUNDLE_GEMFILE=gemfiles/rubyzip2.gemfile bundle exec rspec
Примеры из этого README покрыты тестами:
bundle exec rspec spec/integration/readme_core_spec.rb
bundle exec rspec spec/integration/readme_structural_spec.rb
bundle exec rspec spec/integration/readme_advanced_spec.rb
Архитектура
Конвейер рендеринга: LiquidXlsx.render → Template → Package (ZIP I/O) →
Workbook (оркестрация листов) → для каждого листа Worksheet (XML в/из) +
Renderer (управляет AST из TemplateParser) → Package#write.
Ключевые файлы:
lib/liquid_xlsx.rb— точка входа, публичный API, регистрация теговlib/liquid_xlsx/template.rb— класс шаблонаlib/liquid_xlsx/package.rb— чтение/запись ZIP-архиваlib/liquid_xlsx/workbook.rb— оркестрация нескольких листовlib/liquid_xlsx/worksheet.rb— манипуляция XML одного листаlib/liquid_xlsx/renderer.rb— применение AST к строкам листаlib/liquid_xlsx/template_parser.rb— построение AST структурных теговlib/liquid_xlsx/template_nodes.rb— типы узлов ASTlib/liquid_xlsx/formula_translator.rb— смещение формулlib/liquid_xlsx/merge_cells_transformer.rb— смещение merge-диапазоновlib/liquid_xlsx/cell_reference.rb— парсер A1-ссылокlib/liquid_xlsx/shared_strings.rb— чтение общих строкlib/liquid_xlsx/tags/sheet_tag.rb— тег{% sheet %}lib/liquid_xlsx/tags/image_tag.rb— тег{% image_tag %}lib/liquid_xlsx/image.rb— определение размеров PNG/JPEG/GIFlib/liquid_xlsx/drawing_builder.rb— построение XML рисунковlib/liquid_xlsx/errors.rb— иерархия ошибокlib/liquid_xlsx/filters.rb— пользовательские Liquid-фильтры
Лицензия
MIT