liquid_xlsx 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +146 -4
- data/lib/liquid_xlsx/errors.rb +2 -0
- data/lib/liquid_xlsx/file_part.rb +32 -0
- data/lib/liquid_xlsx/package.rb +18 -1
- data/lib/liquid_xlsx/renderer.rb +3 -11
- data/lib/liquid_xlsx/streaming_context.rb +34 -0
- data/lib/liquid_xlsx/streaming_renderer.rb +300 -0
- data/lib/liquid_xlsx/tags/sheet_tag.rb +4 -0
- data/lib/liquid_xlsx/template.rb +13 -1
- data/lib/liquid_xlsx/validation/block_boundary.rb +30 -0
- data/lib/liquid_xlsx/validation/issue.rb +36 -0
- data/lib/liquid_xlsx/validation/liquid_usage.rb +149 -0
- data/lib/liquid_xlsx/validation/result.rb +80 -0
- data/lib/liquid_xlsx/validation/usage.rb +61 -0
- data/lib/liquid_xlsx/validator.rb +386 -0
- data/lib/liquid_xlsx/version.rb +1 -1
- data/lib/liquid_xlsx/workbook.rb +27 -0
- data/lib/liquid_xlsx/worksheet.rb +24 -0
- data/lib/liquid_xlsx.rb +46 -2
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 619408f5440742c3210d5a61c30e44b16165d94ab673cefbb132b310310e0076
|
|
4
|
+
data.tar.gz: 208342e1abeed7c51352a204522030f16a36d8d7a3a08e1f6e07ca4a32f08a2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9edb7d9a0473e8220bf4a8f78f492369db9fea4a1b9c07fc4ce92c71f3d999848ce7e64ae53650c84f34541ef091e133f49aad47e96189c7163043c5f902ee13
|
|
7
|
+
data.tar.gz: f9054e1f438f9253f8a03e5e1dd8c806d9ab9e4465ccfbaa95df14d4bc0d8b42935213ad7adf820bd106fc992a5a4ec70d13974e93979128c54cda85bccffd97
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (2026-10-25)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Explicit `streaming: true` rendering to files, including `Template#render_to_file`.
|
|
8
|
+
Rows are serialized directly to a Tempfile without output DOM nodes or retained
|
|
9
|
+
row/cell hashes. Worksheet ZIP entries stream from disk with exact byte sizes
|
|
10
|
+
on rubyzip 2 and 3; temporary row files are released on success and failure.
|
|
11
|
+
- Workbook-wide preflight with `StreamingUnsupportedError` coordinates for
|
|
12
|
+
formulas, images, sheet tags and merged ranges at or below the first loop.
|
|
13
|
+
Header merges, nested/consecutive loops, row conditions, shared Liquid context,
|
|
14
|
+
styles, dimensions, column widths and frozen panes are preserved.
|
|
15
|
+
- Single-pass streaming loops over any object implementing `each`, including
|
|
16
|
+
sized/unsized and lazy Enumerators. Length-dependent `forloop` fields are only
|
|
17
|
+
available when `size` returns an Integer; otherwise they are empty or raise
|
|
18
|
+
`MissingVariableError` in strict mode.
|
|
19
|
+
- Compiled simple-variable expressions in streaming cells. Strings avoid
|
|
20
|
+
repeated Liquid rendering; nil/missing values retain Liquid error semantics.
|
|
21
|
+
String-keyed row hashes and collection arrays are not copied.
|
|
22
|
+
- `test_data/bench_stream.rb`: 19-column file-render benchmark for Array/Enumerator
|
|
23
|
+
input with wall time and peak RSS measurements, with or without YJIT.
|
|
24
|
+
|
|
25
|
+
The default renderer is unchanged. Streaming binary-string output is explicitly
|
|
26
|
+
rejected with `ArgumentError`.
|
|
27
|
+
|
|
28
|
+
## 0.3.0 (2026-08-14)
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `LiquidXlsx.validate(template:)` — check a template without rendering it and
|
|
33
|
+
without any data. Returns a `Validation::Result` listing every problem found
|
|
34
|
+
instead of raising on the first one.
|
|
35
|
+
|
|
36
|
+
This is deliberately not "render it and see what happens". The renderer parses
|
|
37
|
+
only the Liquid it actually executes, so a mistake inside an `{% if %}` branch
|
|
38
|
+
the current document does not take, or inside a loop over an empty collection,
|
|
39
|
+
stays invisible until some other document hits it. Rendering also stops at the
|
|
40
|
+
first failure and runs the template against live records — a heavy price for
|
|
41
|
+
the question "is the file I just uploaded any good?".
|
|
42
|
+
|
|
43
|
+
Issue codes: `:invalid_xlsx`, `:missing_worksheet`, `:template_syntax`,
|
|
44
|
+
`:liquid_syntax`, `:liquid_expression` (warning), `:unknown_filter`,
|
|
45
|
+
`:sheet_tag_disabled`, `:sheet_template_missing`,
|
|
46
|
+
`:merged_cell_crosses_block`, `:liquid_in_formula` (warning),
|
|
47
|
+
`:liquid_in_rich_text` (warning). Each carries the sheet, row and cell it came
|
|
48
|
+
from, plus a stable `code` a host application can map to its own wording.
|
|
49
|
+
|
|
50
|
+
Beyond issues, the result reports what the template *uses*: `roots` (the input
|
|
51
|
+
variables it expects, with loop variables and `{% assign %}` names excluded),
|
|
52
|
+
full `variables` paths, and `filters` — so a host application can check them
|
|
53
|
+
against its own data model without parsing Liquid itself.
|
|
54
|
+
|
|
55
|
+
- Two silent losses are now reported, both of which used to produce a document
|
|
56
|
+
with tags left in it and no error anywhere:
|
|
57
|
+
- Liquid inside a formula is never evaluated (`:liquid_in_formula`).
|
|
58
|
+
- Liquid inside a rich-text inline string — text split into runs because part
|
|
59
|
+
of the cell is formatted differently — is not read (`:liquid_in_rich_text`).
|
|
60
|
+
|
|
61
|
+
- Expressions Liquid silently misreads are reported as warnings
|
|
62
|
+
(`:liquid_expression`): `{{ invoice.total b c }}` and `{{ invoice..total }}`
|
|
63
|
+
print the total and quietly drop the rest. Liquid's default lax parser accepts
|
|
64
|
+
them, so nothing fails — which is exactly why they are worth pointing out.
|
|
65
|
+
Only fragments the renderer would actually reject are errors.
|
|
66
|
+
|
|
67
|
+
- `Tags::SheetTag#template_name` / `#as_name` are now public, so the sheet a
|
|
68
|
+
`{% sheet %}` clones can be checked without rendering the tag.
|
|
69
|
+
|
|
3
70
|
## 0.2.2 (2026-08-14)
|
|
4
71
|
|
|
5
72
|
### Fixed
|
data/README.md
CHANGED
|
@@ -44,7 +44,10 @@ Object API: `LiquidXlsx::Template.new(path).render_to_file(data, out)`, or
|
|
|
44
44
|
- Formulas and merged ranges shift automatically as loops expand rows
|
|
45
45
|
- `{% sheet %}` — generate whole worksheets from data
|
|
46
46
|
- `{% image_tag %}` — embed PNG / JPEG / GIF
|
|
47
|
+
- Explicit `streaming: true` file rendering for large, restricted templates
|
|
47
48
|
- Errors report the sheet, row and cell they came from
|
|
49
|
+
- `LiquidXlsx.validate` — check a template without data: every problem at once,
|
|
50
|
+
including the branches a trial render would never reach
|
|
48
51
|
|
|
49
52
|
**Requirements:** Ruby >= 3.1. Works with both rubyzip 2.x and 3.x.
|
|
50
53
|
|
|
@@ -131,6 +134,75 @@ template.render_to_file(data, "output.xlsx") # рендер + запись в ф
|
|
|
131
134
|
binary = template.render(data) # рендер, возвращает бинарную строку
|
|
132
135
|
```
|
|
133
136
|
|
|
137
|
+
## Потоковый режим
|
|
138
|
+
|
|
139
|
+
Для выгрузок на сотни тысяч строк включите `streaming: true`. Каждая готовая
|
|
140
|
+
строка сразу записывается во временный файл; строки результата и DOM листа
|
|
141
|
+
в памяти не накапливаются. Затем лист записывается в ZIP с точным байтовым
|
|
142
|
+
размером, без ZIP64. Временные файлы удаляются и при успехе, и при исключении.
|
|
143
|
+
Без этой опции используется прежний рендер со всеми возможностями.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
rows = Enumerator.new do |output|
|
|
147
|
+
400_000.times do |index|
|
|
148
|
+
output << { "name" => "Product #{index}", "price" => index + 0.5 }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
LiquidXlsx.render(template: "prices.xlsx", output: "prices_out.xlsx",
|
|
153
|
+
data: { "rows" => rows }, streaming: true)
|
|
154
|
+
|
|
155
|
+
# Объектный API: опция в конструкторе или для отдельного вызова.
|
|
156
|
+
template = LiquidXlsx::Template.new("prices.xlsx", streaming: true)
|
|
157
|
+
template.render_to_file(data, "prices_out.xlsx")
|
|
158
|
+
LiquidXlsx::Template.new("prices.xlsx").render_to_file(data, "prices_out.xlsx", streaming: true)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`Template#render`, возвращающий бинарную строку, при `streaming: true`
|
|
162
|
+
выбрасывает `ArgumentError`: потоковый результат нужно писать в файл.
|
|
163
|
+
|
|
164
|
+
Поддерживаются вложенные и последовательные циклы, строки `{% if/elsif/else %}`,
|
|
165
|
+
внутриячеечный Liquid, общий контекст `assign/capture`, стили ячеек и строк,
|
|
166
|
+
высоты, ширины колонок, закрепление и объединения **выше первого цикла**.
|
|
167
|
+
Пустые ячейки без стиля не записываются; `dimension` учитывает записанные
|
|
168
|
+
ячейки и число выведенных строк.
|
|
169
|
+
|
|
170
|
+
До исполнения данных проверяются **все листы и ветки шаблона**. Любая формула
|
|
171
|
+
на листе, объединение в строке первого цикла или ниже (в том числе пересекающее
|
|
172
|
+
его начало), `{% image_tag %}` и `{% sheet %}` вызывают
|
|
173
|
+
`LiquidXlsx::StreamingUnsupportedError` с листом, строкой и ячейкой.
|
|
174
|
+
Объединения через границу условного блока по-прежнему запрещены.
|
|
175
|
+
|
|
176
|
+
Коллекцией цикла может быть любой объект с `each`: `Array`, `Enumerator`,
|
|
177
|
+
`Enumerator::Lazy`, объект приложения или результат метода Drop. Гем вызывает
|
|
178
|
+
`each` один раз на исполнение цикла; `for ... else` определяется по наличию
|
|
179
|
+
выданных элементов. `size`, если он есть, должен возвращать число элементов
|
|
180
|
+
без обхода коллекции. Только целый `Integer` считается известным размером.
|
|
181
|
+
При неизвестном размере доступны `forloop.index`, `index0`, `first`;
|
|
182
|
+
`length`, `last`, `rindex`, `rindex0` дают пустое значение в lenient-режиме и
|
|
183
|
+
`MissingVariableError` с координатами при `strict_variables: true`.
|
|
184
|
+
|
|
185
|
+
Для минимального расхода памяти передавайте Enumerator и Hash со строковыми
|
|
186
|
+
ключами на всех уровнях. Такой Hash элемента передаётся в контекст без копии;
|
|
187
|
+
Hash с другими ключами нормализуется при получении элемента. Массивы коллекций
|
|
188
|
+
заранее не обходятся и не копируются. Уже созданный Array остаётся в памяти
|
|
189
|
+
приложения; потоковый рендер устраняет дополнительное накопление результата.
|
|
190
|
+
Шаблон, shared strings и остальные исходные части книги по-прежнему читаются
|
|
191
|
+
в память; расход памяти зависит от их размера и от одной текущей строки.
|
|
192
|
+
В потоковом режиме выполняется minor GC раз в 16 384 выведенные строки, чтобы
|
|
193
|
+
ограничить временные аллокации при большом входном Array; настройки GC процесса
|
|
194
|
+
не меняются. Для XML строк требуется свободное место во временном каталоге (rubyzip также
|
|
195
|
+
использует временные файлы при сборке архива).
|
|
196
|
+
|
|
197
|
+
Измерение полного рендера до готового файла, с пиковым приростом RSS из
|
|
198
|
+
`/proc/self/status` (`VmHWM`), в отдельном процессе для каждого запуска:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
bundle exec ruby test_data/bench_stream.rb normal array 20000
|
|
202
|
+
bundle exec ruby test_data/bench_stream.rb streaming array 100000
|
|
203
|
+
bundle exec ruby --yjit test_data/bench_stream.rb streaming enumerator 400000
|
|
204
|
+
```
|
|
205
|
+
|
|
134
206
|
## Синтаксис шаблонов
|
|
135
207
|
|
|
136
208
|
Библиотека разделяет два слоя Liquid:
|
|
@@ -221,8 +293,8 @@ LiquidXlsx.render(template: ..., output: ..., data: Report.new("Sales", 42))
|
|
|
221
293
|
либо `#to_liquid`, возвращающий Hash/Drop. Произвольный объект только с
|
|
222
294
|
методами-атрибутами, но без `to_liquid`, работать не будет — Liquid ищет
|
|
223
295
|
переменные через `[]`.
|
|
224
|
-
- Drop-методы, используемые в `{% for %}`, должны возвращать именно `Array`.
|
|
225
|
-
`Enumerable`/`ActiveRecord::Relation` напрямую не поддерживаются —
|
|
296
|
+
- Drop-методы, используемые в `{% for %}`, в обычном режиме должны возвращать именно `Array`.
|
|
297
|
+
`Enumerable`/`ActiveRecord::Relation` в обычном режиме напрямую не поддерживаются —
|
|
226
298
|
материализуйте их через `.to_a` в самом методе.
|
|
227
299
|
- Если Drop-метод возвращает вложенный `Hash`, используйте **строковые** ключи:
|
|
228
300
|
`stringify_keys` рекурсивно нормализует корневой `Hash` (и результат
|
|
@@ -681,6 +753,7 @@ LiquidXlsx.render(
|
|
|
681
753
|
template:,
|
|
682
754
|
output:,
|
|
683
755
|
data:,
|
|
756
|
+
streaming: false, # true — строки результата через временный файл
|
|
684
757
|
strict_variables: false, # true — raise на отсутствующей переменной
|
|
685
758
|
strict_filters: false, # true — raise на неизвестном фильтре
|
|
686
759
|
recalculate_formulas: false, # true — удалить calcChain, форсировать пересчёт
|
|
@@ -696,6 +769,7 @@ LiquidXlsx.render(
|
|
|
696
769
|
|
|
697
770
|
| Опция | По умолчанию | Описание |
|
|
698
771
|
|------------------------|--------------|---------------------------------------------------------|
|
|
772
|
+
| `streaming` | `false` | Потоковый рендер в файл с ограничениями шаблона |
|
|
699
773
|
| `strict_variables` | `false` | При `true` отсутствующая переменная → `MissingVariableError` |
|
|
700
774
|
| `strict_filters` | `false` | При `true` неизвестный фильтр → исключение Liquid |
|
|
701
775
|
| `recalculate_formulas` | `false` | Удалить `calcChain.xml`, форсировать пересчёт в Excel |
|
|
@@ -727,6 +801,72 @@ LiquidXlsx.render(template: "invoice.xlsx", output: "out.xlsx",
|
|
|
727
801
|
на `Liquid::Environment` ничего не регистрируется — глобальная конфигурация
|
|
728
802
|
Liquid в приложении остаётся нетронутой.
|
|
729
803
|
|
|
804
|
+
## Проверка шаблона без рендера
|
|
805
|
+
|
|
806
|
+
`LiquidXlsx.validate` разбирает шаблон и возвращает список всех найденных
|
|
807
|
+
проблем. Данные не нужны, ничего не исполняется, на первой ошибке разбор не
|
|
808
|
+
останавливается.
|
|
809
|
+
|
|
810
|
+
```ruby
|
|
811
|
+
result = LiquidXlsx.validate(
|
|
812
|
+
template: "invoice.xlsx",
|
|
813
|
+
known_filters: MyApp::Filters.public_instance_methods(false), # необязательно
|
|
814
|
+
dynamic_sheets: false # как будете рендерить
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
result.valid? # => false
|
|
818
|
+
result.errors # ошибки: документ не напечатается
|
|
819
|
+
result.warnings # предупреждения: напечатается, но не так, как задумано
|
|
820
|
+
|
|
821
|
+
result.errors.first.code # => :liquid_syntax
|
|
822
|
+
result.errors.first.location # => "Invoice!B12"
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
Почему это не «отрендерить и посмотреть»: рендер разбирает только тот Liquid,
|
|
826
|
+
который реально исполняет. Опечатка в ветке `{% if %}`, которую этот документ не
|
|
827
|
+
выбрал, или в теле цикла по пустой коллекции при пробном рендере не всплывёт — и
|
|
828
|
+
дождётся другого документа. Плюс рендер выполняет шаблон на живых записях, что
|
|
829
|
+
для проверки только что загруженного файла — лишнее.
|
|
830
|
+
|
|
831
|
+
### Что находит
|
|
832
|
+
|
|
833
|
+
| Код | Уровень | Что это |
|
|
834
|
+
|---|---|---|
|
|
835
|
+
| `:invalid_xlsx` | ошибка | файл не читается как книга `.xlsx` |
|
|
836
|
+
| `:missing_worksheet` | ошибка | лист объявлен в `workbook.xml`, а его части в пакете нет |
|
|
837
|
+
| `:template_syntax` | ошибка | непарные структурные теги, тег не на отдельной строке |
|
|
838
|
+
| `:liquid_syntax` | ошибка | Liquid не разберёт фрагмент — рендер упадёт |
|
|
839
|
+
| `:liquid_expression` | предупреждение | Liquid прочтёт выражение иначе, чем написано |
|
|
840
|
+
| `:unknown_filter` | ошибка | фильтра не будет при рендере |
|
|
841
|
+
| `:sheet_tag_disabled` | ошибка | `{% sheet %}` при `dynamic_sheets: false` |
|
|
842
|
+
| `:sheet_template_missing` | ошибка | `{% sheet template: "X" %}`, а листа `X` нет |
|
|
843
|
+
| `:merged_cell_crosses_block` | ошибка | объединение пересекает границу блока |
|
|
844
|
+
| `:liquid_in_formula` | предупреждение | Liquid внутри формулы не вычисляется |
|
|
845
|
+
| `:liquid_in_rich_text` | предупреждение | Liquid в ячейке с разным форматированием не читается |
|
|
846
|
+
|
|
847
|
+
Разница между `:liquid_syntax` и `:liquid_expression` существенна.
|
|
848
|
+
`{{ invoice.total b c }}` и `{{ invoice..total }}` Liquid принимает: печатает
|
|
849
|
+
сумму, а лишнее молча выбрасывает. Документ выйдет, поэтому это предупреждение,
|
|
850
|
+
а не ошибка — но в шаблоне написано не то, что имелось в виду.
|
|
851
|
+
|
|
852
|
+
### Что шаблон использует
|
|
853
|
+
|
|
854
|
+
```ruby
|
|
855
|
+
result.roots # => ["invoice", "customer"] — что шаблон ждёт на входе
|
|
856
|
+
result.variables # => [#<VariableUse root: "invoice", segments: ["items", "title"]>, ...]
|
|
857
|
+
result.filters # => ["money2", "date_words"]
|
|
858
|
+
result.sheets # => ["Invoice", "Приложение"]
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
`roots` — именно входные переменные: имена, которые шаблон заводит сам
|
|
862
|
+
(`{% for item in ... %}`, `{% assign %}`, `{% capture %}`), из списка исключены.
|
|
863
|
+
Так приложение может сверить шаблон со своей моделью данных, не разбирая Liquid
|
|
864
|
+
самостоятельно.
|
|
865
|
+
|
|
866
|
+
У пути есть признак `truncated?`: в `{{ items[i].title }}` за динамическим
|
|
867
|
+
индексом проверять нечего, и об этом сказано явно — чтобы вызывающий код не
|
|
868
|
+
объявил «нет такого поля» там, где поле определяется в рантайме.
|
|
869
|
+
|
|
730
870
|
## Обработка ошибок
|
|
731
871
|
|
|
732
872
|
Все ошибки наследуются от `LiquidXlsx::Error < StandardError`. Там, где
|
|
@@ -739,7 +879,9 @@ LiquidXlsx::Error
|
|
|
739
879
|
├── RenderError ошибка рендера (включая {% sheet %}/{% image_tag %}),
|
|
740
880
|
│ └── MissingVariableError при strict_variables: true
|
|
741
881
|
├── UnsupportedTemplateError merge через границу блока, неизвестный формат
|
|
742
|
-
│
|
|
882
|
+
│ │ картинки, отсутствие template-листа
|
|
883
|
+
│ └── StreamingUnsupportedError формулы, images, sheet или merges ниже цикла
|
|
884
|
+
│ в потоковом режиме
|
|
743
885
|
├── InvalidXlsxError битый .xlsx на входе
|
|
744
886
|
└── OutputWriteError ошибка записи выходного файла
|
|
745
887
|
```
|
|
@@ -830,7 +972,7 @@ LiquidXlsx.render(
|
|
|
830
972
|
|
|
831
973
|
```bash
|
|
832
974
|
bundle install # установка зависимостей
|
|
833
|
-
bundle exec rspec # полный набор тестов
|
|
975
|
+
bundle exec rspec # полный набор тестов
|
|
834
976
|
bundle exec rspec spec/integration/ # только интеграционные
|
|
835
977
|
bundle exec rubocop # линтер
|
|
836
978
|
bundle exec rubocop -a # автоисправление
|
data/lib/liquid_xlsx/errors.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
module LiquidXlsx
|
|
6
|
+
# A worksheet envelope around rows on disk. Ownership passes to Package;
|
|
7
|
+
# neither the complete XML nor its ZIP input is accumulated in memory.
|
|
8
|
+
class FilePart
|
|
9
|
+
attr_reader :file
|
|
10
|
+
|
|
11
|
+
def initialize(file, prefix, suffix)
|
|
12
|
+
@file = file
|
|
13
|
+
@prefix = prefix
|
|
14
|
+
@suffix = suffix
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def bytesize
|
|
18
|
+
@prefix.bytesize + @file.size + @suffix.bytesize
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write_to(output)
|
|
22
|
+
output.write(@prefix)
|
|
23
|
+
@file.rewind
|
|
24
|
+
IO.copy_stream(@file, output)
|
|
25
|
+
output.write(@suffix)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def close
|
|
29
|
+
@file.close!
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/liquid_xlsx/package.rb
CHANGED
|
@@ -123,6 +123,11 @@ module LiquidXlsx
|
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
# Release owned worksheet files on success and on any render/write failure.
|
|
127
|
+
def close_file_parts
|
|
128
|
+
@files.each_value { |content| content.close if content.is_a?(FilePart) }
|
|
129
|
+
end
|
|
130
|
+
|
|
126
131
|
# Write modified content to a binary string.
|
|
127
132
|
# @return [String]
|
|
128
133
|
def to_binary
|
|
@@ -462,13 +467,25 @@ module LiquidXlsx
|
|
|
462
467
|
# content it would disagree with what actually gets written.
|
|
463
468
|
# See ZIP_SIZE_KEYWORD — rubyzip 2.x has no such keyword and needs none.
|
|
464
469
|
def write_entry(zip, name, content)
|
|
465
|
-
if
|
|
470
|
+
if content.is_a?(FilePart)
|
|
471
|
+
write_file_entry(zip, name, content)
|
|
472
|
+
elsif ZIP_SIZE_KEYWORD
|
|
466
473
|
zip.get_output_stream(name, size: content.bytesize) { |f| f.write(content) }
|
|
467
474
|
else
|
|
468
475
|
zip.get_output_stream(name) { |f| f.write(content) }
|
|
469
476
|
end
|
|
470
477
|
end
|
|
471
478
|
|
|
479
|
+
def write_file_entry(zip, name, content)
|
|
480
|
+
if ZIP_SIZE_KEYWORD
|
|
481
|
+
zip.get_output_stream(name, size: content.bytesize) { |f| content.write_to(f) }
|
|
482
|
+
else
|
|
483
|
+
entry = Zip::Entry.new("", name)
|
|
484
|
+
entry.size = content.bytesize
|
|
485
|
+
zip.get_output_stream(entry) { |f| content.write_to(f) }
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
472
489
|
# Open a brand-new archive for writing across rubyzip 2.x and 3.x.
|
|
473
490
|
# See ZIP_CREATE_KEYWORD for why the call has to be spelled two ways.
|
|
474
491
|
def open_new_zip(output_path, &)
|
data/lib/liquid_xlsx/renderer.rb
CHANGED
|
@@ -741,18 +741,10 @@ module LiquidXlsx
|
|
|
741
741
|
|
|
742
742
|
private
|
|
743
743
|
|
|
744
|
+
# Shared with Validator: the renderer only sees blocks it executes, the
|
|
745
|
+
# validator sees all of them, and the two must judge a crossing the same way.
|
|
744
746
|
def crosses_boundary?(merge_start, merge_end, block_start, block_end)
|
|
745
|
-
|
|
746
|
-
return false if merge_end < block_start || merge_start > block_end
|
|
747
|
-
|
|
748
|
-
# Merge strictly inside the block body (between the tag rows)
|
|
749
|
-
return false if merge_start > block_start && merge_end < block_end
|
|
750
|
-
|
|
751
|
-
# Merge entirely on a single structural tag row (it is consumed with it)
|
|
752
|
-
return false if merge_start == merge_end && (merge_start == block_start || merge_start == block_end)
|
|
753
|
-
|
|
754
|
-
# Any other overlap = crossing (including merges that swallow a tag row)
|
|
755
|
-
true
|
|
747
|
+
Validation::BlockBoundary.crosses?(merge_start, merge_end, block_start, block_end)
|
|
756
748
|
end
|
|
757
749
|
|
|
758
750
|
def clone_merge(ref, start_offset, end_offset)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiquidXlsx
|
|
4
|
+
# Liquid does not define Object#to_liquid for arbitrary enumerables. Adapt
|
|
5
|
+
# only values read from the context, without mutating caller-owned hashes.
|
|
6
|
+
class StreamingContext < Liquid::Context
|
|
7
|
+
class Collection
|
|
8
|
+
def initialize(source)
|
|
9
|
+
@source = source
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_liquid
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def each(&)
|
|
17
|
+
@source.each(&)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def size
|
|
21
|
+
@source.size if @source.respond_to?(:size)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def lookup_and_evaluate(obj, key, **options)
|
|
26
|
+
value = super
|
|
27
|
+
if !value.respond_to?(:to_liquid) && value.respond_to?(:each)
|
|
28
|
+
Collection.new(value)
|
|
29
|
+
else
|
|
30
|
+
value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|